如何在 JavaScript 中使用 .wasm 文件
如何在 JS 中使用 .wasm 文件
假设你已经有一个.wasm
模块
我们在这里假设您已经有一个.wasm
模块,无论是从 C / C ++ 程序编译还是直接从 s-exprs 汇编。
加载并运行
尽管将来有计划允许像使用 ES6 模块一样加载 WebAssembly 模块(使用 <script type='module'>
),
但 WebAssembly 当前必须由 JavaScript 加载和编译。
对于基本加载,分三个步骤:
- 将
.wasm
字节获取到类型化数组中,或者ArrayBuffer
- 将字节编译成
WebAssembly.Module
- 实例化
WebAssembly.Moduleimport
以获得可调用的导出
让我们更详细地讨论这些步骤。
-
第一步,有很多方法可以获取类型化的数组或
ArrayBuffer
字节:在网络上,使用 XHR 或 fetch,File 从 IndexedDB 检索,甚至直接在 JavaScript 中合成。 -
下一步是使用异步函数编译字节,该函数
WebAssembly.compile
返回一个解析为 a 的 Promise WebAssembly.Module。甲 Module 对象是无状态的支撑结构克隆这意味着经编译的代码可被存储在索引资料和 / 或经由窗口和工人之间共享 postMessage。 -
最后一步就是实例的
Module
通过构造一个新的WebAssembly.Instance
超车 Module 和被请求的任何进口 Module。 Instance 对象就像函数闭包,将代码与环境配对,并且不能结构化克隆。
我们可以将最后两个步骤合并为一个 instantiate
操作,该操作同时需要字节和导入,并异步返回 Instance
:
function instantiate(bytes, imports) {
return WebAssembly.compile(bytes).then(m => new WebAssembly.Instance(m, imports));
}
为了实际演示这一点,我们首先需要引入另一部分 JS API:
Function 导入 and 导出
像 ES6 模块一样,WebAssembly 模块可以导入和导出函数(以及稍后将介绍的其他类型的对象)。
我们可以在此模块中看到一个简单的示例,它们 i 从模块导入一个函数 imports
并导出一个函数 e
:
;; simple.wasm
(module
(func $i (import "imports" "i") (param i32))
(func (export "e")
i32.const 42
call $i))
(这里,我们不是使用 C / C ++ 编写模块并编译为 WebAssembly,而是直接以文本格式编写模块,该文本格式可以直接组装为二进制文件 simple.wasm。)
查看此模块,我们可以看到一些内容。首先,WebAssembly 导入具有两级名称空间;在这种情况下,内部名称 $i 的导入是从导入的 imports.i。同样,我们必须在传递给的导入对象中反映此两级名称空间 instantiate:
var importObject = { imports: { i: arg => console.log(arg) } };
将本节和最后一节中的所有内容放在一起,我们可以使用简单的 promise 链来获取,编译和实例化我们的模块:
fetch('simple.wasm').then(response => response.arrayBuffer())
.then(bytes => instantiate(bytes, importObject))
.then(instance => instance.exports.e());
最后一行调用导出的 WebAssembly 函数,该函数又调用导入的 JS 函数,该函数最终执行 console.log(42)
。
内存
线性内存是另一个重要的 WebAssembly 构建块,通常用于表示已编译的 C / C ++ 应用程序的整个堆。从 JavaScript 的角度来看,线性内存(以下简称“内存”)可以认为是可调整大小的 ArrayBuffer,可针对负载和存储的低开销沙箱进行精心优化。
可以通过提供初始大小以及最大大小(可选)来从 JavaScript 创建内存:
var memory = new WebAssembly.Memory({initial:10, maximum:100});
首先要注意的重要的事情是,单位 initial
和 maximum
是 WebAssembly
页被固定为 64KiB
。因此,memory 上述文件的初始大小为 10 页,即 640KiB,最大大小为 6.4MiB。
由于 JavaScript 中的大多数字节范围操作已经在 ArrayBuffer
类型数组上进行操作,而不是定义一组全新的不兼容操作,因此 WebAssembly.Memory
只需提供一个 buffer 返回的 getter 即可公开其字节 ArrayBuffer
。
例如,42 直接写入线性存储器的第一个字:
new Uint32Array(memory.buffer)[0] = 42;
创建后,可以通过调用来增加内存,在该内存 Memory.prototype.grow
中再次以 WebAssembly 页面为单位指定参数:
memory.grow(1);
如果 maximum
在创建时提供了 a ,则尝试超出此范围 maximum
将引发 RangeError
异常。引擎利用此提供的上限来提前保留内存,这可以使调整大小的效率更高。
由于 ArrayBuffers byteLength
是不可变的,因此,在成功完成 Memory.grow
操作后, buffergetter
将返回一个新 ArrayBuffer 对象(带有 new byteLength
),而之前的所有 ArrayBuffer
对象都将“分离”(零长度,抛出许多操作)。
与功能一样,线性存储器可以在模块内部定义或导入。
同样,模块也可以选择导出其内存。这意味着 JavaScript 可以通过创建 a new WebAssembly.Memory 并将其作为导入传入或通过接收导出来访问 WebAssembly 实例的内存 Memory。
例如,让我们采用一个 WebAssembly 模块,该模块对一个整数数组求和(用“…”代替函数的主体):
(module
(memory (export "mem") 1)
(func (export "accumulate") (param $ptr i32) (param $length i32) …))
由于此模块导出其内存,因此给定 Instance 名为的模块 instance,我们可以使用其导出的 memgetter 在实例的线性内存中直接创建并填充输入数组,如下所示:
var i32 = new Uint32Array(instance.exports.mem);
for (var i = 0; i < 10; i++)
i32[i] = i;
var sum = instance.exports.accumulate(0, 10);
内存导入的工作方式与函数导入一样,只是将 Memory 对象作为值而不是 JS 函数传递。内存导入之所以有用,有两个原因:
- 它们允许 JavaScript 在模块编译之前或与之同时获取并创建内存的初始内容。
- 它们允许单个
Memory
对象由多个实例导入,这是在 WebAssembly 中实现动态链接的关键构建块。