本节摘要:Wasm 与 JavaScript 之间的全部官方接口,是一组职责分明的小对象:编译与实例化走 Module 与 Instance,运行时资源走 Memory、Table、Global,错误家族按阶段分三类。本节逐个盘点这些接口的语义与惯用法,建立后续两节的词汇基础。
WebAssembly 全局对象是宿主注入 JavaScript 的官方接口面:它把"编译字节、点亮实例、共享资源、上报错误"四件事各自封装成独立的类型与函数。本节按这四件事的顺序过一遍全表,每个接口都配最小可用示例。
编译与实例化各有两条路径,按"数据从哪来"与"要不要拆步"选择。
一次性路径把编译与实例化合并成单个 Promise,适合"拿到字节地址就一步到位"的绝大多数场景:
// 推荐:流式版本,边下载边编译,大模块收益明显 const { module, instance } = await WebAssembly.instantiateStreaming( fetch("/wasm/app.wasm"), { env: { log: (n) => console.log(n) } } // 导入对象:按模块申报逐项供给 ); instance.exports.calculate(42);
拆步路径显式先编译出 Module、再按需实例化成 Instance,适合"一份字节点亮多个实例"或"模块要先审查再运行"的场景:
const bytes = await (await fetch("/wasm/app.wasm")).arrayBuffer(); const mod = await WebAssembly.compile(bytes); // 编译:验证在此完成 console.log(WebAssembly.Module.imports(mod)); // 静态体检:它需要什么 console.log(WebAssembly.Module.exports(mod)); // 静态体检:它提供什么 const inst1 = await WebAssembly.instantiate(mod, importsA); const inst2 = await WebAssembly.instantiate(mod, importsB); // 第二份独立实例
拆步路径还送一份免费体检:imports 与 exports 的静态查询在任何实例化之前就能回答"这个模块依赖谁、开放什么"——插件系统按此做能力预审,不合规的模块根本不进实例化。同步版 compileSync 与 instantiateSync 存在但少用:它们阻塞主线程,只值得在极小模块(几 KB 级)且初始化路径敏感时动用。另有 WebAssembly.validate 可以零成本判断字节是否为合法模块,作为输入防线使用。
Memory、Table、Global 是三类"可以脱离单个实例存在"的资源对象,它们的共享语义是互操作架构的支点。
Memory 对象包裹线性内存,buffer 属性是宿主的读写窗口;它可以由 JavaScript 创建后作为导入递给模块,也可以由模块导出后供宿主读取——第五章第二节的零拷贝共享就建立在它上面。Table 对象装函数引用,宿主可以把自己的函数 set 进表里,让模块经 call_indirect 回调宿主——回调机制的物理载体。Global 对象是单个类型化数值,可变全局在多个模块间充当共享的轻量配置:
const sharedMem = new WebAssembly.Memory({ initial: 16, maximum: 64 }); const flag = new WebAssembly.Global({ value: "i32", mutable: true }, 0); // 同一份资源递进多个模块 —— 状态由此贯通 const a = await WebAssembly.instantiate(modA, { env: { mem: sharedMem, flag } }); const b = await WebAssembly.instantiate(modB, { env: { mem: sharedMem, flag } });
这段代码的语义值得读两遍:两个实例各自的栈与局部互不相干(第三章的实例隔离),但它们看到的线性内存是同一块、全局是同一个——模块间的"共享内存通信"不需要任何额外机制,递同一个对象进去就是了。服务端运行时提供同构的 API(多数仿照这套接口设计),浏览器里学的写法可以直接平移。
Wasm 的错误类型按生命周期阶段划分,排查时先认类型再找原因,效率天差地别:
| 错误类型 | 抛出阶段 | 典型原因 |
|---|---|---|
| CompileError | 编译期 | 字节非法:魔数错误、验证不通过 |
| LinkError | 实例化期 | 导入缺失、签名不匹配、容量不足 |
| RuntimeError | 执行期 | 陷阱:越界、除零、unreachable |
三类错误的边界与第三章的"三类失败"一一对应。惯用的防御结构是按阶段分层捕获:
try { const { instance } = await WebAssembly.instantiateStreaming(fetch(url), imports); instance.exports.run(); } catch (err) { if (err instanceof WebAssembly.CompileError) { // 字节坏了:检查构建产物与传输完整性 } else if (err instanceof WebAssembly.LinkError) { // 供给没对上:逐项核对导入对象的命名空间、名字与签名 } else if (err instanceof WebAssembly.RuntimeError) { // 模块内部陷阱:读 message 里的原因,决定丢弃或重建实例 } else { throw err; // TypeError 等:通常是调用方式问题,不该吞掉 } }
值得强调的取舍是 RuntimeError 的处理姿态:陷阱意味着实例状态已不可信,正确做法是丢弃实例、必要时重新实例化,而不是捕获后继续复用——第三章的"当场熄火"哲学落到宿主侧,就是这条纪律。
词汇表备齐,下一节算细账:数据过境的每条通道各要花多少成本,零拷贝怎么设计。