JavaScript 与 WebAssembly 怎么对话 — 类型、内存、wasm-bindgen¶
1. 这一章讲什么¶
上一章把模块跑起来了,这一章专讲跑起来之后最难的那部分:两边怎么交换数据。 Wasm 的世界里只有四种数,JS 的世界里什么都有;Wasm 的内存没人帮你收,JS 的垃圾回收器全包。 这一章把这条边界上的每一个坑都摆出来:类型怎么转、内存谁分配、字符串和对象怎么过去、 出错会炸成什么样、wasm-bindgen 如何把这些手工活全自动化。1
2. 顶层全景¶
JS 调 Wasm:instance.exports.xxx(参数) —— 标量自动转,复合数据先写进线性内存
│
│ 数据本体:线性内存(共享,零拷贝)
│ 地址语言:指针(i32 偏移)+ 长度
▼
Wasm 调 JS:import 对象里塞 JS 函数 —— Wasm 通过 externref/funcref 拿到引用
│
│ 内存管理:没有 GC,模块内 malloc(size) /free(ptr) 手工记账
│ 自动化:wasm-bindgen 替你生成「翻译层」,字符串、数组直接传
▼
出错:编译失败 CompileError / 实例化缺 import 直接炸 / 运行期越界 RuntimeError
图说:三条通路(调进、调出、内存)+ 一个翻译器(wasm-bindgen),就是全章地图。
3. 核心原理¶
3.1 两种类型系统,中间隔着一条沟¶
Wasm 是静态类型、低层的:全部家当就是四个数——32 位整数 i32、64 位整数 i64、
32 位浮点 f32、64 位浮点 f64(按 IEEE 754 标准);外加 WebAssembly 2.0 引入的
funcref/externref 两种引用类型。没有字符串、没有对象、没有数组——
一切复杂数据都是线性内存里的原始字节。2
JS 是动态类型、高层的:number、string、boolean、object、array 什么都有,
而且它的 number 一律是 64 位浮点(标准名 double)。3
这条沟具体在哪,书里点了两处最疼的:4
- 精度:
i64的整数超过 2 的 53 次方时,JS 的 Number 装不下,会悄悄截断—— 做密码学、大整数必须换BigInt(JS 里表示任意精度整数的类型)来传; - 引用类型:
externref让 Wasm 可以「拿着」一个 JS 对象而不复制它—— 这是 2.0 提案里最实用的互操作新能力。
3.2 数据怎么过去:三条路,按数据量选¶
| 数据 | 走哪条路 | 成本 |
|---|---|---|
| 单个数(整数/浮点) | 直接当参数传,JS API 自动按规则转(Number → i32/f64,BigInt → i64) | 几乎为零 |
| 数组、图像这类批量字节 | 序列化进线性内存(类型化数组的 setInt32/setFloat32),把指针 + 长度当参数传 |
低(零拷贝) |
| 嵌套对象、结构体 | JSON.stringify 成字符串再走字节路(JSON 是把对象转成文本的通用格式);或交给 wasm-bindgen / serde 这类工具 |
高(解析开销) |
核心动作是第二个:传的不是数据,是「地址 + 长度」。Wasm 函数拿到指针,
自己去线性内存里读;结果也写在内存里,JS 按同样的地址读回。
书里强调这条路要在两端都做边界检查——Wasm 的校验器挡静态越界,
JS 侧则要拿偏移量和 memory.buffer.byteLength 比一比,防动态越界。5
3.3 线性内存:一份,手工,可封顶¶
和第 3 章衔接,这里补三个模型层的细节:6
- 一个实例最多声明一块内存(多内存支持在提案路上);
- 这块内存在多线程下是
SharedArrayBuffer,单线程下是ArrayBuffer—— 都是一个意思:JS 和 Wasm 零拷贝共享; - 没有垃圾回收:变量、数组、结构体占的每字节都得自己安排。
JS 侧最小读写:const view = new Uint32Array(instance.exports.memory.buffer); view[0] = 42;
——往 0 号格子写一个整数,Wasm 那头用 load 指令就能读到。7
3.4 没有 GC,所以要有 malloc/free¶
复杂应用躲不开动态分配。C/C++ 世界的答案是 malloc/free,在 Wasm 里它们通常被编译成 模块的两个 export 函数,工作方式与原生一致:8
malloc(size):在堆区(linear memory 里划出的一段)找一块够大的空闲区 (简单实现用「首个适配」,讲究的用伙伴系统),返回块的起始指针,同时记账防碎片;free(ptr):把块标回「可用」,相邻的空闲区合并,回收连续空间。
JS 侧的配套用法(书里原例):const ptr = instance.exports.malloc(1024),
然后 new Uint8Array(instance.exports.memory.buffer).set(data, ptr) 把数据写到那个地址,
用完 free(ptr)。9
三个代价必须认账:泄漏(忘了 free)、重复释放(double-free,会搞坏堆)、
碎片。书里给的优化:批量分配、用 arena(预分配一大块,短命的临时对象都从里面取)、
盯紧 memory.grow() 的回调。10
Rust 侧也一样,书里的例子直接用 std::alloc 暴露 allocate(size)/deallocate(ptr, size)
两个函数,本质是同一件事的两门语言写法。11
3.5 wasm-bindgen:把「翻译层」交给机器生成¶
手工玩指针又慢又容易错。wasm-bindgen 是 Rust 生态的答案:
你在 Rust 函数上标 #[wasm_bindgen],它编译时生成一个 .js 垫片(shim),
把类型翻译全包了。书里那个经典例子:12
import init, { greet } from './pkg/module.js';
init().then(() => console.log(greet('World'))); // "Hello, World!"
JS 这边看起来就是调了个普通函数:字符串进、字符串出。
底下发生的事:垫片把 JS 字符串编码成 UTF-8 写进线性内存、记下临时分配、
调完 Rust 再把返回的字节解码回 JS 字符串、顺手清掉临时内存防泄漏。
Vec<u8> 映射成 JS 数组,闭包可以当 JS 回调(回头再调用的函数)用,异步靠 JsFuture,浏览器 API 走
js_namespace = js_sys。13
3.6 双向调用:export 进门,import 出门¶
JS 调 Wasm 就是 instance.exports.add(7, 5) // 12——exports 是个普通 JS 对象,
函数就在上面;i64 出入自动配 BigInt。书里建议给裸 export 包一层 JS 外壳,
顺手做类型转换和「export 存不存在」的检查。14
Wasm 调 JS 靠实例化时的 import 对象。书里 C 的例子:
const importObject = { env: { js_callback: (val) => console.log(`Received: ${val}`) } };
// 实例化后调 trigger_callback() → 打印 "Received: 100"
Rust + wasm-bindgen 的版本更短:extern "C" { fn alert(s: &str); } 声明一下,
就能在 Rust 里直接 alert("Hello from Rust!")。15
两个高级提醒(书里点到的):重入——Wasm 调 JS、JS 又回调 Wasm, 调用栈可能越叠越深直到溢出,要用标志位或异步让出打断;动态分发—— 函数表(table)可以让 Wasm 按索引调不同的 import,适合做插件式的事件处理器。16
3.7 复杂数据结构:压扁成字节,或者交给工具¶
结构体(比如 {i32 id, f64 value, i32 length, 数组})跨边界的标准姿势是压扁:
malloc 一块连续内存,用 DataView 的 setInt32/setFloat64 按序把字段(结构体的成员)填进去,
把基址指针 + 大小传给 Wasm。这里有个真坑:对齐——Wasm 要求自然对齐
(i32 4 字节对齐,f64 8 字节),填错了轻则性能掉,直接拿错地址。17
嫌麻烦有两条自动化路:C/C++ 用 Emscripten 的 --closure 1 生成结构体搬运助手;
Rust 用 serde_wasm_bindgen,函数签名直接写 process_users(users: JsValue) -> JsValue,
底下的克隆进 Wasm 堆它全包。18
字符串就是字节数组:带长度前缀或以 \0 结尾;数组就是指针+长度。
书里 Rust 的反转字符串例子,JS 侧 reverse('WebAssembly') 返回 "ylbmessAbeW"——
十二个字节的来回,全在垫片底下完成。19
3.8 主线例子:一个用户列表处理(把机制串起来)¶
原书本章没有单一贯穿例子,我们把它的零件组装成一个完整走查。
输入:JS 有一个用户数组 [{id: 1, name: "Ada"}, {id: 2, name: "Bob"}],
要在 Wasm(Rust)里按 id 排序后返回。
(组合与具体字段是为演示编的;每个机制本身都在书里有出处。)
第 1 步:选型。 两种走法——手工压扁,或 serde_wasm_bindgen。
书里的口径是「生产环境用工具链」,所以走 JsValue 这条路。20
第 2 步:Rust 侧写函数。 #[wasm_bindgen] pub fn sort_users(users: JsValue) -> JsValue,
内部把 JsValue 反序列化成 Rust 结构体、排序、再序列化回 JsValue。
这一步编译后,垫片已自动生成。
第 3 步:JS 侧调用。 sort_users([{id:1,name:"Ada"},{id:2,name:"Bob"}]) ——
看起来的普通调用。底下:垫片把对象数组 JSON 化成字节 → 在 Wasm 堆里
malloc 一块 → 编码成指针+长度传进去 → Rust 返回同样形式的结果 →
垫片解码回 JS 数组并 free 临时内存。21
第 4 步:看成本。 书里明说 JSON/序列化「slower due to parsing」,不适合实时场景——
如果这是逐帧的图像滤镜,就该退回 3.2 的批量字节路:JS 把像素 set 进共享内存,
只传 (ptr, len)。22
第 5 步:防炸。 调用包在 try-catch 里;Wasm 那头如果越界访问内存,
JS 这侧收到的是 WebAssembly.RuntimeError;编译器阶段的问题则是
WebAssembly.CompileError。23
3.9 错误与调试:Wasm 的炸法和 JS 不一样¶
JS 出错往下传,Wasm 出错当场停(trap)。书里列的错误三族:24
| 阶段 | 错误类型 | JS 侧表现 |
|---|---|---|
| 编译 | 二进制不合法 | WebAssembly.compile() 以 SyntaxError/CompileError 拒绝 |
| 实例化 | import 没给全、签名不对 | 实例化直接失败 |
| 运行 | 越界访问、整数溢出陷阱 | 抛 WebAssembly.RuntimeError |
防御三件套:WebAssembly.validate() 预检、实例化和调用都包 try-catch、
Wasm 内部用返回错误码(比如 i32 错误枚举:只取有限几个值的类型)代替抛异常。
深一层可以用 Binaryen 的 wasm-decompile 看伪代码、wasm-objdump 核对 import/export 段。25
4. 作者的判断与证据¶
i64超 2^53 要 BigInt:这是规范行为,可靠;- 「Interface Types 提案 2024 年已批准,将消除大量样板代码」:书里多次引用这个时间表, 它是真实的(后更名 Component Model 路线的一部分),但「消除样板」至今仍是进行时, wasm-bindgen 依旧日常必需;
- 「JSON 兜底但慢」:与工程经验一致,无争议。
判断(我们的,不是书里的): 这一章的互操作清单是全书最「保值」的部分之一, 因为边界规则由 Wasm 规范定义,变化慢。真正会过时的是工具名——
serde_wasm_bindgen、--closure 1这类具体旋钮随工具链版本漂移,机制(指针+长度、编解码、import/export)不变。 如果错,会错在: 如果 Component Model 的高阶类型直接传值被浏览器全面落地, 「手工压扁 + 序列化」这一整节会变成历史资料——但截至本书出版的 2026 年,主线仍是书里的做法。
5. 边界与局限¶
- Wasm 异常处理:跨边界抛异常的提案书里只说「还没全面批准,先用错误码」, 没给跟踪这个特性的方法;
- SharedArrayBuffer 的政策前提(跨源隔离头)没讲,多线程共享内存时这是硬门槛;
- funcref/externref 的 table 操作只给了概念,没有完整示例;
- 性能数字缺席:跨边界调用的成本量级(每次调用的固定开销)没有量化,只说「尽量批处理」。
6. 可带走的¶
- Wasm 只有四种数;JS 的 number 是 64 位浮点——
i64大整数过界必走 BigInt,否则会悄悄截断。 - 批量数据的密码是「指针 + 长度」:数据进共享线性内存,参数只传地址。
- 两端都要边界检查:Wasm 校验器管静态,JS 拿
memory.buffer.byteLength管动态。 - 没有 GC:malloc/free 是 export 出来的普通函数;泄漏、double-free、碎片是你的责任。
- arena + 批量分配是书里给的降本三板斧。
- Rust 项目默认上 wasm-bindgen:字符串、Vec、闭包直接过界,临时内存自动清。
- Wasm 调 JS 走 import 对象;小心重入(来回回调会叠爆栈)。
- 结构体压扁要注意对齐:
i324 字节、f648 字节,错了性能和正确性一起掉。 - 错误三族分阶段接住:编译期 CompileError、实例化期缺 import、运行期 RuntimeError。
- 跨边界调用有固定开销:循环里逐次调不如攒一批一次传。
7. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 四种原始类型与 2.0 引用类型 | Communicating between JavaScript and WebAssembly | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:63(搜「signed and unsigned integers」) · text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:77(搜「2^53」) |
| JS Number 精度问题 | 同上 | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:75(搜「64-bit doubles」) |
| 批量数据与指针+长度 | 同上 | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:193(搜「linear memory」) |
| 线性内存模型 | 同上 | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:261(搜「single, contiguous block」) · text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:275(搜「at most one memory」) |
| malloc/free 机制 | 同上 | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:361(搜「first-fit allocator」) · text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:381(搜「malloc(1024)」) |
| Rust alloc 例子 | 同上 | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:401(搜「std::alloc」) |
| wasm-bindgen 机制与 greet 例 | 同上 | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:421(搜「game-changing abstraction layer」) · text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:439(搜「pub fn greet」) |
| JS 调 export | 同上 | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:517(搜「gateway to invoking」) · text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:547(搜「add(7, 5)」) |
| import 调 JS 与重入警告 | 同上 | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:619(搜「flips the interaction paradigm」) · text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:693(搜「reentrancy」) |
| C 回调与 Rust alert 例 | 同上 | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:701(搜「js_callback」) · text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:729(搜「extern "C"」) |
| 结构体压扁与对齐 | 同上 | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:745(搜「flattened」) · text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:777(搜「natural alignment」) |
| JSON/serde_wasm_bindgen | 同上 | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:789(搜「JSON.stringify」) · text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:803(搜「serde_wasm_bindgen」) |
| 反转字符串例 | 同上 | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:863(搜「reverse_string」) |
| 错误三族与 validate | 同上 | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:891(搜「WebAssembly.RuntimeError」) · text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:911(搜「WebAssembly.validate」) |
| 最佳实践 | 同上 | text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:971(搜「Minimize Performance Overhead」) |
-
出处:「Communicating between JavaScript and WebAssembly」第 13–17 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:13,搜「Effective communication」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 63–73 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:63,搜「statically typed and low-level」)。四个原始类型的清单见text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:85(搜「four primitive types」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 123 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:123,搜「64-bit floating-point value」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 77–79 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:77,搜「exceeds 2^53」)与text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:79(搜「externref」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 193–217 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:199,搜「Uint8Array(instance.exports.memory.buffer」)与text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:305(搜「bounds checks」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 261–287 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:263,搜「starting at address zero」)与text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:275(搜「at most one memory」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 289–293 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:291,搜「view[0] = 42」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 345–373 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:359,搜「malloc(size)」)。「first-fit allocator or more sophisticated buddy systems」见text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:361(搜「buddy systems」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 375–387 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:381,搜「malloc(1024)」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 389–393 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:389,搜「batch allocations」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 399–417 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:401,搜「std::alloc」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 419–461 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:421,搜「boilerplate」)与text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:439(搜「pub fn greet」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 449–475 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:459,搜「JsFuture」)与text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:465(搜「js_namespace」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 515–585 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:547,搜「add(7, 5)」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 699–719 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:701,搜「js_callback」)与text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:729(搜「extern "C"」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 691–695 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:693,搜「stacking overflows」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 741–785 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:745,搜「flattened」)与text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:777(搜「natural alignment」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 787–817 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:803,搜「serde_wasm_bindgen」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 859–875 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:863,搜「reverse_string」)与text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:873(搜「ylbmessAbeW」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 787–809 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:797,搜「language-specific toolchains」)。演示例的用户字段为编排所加,机制出处在此。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 449 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:449,搜「UTF-8 string conversions」)。垫片内部的编解码、临时分配、自动清理流程按原书描述。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 219 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:219,搜「slower due to parsing」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 877–941 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:891,搜「WebAssembly.RuntimeError」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 881–893 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:881,搜「Compilation failures」)。 ↩ -
出处:「Communicating between JavaScript and WebAssembly」第 909–933 段(
text/15-fm-ultimate-webassembly-for-high-performance-apps-b.txt:927,搜「wasm-decompile」)。 ↩