背景
Cloudflare Workers 是 Cloudflare 提供的 Serverless 运行平台,代码运行在全球 150+ 个数据中心的边缘节点上。它原生支持 JavaScript,而随着 WebAssembly(WASM)支持的加入,Rust 开发者也可以把自己的代码编译成 WASM,部署到这套平台上运行。
这篇文章以一个实际项目为例,介绍如何把 Rust 代码编译为 WASM,先在本地浏览器中跑通,再上传到 Workers 作为 Serverless 函数对外提供服务。
原文地址:https://blog.cloudflare.com/cloudflare-workers-as-a-serverless-rust-platform/
什么时候适合用 WASM
在开始之前,有一点需要明确:WASM 不是万能的。
Cloudflare 官方文档中有一段很务实的说法:对于轻量任务,比如做一次请求重定向、校验一个 Token,纯 JavaScript 往往比 WASM 更快、更简单。原因在于 WASM 运行在独立的内存空间里,数据进出都需要拷贝,如果代码本身没有密集的计算,引入 WASM 反而会带来额外开销。
WASM 真正发挥优势的场景是计算密集型任务:图像处理、加解密、复杂的字符串操作等。
环境搭建
Rust 的 WASM 工具链目前已经相当成熟,核心工具是wasm-pack。
# 安装 wasm-packcurlhttps://rustwasm.github.io/wasm-pack/installer/init.sh-sSf|sh# 安装 cargo-generate,用于基于模板创建项目cargoinstallcargo-generate# 用官方模板创建一个新项目cargogenerate--githttps://github.com/rustwasm/wasm-pack-templatewasm-pack的作用是把 Rust 代码编译成 WebAssembly,同时生成 JavaScript 和 Rust 之间的类型绑定(binding)。这个绑定层很关键,后面会详细说。
代码结构:#[wasm_bindgen]
模板项目的核心模式是这样的:
#[wasm_bindgen]extern{fnalert(s:&str);}#[wasm_bindgen]pubfngreet(){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 模块:
npminit wasm-app wwwcdwwwnpminstallnpmstart浏览器访问http://localhost:8080,如果看到页面正常渲染,说明 WASM 模块加载成功。
把自己的 wasm 包用npm link关联进来,修改www/index.js,调用自己的函数:
import*aswasmfrom"my-wasm-module";letresult=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-syscrate,封装了标准 ECMAScript 提供的所有全局对象,其中包括Date:
fnget_rng()->SmallRng{usejs_sys::Date;userand::SeedableRng;letticks=Date::now();lettick_bytes=transmute(ticksasu128);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 函数没有区别:
asyncfunctionhandleRequest(request){leturl=newURL(request.url);letphraseCount=parseInt(url.searchParams.get("phrases")||100);letnewLine=parseInt(url.searchParams.get("newline")||0);// 调用 Rust 编译的 WASM 函数letphraseText=mod.get_phrase_text(phraseCount,newLine);returnnewResponse(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 这条路是通的,而且随着工具链的持续完善,门槛在逐步降低。