如何在 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});

首先要注意的重要的事情是,单位 initialmaximumWebAssembly 页被固定为 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 中实现动态链接的关键构建块。

AXIHE / 精选资源

浏览全部教程

面试题

学习网站

前端培训
自己甄别

前端书籍

关于朱安邦

我叫 朱安邦,阿西河的站长,在杭州。

以前是一名平面设计师,后来开始接接触前端开发,主要研究前端技术中的JS方向。

业余时间我喜欢分享和交流自己的技术,欢迎大家关注我的 Bilibili

关注我: Github / 知乎

于2021年离开前端领域,目前重心放在研究区块链上面了

我叫朱安邦,阿西河的站长

目前在杭州从事区块链周边的开发工作,机械专业,以前从事平面设计工作。

2014年底脱产在老家自学6个月的前端技术,自学期间几乎从未出过家门,最终找到了满意的前端工作。更多>

于2021年离开前端领域,目前从事区块链方面工作了