背景

Cloudflare Workers 是 Cloudflare 提供的 Serverless 运行平台,代码运行在全球 150+ 个数据中心的边缘节点上。它原生支持 JavaScript,而随着 WebAssembly(WASM)支持的加入,Rust 开发者也可以把自己的代码编译成 WASM,部署到这套平台上运行。

这篇文章以一个实际项目为例,介绍如何把 Rust 代码编译为 WASM,先在本地浏览器中跑通,再上传到 Workers 作为 Serverless 函数对外提供服务。

原文地址:https://blog.cloudflare.com/cloudflare-workers-as-a-serverles...


什么时候适合用 WASM

在开始之前,有一点需要明确:WASM 不是万能的。

Cloudflare 官方文档中有一段很务实的说法:对于轻量任务,比如做一次请求重定向、校验一个 Token,纯 JavaScript 往往比 WASM 更快、更简单。原因在于 WASM 运行在独立的内存空间里,数据进出都需要拷贝,如果代码本身没有密集的计算,引入 WASM 反而会带来额外开销。

WASM 真正发挥优势的场景是计算密集型任务:图像处理、加解密、复杂的字符串操作等。


环境搭建

Rust 的 WASM 工具链目前已经相当成熟,核心工具是 wasm-pack

# 安装 wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

# 安装 cargo-generate,用于基于模板创建项目
cargo install cargo-generate

# 用官方模板创建一个新项目
cargo generate --git https://github.com/rustwasm/wasm-pack-template

wasm-pack 的作用是把 Rust 代码编译成 WebAssembly,同时生成 JavaScript 和 Rust 之间的类型绑定(binding)。这个绑定层很关键,后面会详细说。


代码结构:#[wasm_bindgen]

模板项目的核心模式是这样的:

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet() {
    alert("Hello from Rust!");
}

这里做了两件事:

第一,通过 extern 块声明外部函数——也就是宿主环境(浏览器或 Workers)提供的函数,比如 alert

第二,用 #[wasm_bindgen] 标注的 pub fn,会被暴露出去,让 JavaScript 侧可以直接调用,就像调用普通 JS 函数一样。

编译命令:

wasm-pack build

编译产物在 pkg/ 目录下,包含 .wasm 二进制文件和自动生成的 JS 胶水代码。


本地验证

编译完成后,先不急着上线,在本地浏览器里跑通是个好习惯。

npm 上有一个 create-wasm-app 模板,提供了一个预配置好 webpack 的测试页面,可以直接 import WASM 模块:

npm init wasm-app www
cd www
npm install
npm start

浏览器访问 http://localhost:8080,如果看到页面正常渲染,说明 WASM 模块加载成功。

把自己的 wasm 包用 npm link 关联进来,修改 www/index.js,调用自己的函数:

import * as wasm from "my-wasm-module";
let result = wasm.get_phrase_text(100, 10);
console.log(result);

一个坑:系统调用在 WASM 里不可用

作者在实际开发中踩了一个典型的坑:用 Rust 的随机数库 SmallRng::from_entropy() 时,本地 cargo test 完全正常,但在浏览器里跑 WASM 时直接崩溃。

原因是 from_entropy() 底层依赖系统调用来获取熵值,而 WASM 的编译目标是 wasm32-unknown-unknown——这个 unknown 意味着目标平台不保证提供任何系统调用。编译器不报错,但运行时会直接失败。

解决方案是换掉系统级调用,改用 JavaScript 宿主提供的 Web API。Rust 有一个 js-sys crate,封装了标准 ECMAScript 提供的所有全局对象,其中包括 Date

fn get_rng() -> SmallRng {
    use js_sys::Date;
    use rand::SeedableRng;

    let ticks = Date::now();
    let tick_bytes = transmute(ticks as u128);
    SmallRng::from_seed(tick_bytes)
}

Date.now() 作为种子,绕开了系统调用,问题解决。

这个经验值得记住:在 WASM 环境下,任何涉及 I/O、系统熵、文件系统、时间获取的操作,都需要通过宿主环境(JS)来代理,不能直接走 Rust 标准库的对应实现。


上传到 Workers

本地浏览器跑通之后,接下来把 .wasm 文件上传到 Cloudflare Workers。

上传时把 WASM 绑定到一个全局变量(比如 BOBROSS_WASM),Workers 运行时会在 Worker 脚本启动时自动实例化这个模块。

但这里有一个需要手动处理的地方:wasm-pack build 生成的 JS 胶水代码是为浏览器环境写的(使用了 ES module 的 import/export 语法),Workers 的 WASM 实例化方式和浏览器略有不同,需要做几处改造:

  • 删除顶部的 import 语句
  • 去掉函数的 export 关键字
  • 把所有函数包进一个模块对象
  • 构造 importObject,把需要注入的外部函数传进去
  • 在创建 WebAssembly.Instance 时传入这个 importObject

改造完成后,在 Worker 脚本里调用 Rust 函数的方式和调用普通 JavaScript 函数没有区别:

async function handleRequest(request) {
    let url = new URL(request.url);
    let phraseCount = parseInt(url.searchParams.get("phrases") || 100);
    let newLine = parseInt(url.searchParams.get("newline") || 0);

    // 调用 Rust 编译的 WASM 函数
    let phraseText = mod.get_phrase_text(phraseCount, newLine);
    return new Response(phraseText);
}

请求进来,Rust 函数被调用,结果直接返回——运行在全球 150+ 个边缘节点上。


整体流程回顾

Rust 源码
    ↓ wasm-pack build
.wasm 二进制 + JS 胶水代码
    ↓ 本地 npm link + webpack 测试页
浏览器验证通过
    ↓ 手动改造 JS 胶水代码(适配 Workers)
上传 .wasm + Worker 脚本到 Cloudflare
    ↓
全球边缘节点运行

小结

这篇博客展示的是一条完整的路径:Rust 代码 → WASM → Cloudflare Workers。整个工具链在当时(2018年)还比较初期,需要手动处理 JS 胶水代码的适配。现在 Cloudflare 已经提供了 Wrangler CLI,把这些工作都封装进去了,流程更加顺滑。

几个值得关注的核心点:

WASM 不是银弹,轻量逻辑用 JS 就好,WASM 适合计算密集型场景。

系统调用在 WASM 里不可用,需要通过 js-sys 等工具桥接宿主环境的 Web API。

胶水代码是关键中间层wasm-bindgen 自动处理了 JS 和 Rust 之间的类型转换和内存管理,理解它的工作方式对排查问题很有帮助。

Serverless + Rust + WASM 这条路是通的,而且随着工具链的持续完善,门槛在逐步降低。

标签: none

添加新评论