背景
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。
bash
# 安装 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]
模板项目的核心模式是这样的:
rust
#[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 函数一样。
编译命令:
bash
wasm-pack build
编译产物在 pkg/ 目录下,包含 .wasm 二进制文件和自动生成的 JS 胶水代码。
本地验证
编译完成后,先不急着上线,在本地浏览器里跑通是个好习惯。
npm 上有一个 create-wasm-app 模板,提供了一个预配置好 webpack 的测试页面,可以直接 import WASM 模块:
bash
npm init wasm-app www
cd www
npm install
npm start
浏览器访问 http://localhost:8080,如果看到页面正常渲染,说明 WASM 模块加载成功。
把自己的 wasm 包用 npm link 关联进来,修改 www/index.js,调用自己的函数:
javascript
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:
rust
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 函数没有区别:
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 这条路是通的,而且随着工具链的持续完善,门槛在逐步降低。