导语:JS 与 WASM 之间的桥梁
WebAssembly 本身是一个"黑盒"计算模块,它只能做纯计算——不能访问 DOM、不能发起 HTTP 请求、不能读写文件。这意味着 WASM 与外界的所有交互都必须通过 JavaScript 进行。
JS ↔ WASM 的互操作有三条通道:
- 函数导入/导出:最直接的方式,JS 调用 WASM 导出的函数,WASM 调用 JS 提供的 import 函数
- 线性内存共享:JS 和 WASM 共享同一块
ArrayBuffer内存区域 - wasm-bindgen:Rust 生态提供的自动化绑定层,大幅减少样板代码
一句话总结:JS 是 WASM 的"操作系统 "——WASM 需要的能力(DOM、I/O、网络)全部通过 JS 注入。
1. JS API 核心:WebAssembly 对象
浏览器环境中的 WebAssembly 对象提供了四个核心构造函数和三个关键方法:
// ========== 核心构造函数 ==========
// 1. WebAssembly.Module — 已编译/校验的 WASM 模块(可序列化、多实例复用)
const module = new WebAssembly.Module(wasmBytes);
// 2. WebAssembly.Instance — 模块的一个运行时实例(含内存和执行状态)
const instance = new WebAssembly.Instance(module, importObject);
// 3. WebAssembly.Memory — 可扩容的线性内存
const memory = new WebAssembly.Memory({ initial: 1, maximum: 10 });
// 4. WebAssembly.Table — 间接函数调用表
const table = new WebAssembly.Table({ initial: 2, element: 'anyfunc' });
// ========== 异步方法 ==========
// 编译 + 实例化一步完成
const { module, instance } = await WebAssembly.instantiateStreaming(
fetch('/module.wasm'),
importObject
);
// 仅校验和编译(可用于缓存)
const module = await WebAssembly.compileStreaming(fetch('/module.wasm'));
// 使用缓存模块创建实例
const instance = await WebAssembly.instantiate(module, importObject);
一句话总结:
Module是"代码",Instance是"进程",Memory是"共享内存",三者协作构成完整的 WASM 运行时生命周期。
2. 导入对象(importObject)详解
WASM 模块通过导入对象获取外部能力,这是安全沙箱的核心体现——模块只能使用显式注入的能力。
const importObject = {
// env 命名空间(对应 Rust 的 #[no_mangle] + extern "C" 导出)
env: {
// 内存注入:JS 和 WASM 共享同一块内存
memory: new WebAssembly.Memory({ initial: 10 }),
// 表注入:用于函数指针/动态分发
__indirect_function_table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }),
// 自定义函数注入:WASM 可以调用 JS 函数
abort: (msgPtr, filePtr, line, col) => {
console.error(`WASM abort at ${line}:${col}`);
throw new Error('WASM abort');
},
// 提供 console.log 能力
js_log: (ptr, len) => {
const bytes = new Uint8Array(memory.buffer, ptr, len);
const text = new TextDecoder().decode(bytes);
console.log('[WASM]', text);
},
// 提供 Date.now()
js_now: () => Date.now(),
// 提供 Math 函数
js_sin: Math.sin,
},
// wasi_snapshot_preview1 命名空间(WASI 标准接口)
wasi_snapshot_preview1: {
fd_write: (fd, iovs, iovs_len, bytes_written) => {
// 实现标准输出重定向
// ...
},
// ...更多 WASI 接口
},
};
一句话总结:importObject 是 WASM 与外界的"能力契约"——模块声明需要什么,宿主决定给什么。
3. 线性内存共享机制
线性内存是 JS 与 WASM 交换数据的唯一手段,理解它是实现高效互操作的关键。
3.1 基础操作
// WASM 侧:Rust 代码
#[no_mangle]
pub extern "C" fn get_data_ptr() -> *mut u8 {
static mut BUFFER: [u8; 1024] = [0; 1024];
unsafe { BUFFER.as_mut_ptr() }
}
#[no_mangle]
pub extern "C" fn process_data(ptr: *mut u8, len: usize) -> i32 {
let slice = unsafe { std::slice::from_raw_parts_mut(ptr, len) };
// 处理数据...
slice.len() as i32
}
// JS 侧:读写共享内存
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/module.wasm'),
{ env: { memory: new WebAssembly.Memory({ initial: 1 }) } }
);
// 获取 WASM 分配的数据指针
const ptr = instance.exports.get_data_ptr();
// 写入数据
const text = "Hello from JS";
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
const wasmMemory = new Uint8Array(instance.exports.memory.buffer);
wasmMemory.set(bytes, ptr);
// 调用 WASM 处理
const result = instance.exports.process_data(ptr, bytes.length);
console.log('Processed:', result);
3.2 复杂数据结构设计
对于更大的数据结构,一种常见模式是定义共享的数据布局:
// 共享的数据头结构(4字节对齐)
// offset +0: 数据长度(u32)
// offset +4: 实际数据起始
function writeString(memory, ptr, text) {
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
const view = new DataView(memory.buffer);
view.setUint32(ptr, bytes.length, true); // 写入长度
new Uint8Array(memory.buffer, ptr + 4).set(bytes); // 写入数据
}
function readString(memory, ptr) {
const view = new DataView(memory.buffer);
const len = view.getUint32(ptr, true);
const bytes = new Uint8Array(memory.buffer, ptr + 4, len);
return new TextDecoder().decode(bytes);
}
3.3 内存扩容注意事项
WASM 内存扩容时会创建一个新的 ArrayBuffer(旧引用失效):
let memory = instance.exports.memory;
let buffer = memory.buffer; // 原始引用
// WASM 中调用 memory.grow(1) 后:
// buffer 不再有效,需要重新获取
buffer = memory.buffer; // ← 必须重新获取引用
一句话总结:线性内存是 JS/WASM 共享的"进程内存",
Uint8Array/DataView是两边的读写工具,扩容时必须重新获取引用。
4. 使用 wasm-bindgen 简化绑定
手动管理内存和类型转换非常繁琐,wasm-bindgen 自动处理这些细节。
4.1 基本用法
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}! 👋", name)
}
#[wasm_bindgen]
pub struct Point {
x: f64,
y: f64,
}
#[wasm_bindgen]
impl Point {
#[wasm_bindgen(constructor)]
pub fn new(x: f64, y: f64) -> Point {
Point { x, y }
}
pub fn distance_to(&self, other: &Point) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
#[wasm_bindgen(getter)]
pub fn x(&self) -> f64 { self.x }
#[wasm_bindgen(setter)]
pub fn set_x(&mut self, x: f64) { self.x = x; }
}
4.2 高级特性:回调与闭包
#[wasm_bindgen]
pub fn run_callback(callback: &js_sys::Function) {
let this = JsValue::NULL;
let arg = JsValue::from_str("来自 WASM 的消息");
let _ = callback.call1(&this, &arg);
}
// 使用 Closure 持有闭包(避免 GC)
use wasm_bindgen::closure::Closure;
#[wasm_bindgen]
pub fn set_interval(ms: u32, callback: js_sys::Function) {
let closure = Closure::wrap(Box::new(move || {
callback.call0(&JsValue::NULL).unwrap();
}) as Box<dyn FnMut()>);
web_sys::window()
.unwrap()
.set_interval_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
ms as i32,
)
.unwrap();
// ⚠️ 必须 forget,否则闭包会被释放
closure.forget();
}
4.3 与 web-sys 交互
#[wasm_bindgen(start)]
pub fn main() {
let window = web_sys::window().expect("no window");
let document = window.document().expect("no document");
let body = document.body().expect("no body");
// 从 Rust 直接操作 DOM
let val = document.create_element("p").unwrap();
val.set_text_content(Some("Hello from Rust WASM!"));
body.append_child(&val).unwrap();
// 监听事件
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
web_sys::console::log_1(&format!("点击位置: ({}, {})", event.client_x(), event.client_y()).into());
}) as Box<dyn FnMut(_)>);
document.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref()).unwrap();
closure.forget();
}
一句话总结:
wasm-bindgen自动桥接类型系统和内存管理,web-sys则让 Rust 代码像 JS 一样操作 DOM。
5. 异步互操作模式
5.1 JS Promise → Rust Future
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::Response;
#[wasm_bindgen]
pub async fn fetch_data(url: String) -> Result<String, JsValue> {
let window = web_sys::window().unwrap();
// 发起 fetch
let resp_value = JsFuture::from(window.fetch_with_str(&url)).await?;
let resp: Response = resp_value.dyn_into()?;
// 读取文本
let text = JsFuture::from(resp.text()?).await?;
let text = text.as_string().unwrap_or_default();
Ok(text)
}
5.2 Rust async → JS Promise
wasm-bindgen 自动将 async fn 转换为返回 Promise 的 JS 函数,无需额外操作。
// JS 侧
const rustLib = await import('./pkg/rust_lib.js');
const data = await rustLib.fetch_data('https://api.example.com/data');
console.log(data);
一句话总结:
wasm-bindgen-futures让 JS Promise 和 Rust Future 互通,async fn自动导出为 JSasync function。
6. 常见陷阱与最佳实践
6.1 内存管理
// ❌ 错误:返回指向栈数据的指针
#[wasm_bindgen]
pub fn bad() -> *const u8 {
let local = vec![1, 2, 3];
local.as_ptr() // local 在函数返回后释放,指针悬空!
}
// ✅ 正确:使用 Box 或 wasm-bindgen 管理类型
#[wasm_bindgen]
pub fn good() -> Box<[u8]> {
vec![1, 2, 3].into_boxed_slice()
}
// ✅ 或:使用 wasm-bindgen 返回 String
#[wasm_bindgen]
pub fn better() -> String {
"安全返回".to_string()
}
6.2 大数据传递
对于图像、音频等大数据(MB 级别),应通过线性内存传递,而非序列化为 JSON:
#[wasm_bindgen]
pub struct ImageProcessor {
buffer: Vec<u8>,
width: u32,
height: u32,
}
#[wasm_bindgen]
impl ImageProcessor {
#[wasm_bindgen(constructor)]
pub fn new(ptr: *const u8, len: usize, w: u32, h: u32) -> Self {
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
ImageProcessor {
buffer: slice.to_vec(),
width: w,
height: h,
}
}
// 同样通过内存指针返回处理结果
pub fn process(&self, out_ptr: *mut u8) {
let out = unsafe { std::slice::from_raw_parts_mut(out_ptr, self.buffer.len()) };
for (i, p) in self.buffer.iter().enumerate() {
out[i] = p.saturating_add(10); // 简单亮度调整
}
}
}
一句话总结:WASM 互操作的核心设计原则是"用线性内存传大数据,用 wasm-bindgen 传控制逻辑",避免不必要的序列化开销。
7. 总结
JS ↔ WASM 互操作的三个层次:
- 原始 JS API:
WebAssembly.*+ importObject + 手动内存管理——最灵活、最繁琐 - wasm-bindgen 自动化:自动生成类型转换和内存管理的桥梁代码——推荐用于大多数场景
- wasm-bindgen + web-sys:为 Rust 提供完整的 Web API 绑定——让 Rust 像 TS 一样写前端逻辑
关键原则:
- WASM 侧负责纯计算,JS 侧负责 I/O 和渲染
- 大数据走线性内存共享,小数据用 wasm-bindgen 自动转换
- 异步操作通过
wasm-bindgen-futures桥接 Promise + Future
参考阅读:
- Rust 编译到 WebAssembly 完全指南 — wasm-pack 和开发环境
- WASM 性能优化实战 — 体积压缩和 SIMD 加速
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。