本文主要参考 MDN 教程:
Module 与 Instance
已经由浏览器编译为可执行机器码的 Wasm 二进制文件称为 Module。Module 是无状态的,可以直接在主线程和 worker 间传递。每个 Module 都会声明 imports(须导入的)和 exports(所导出的)。
一个 Module 所声明的 imports 和 exports 可以通过静态方法
WebAssembly.Module.imports
和WebAssembly.Module.exports
获取。
Instance 是 Module 的一个有状态的、可执行的实例,它包含了 Module 运行时使用的所有状态:Memory(内存)、Table(引用表)、imports 等。Wasm 模块提供给 JS 的功能需要通过 Instance 来获取。
Module 只是 Wasm 模块对应的二进制代码,内部的功能还无法被 JS 直接使用,需要实例化为一个 Instance 之后,才会拥有相应的内存,并暴露给 JS 可使用的功能。
加载并运行
未来可能会像 ES Module 一样加载 Wasm,但目前,在编译/实例化 Wasm 之前,需要先加载到内存中。
js
// 新的 API instantiateStreaming 直接处理来自网络的字节流,而无需转为 ArrayBuffer
WebAssembly.instantiateStreaming(fetch("fibonacci.wasm")).then((results) => {
// do something with results
});
// 旧的 API instantiate 需要先将 Wasm 转为 ArrayBuffer
fetch("fibonacci.wasm")
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes))
.then((results) => {
// do something with results
});
返回的 results
结构如下,instance.exports
上面就是 Wasm 模块所提供的功能:
js
{
module: Module, // 编译的 WebAssembly.Module 对象
instance: Instance, // module 对象的一个 WebAssembly.Instance 实例
}
关掉 -O3
优化,重新编译 Hello, WebAssembly 中的 fibonacci.cpp 并将结果打印出来:
-O3
不只优化 C++,还优化胶水 JS,编译优化可能会对胶水 JS 压缩、变量名替换等。
实例化的重载
以 simple.wasm 为例:
使用第二种重载形式,将 JS 函数导入 Wasm 中并实例化:
js
// 需要导入 Wasm
const importObject = {
imports: {
imported_func: (arg) => console.log(arg),
},
};
// 新 API,更加高效
WebAssembly.instantiateStreaming(fetch("simple.wasm"), importObject).then(
(results) => {
console.log(results);
results.instance.exports.exported_func();
}
);
// 旧 API
fetch("simple.wasm")
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes, importObject))
.then((results) => {
console.log(results);
results.instance.exports.exported_func();
});
输出结果:
Memory
Wasm 的底层内存模型是一个由无类型字节所构成的连续范围,称为线性内存 ,可以被 Wasm 底层内存指令读写。在这个模型中,读写操作可访问整个线性内存中的任一字节。与原生 C++ 程序不同的是,一个 Instance 能访问的内存局限于 Memory 所包含的特定范围(可能非常小)。不同 Instance 可以拥有各自独立的内存,也可以在不同 Instance 间共享内存,共享内存可以在主线程和 worker 间传递。Memory 在 JS 中可以看作一个大小可变的 ArrayBuffer
,它的内容可通过 WebAssembly.Memory
的实例属性 buffer
的 getter/setter
来读写。
Wasm 和 JS 都可以创建 Memory 对象,这引出了两种获取 WebAssembly.Memory
的方式:
- 在 JS 中使用
WebAssembly.Memory
创建内存并传给 Wasm,创建内存时可指定初始容量、容量上限、是否共享; - 从 Wasm 模块中导出,通过 Instance 上的
exports
属性获取。
内存大小以 Wasm 页------ 64 KB------为单位。
以 memory.wasm 为例:
js
const mem = new WebAssembly.Memory({ initial: 10, maximum: 100 });
const importObject = {
js: { mem },
};
WebAssembly.instantiateStreaming(fetch("memory.wasm"), importObject).then(
({ instance }) => {
const i32 = new Uint32Array(mem.buffer);
for (let i = 0; i < 10; i++) {
i32[i] = i;
}
console.log(instance.exports.accumulate(0, 10));
}
);
WebAssembly.Memory
的实例方法 grow
可以为 Memory 扩容,但如果超出容量上限,将抛出 RangeError
。扩容成功将会生成一个新的 ArrayBuffer
,原先的 buffer
失效。引擎将利用所提供的容量上限提前预留足够的空间,以便更高效地扩容。
Table
像 C++ 这种编译语言中存在函数指针,这使得 Wasm 中必须引入函数引用。出于安全性、可移植性和稳定性的原因,存储引用的字节不能直接由内容读写,所以引用不能保存在 Memory 中。因此,Wasm 引入了 Table,通过整数索引来获取函数引用。
Table 是一个大小可变的引用类型数组,JS 和 Wasm 都可以创建、访问和更改它。Table 数组的元素类型就是所存储的引用类型,目前 Wasm 只允许函数引用类型。
通过索引来调用 Table 中函数时会进行越界检查。
以 table.wasm 为例:
wasm
(module
(table $js.table (import "js" "table") 1 funcref)
(memory $js.memory (export "memory") 1)
(func $doIt (export "doIt") (result i32)
i32.const 0
i32.const 42
i32.store ;;store 42 at address 0
i32.const 0
call_indirect (result i32)
)
(func $log (export "log") (result i32)
i32.const 132
)
)
上面的代码是 wat 格式,可通过 wat2wasm 工具转为 wasm。
JS 代码如下:
js
const table = new WebAssembly.Table({ initial: 1, element: "anyfunc" });
const importObject = {
js: { table },
};
WebAssembly.instantiateStreaming(fetch("table.wasm"), importObject).then(
({ instance }) => {
const { doIt, log } = instance.exports;
table.set(0, log);
console.log(doIt());
console.log(table.get(0)());
}
);
WebAssembly.Table
的实例方法 grow
可以扩展 Table 的大小。
Global
Wasm 和 JS 都可以创建全局变量 Global,并在多个 Instance 间共享。以 global.wasm 为例:
js
const global = new WebAssembly.Global({ value: "i32", mutable: true }, 42);
const importObject = {
js: { global },
};
WebAssembly.instantiateStreaming(fetch("global.wasm"), importObject).then(
({ instance }) => {
const { incGlobal, getGlobal } = instance.exports;
incGlobal();
console.log(getGlobal()); // 43
global.value = 50;
incGlobal();
console.log(getGlobal()); // 51
}
);
Multiplicity
Multiplicity 优化了 Wasm 系统效率:
- 一个 Module 可以有 N 个 Instance。
- 一个 Instance 可以使用 0-1 个 Memory------该 Instance 的内存地址空间,未来可能允许一个 Instance 拥有 0-N 个 Memory。
- 一个 Instance 可以使用 0-1 个 Table------该 Instance 的函数地址空间,未来可能允许一个 Instance 拥有 0-N 个 Table。
- 一个 Memory 或 Table 可以同时被 0-N 个 Instance 使用。