如何导出函数给 JS?
解读
在国内前端与底层性能模块混合开发的场景里,**“把 Rust 函数导出给 JavaScript 调用”**几乎成了 Rust 岗面试的“必答题”。面试官真正想确认的是:
- 你是否理解 WebAssembly 的 FFI 边界规则;
- 能否熟练使用 wasm-bindgen 这一套“事实标准”工具链;
- 是否知道 ABI 可移植性、内存布局、异常安全、包体积优化 这些上线后必踩的坑。
回答时切忌只背“#[wasm_bindgen]”这一行宏,而要展示“编译→绑定→发布→运行时调试”的完整闭环经验。
知识点
- wasm32-unknown-unknown 目标三元组与 LLVM 后端
- wasm-bindgen 宏展开:生成 .wasm、_bg.wasm、.js、.d.ts 四件套
- #[wasm_bindgen] 属性:导出名、重命名、skip_typescript、getter/setter
- js-sys / web-sys:对 JS 全局对象与 DOM API 的零成本绑定
- serde-wasm-bindgen:把 Rust 的 struct/enum 与 JS Object 互转,避免手写序列化
- wasm-pack 工作流:build、test、pack、publish 到 npm 与字节内部 Nexus
- wee_alloc / lol_alloc:替换默认 jemalloc,把体积压到 10 KB 级
- wasm-opt / wasm-strip / wasm-snip:二进制体积与启动速度优化
- 异步边界:Rust Future ↔ JS Promise,#[wasm_bindgen(async)] 与 wasm-bindgen-futures
- 异常传播:Rust Result<T, E> 经 JsValue::from(err) 映射到 JS Error 事件
- 多线程:SharedArrayBuffer + Atomics 前提下的 wasm-bindgen-rayon 方案
- 国内部署:CDN 对
application/wasmMIME 的支持、Gzip/Br 压缩、HTTP 缓存策略
答案
-
环境准备
rustup target add wasm32-unknown-unknown cargo install wasm-pack -
新建库工程
cargo new --lib my_math cd my_math -
Cargo.toml 关键段
[lib] crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2" -
核心代码 src/lib.rs
use wasm_bindgen::prelude::*; // 导出普通函数 #[wasm_bindgen] pub fn add(a: i32, b: i32) -> i32 { a + b } // 导出结构体 + 方法 #[wasm_bindgen] pub struct Calculator { value: f64, } #[wasm_bindgen] impl Calculator { #[wasm_bindgen(constructor)] pub fn new() -> Self { Calculator { value: 0.0 } } pub fn add(&mut self, v: f64) { self.value += v; } pub fn result(&self) -> f64 { self.value } } -
一键构建
wasm-pack build --target web --out-dir pkg产物:
- my_math_bg.wasm(二进制)
- my_math.js(ES 模块胶水)
- my_math.d.ts(TypeScript 声明)
-
前端使用(国内 Vite / Webpack 5 均支持)
import init, { add, Calculator } from './pkg/my_math.js'; async function run() { await init(); // 加载并实例化 wasm console.log('3 + 4 =', add(3, 4)); const calc = new Calculator(); calc.add(10); calc.add(32); console.log('calc result:', calc.result()); } run(); -
体积与性能优化
- 在 Cargo.toml 加
wee_alloc特征,体积立减 ~60 KB; - 用
wasm-opt -Oz再压 15~30%; - 开启
wasm-pack build --weak-refs --reference-types利用最新 GC 提案,减少 JS 胶水; - 若接口多,开启
wasm-snip删除 panic 路径,进一步瘦身。
- 在 Cargo.toml 加
-
单元测试
wasm-pack test --headless --chrome保证 CI 里跑通,再合并主干——国内大厂对“无 headless 测试不发布”是硬性红线。
拓展思考
-
宿主不止浏览器:
在 Node.js 端同样wasm-pack build --target nodejs,但注意 NAPI 与 WASI 两套生态的竞争;字节内部 Serverless 平台已支持 WASI preview2,可直接把 Rust 函数当“冷启动 <50 ms”的轻量服务发布,无需 JS 胶水。 -
复杂数据传递:
若需要把 Vec<复杂结构>、HashMap、BigInt 传给 JS,手写#[wasm_bindgen]会爆炸,推荐 serde-wasm-bindgen 一行to_value(&rust_data)?解决,性能损耗 <5%,却省掉大量维护成本。 -
异步与流式:
对 大文件上传、图片解码 场景,用wasm-bindgen-futures把 Rust Stream 转成 JS AsyncIterator,前端可以for await (const chunk of rustStream)逐片消费,避免一次性把 200 MB 数据拷进 WASM 线性内存。 -
调试与错误上报:
国内线上环境常禁用console.log,务必在 Rust 侧用web_sys::console::error_1(&err);把堆栈打到 Sentry,并开启wasm-pack build --debug保留函数名,方便 Sentry 解析 source-map 级别的符号。 -
安全与合规:
WASM 模块一旦通过postMessage传给 WebWorker,就跨越了 同源隔离;金融类业务需评估 SharedArrayBuffer + Spectre 风险,必要时降级到cross-origin-isolated头部或干脆回退到 JS 实现。
掌握以上“编译→绑定→优化→调试→上线”全链路,面试时就能从“会用 #[wasm_bindgen]”跃迁到“能带团队落地百万 QPS 的 Rust-WASM 网关”,稳稳拿到 Offer。