使用 WebAssembly JavaScript API
如果您已经使用 Emscripten 等工具从其他语言编译了一个模块,或者自行加载并运行了代码,那么下一步就是学习如何使用 WebAssembly JavaScript API 的其他功能。本文将教您所需了解的知识。
注意:如果您不熟悉本文中提到的基本概念,需要更多解释,请先阅读WebAssembly 概念,然后再回来阅读本文。
一些示例
让我们通过一些示例来解释如何使用 WebAssembly JavaScript API,以及如何在网页中加载 Wasm 模块。
注意:您可以在我们的 webassembly-examples GitHub 仓库中找到示例代码。
准备示例
-
首先我们需要一个 Wasm 模块!获取我们的
simple.wasm
文件并将其副本保存在本地机器上的一个新目录中。 -
接下来,在与 Wasm 文件相同的目录中创建一个名为
index.html
的简单 HTML 文件(如果没有方便可用的,可以使用我们的 简单模板)。 -
现在,为了帮助我们理解这里发生了什么,让我们看一下 Wasm 模块的文本表示(我们在将 WebAssembly 格式转换为 Wasm 中也曾遇到过)
wat(module (func $i (import "my_namespace" "imported_func") (param i32)) (func (export "exported_func") i32.const 42 call $i))
-
在第二行,您会看到导入具有两级命名空间——内部函数
$i
是从my_namespace.imported_func
导入的。在编写要导入到 Wasm 模块中的对象时,我们需要在 JavaScript 中反映这种两级命名空间。在您的 HTML 文件中创建一个<script></script>
元素,并向其中添加以下代码jsconst importObject = { my_namespace: { imported_func: (arg) => console.log(arg) }, };
流式传输 WebAssembly 模块
Firefox 58 中新增了直接从底层源编译和实例化 WebAssembly 模块的功能。这是通过 WebAssembly.compileStreaming()
和 WebAssembly.instantiateStreaming()
方法实现的。这些方法比其非流式对应方法更容易使用,因为它们可以直接将字节码转换为 Module
/Instance
实例,无需单独将 Response
放入 ArrayBuffer
中。
此示例(请参阅我们 GitHub 上的 instantiate-streaming.html 演示,并实时查看)展示了如何使用 instantiateStreaming()
获取 Wasm 模块,将 JavaScript 函数导入其中,编译并实例化它,以及访问其导出的函数——所有这些都在一步完成。
将以下内容添加到您的脚本中,在第一个代码块下方
WebAssembly.instantiateStreaming(fetch("simple.wasm"), importObject).then(
(obj) => obj.instance.exports.exported_func(),
);
最终结果是,我们调用了导出的 WebAssembly 函数 exported_func
,该函数又调用了导入的 JavaScript 函数 imported_func
,该函数将 WebAssembly 实例中提供的值 (42) 记录到控制台。如果您现在保存示例代码并在支持 WebAssembly 的浏览器中加载它,您将看到它的实际效果!
注意:这是一个复杂的、冗长的示例,几乎没有实现任何功能,但它确实说明了可能性——在您的 Web 应用程序中将 WebAssembly 代码与 JavaScript 一起使用。正如我们所说的,WebAssembly 旨在不替代 JavaScript;两者可以协同工作,相互借鉴优势。
不使用流式传输加载我们的 Wasm 模块
如果您不能或不想使用上面描述的流式方法,则可以使用非流式方法 WebAssembly.compile()
/ WebAssembly.instantiate()
。
这些方法不直接访问字节码,因此在编译/实例化 Wasm 模块之前,需要额外一步将响应转换为 ArrayBuffer
。
等效的代码如下所示
fetch("simple.wasm")
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes, importObject))
.then((results) => {
results.instance.exports.exported_func();
});
在开发者工具中查看 Wasm
在 Firefox 54+ 中,开发者工具调试器面板具有显示网页中包含的任何 Wasm 代码的文本表示的功能。要查看它,您可以转到调试器面板并单击“wasm://”条目。
除了以文本形式查看 WebAssembly,开发人员还可以使用文本格式调试 WebAssembly(设置断点、检查调用堆栈、单步执行等)。
内存
在 WebAssembly 的低级内存模型中,内存表示为一段连续的无类型字节范围,称为线性内存,模块内部的加载和存储指令会对其进行读写。在这个内存模型中,任何加载或存储都可以访问整个线性内存中的任何字节,这对于忠实地表示 C/C++ 概念(如指针)是必需的。
然而,与原生 C/C++ 程序不同,原生 C/C++ 程序的可用内存范围跨越整个进程,WebAssembly 实例可访问的内存被限制在一个特定的(可能非常小)范围,该范围由 WebAssembly Memory 对象包含。这允许单个 Web 应用程序使用多个独立的库(每个库在内部使用 WebAssembly),以拥有彼此完全隔离的独立内存。此外,更新的实现还可以创建共享内存,可以通过 postMessage()
在 Window 和 Worker 上下文之间传输,并在多个地方使用。
在 JavaScript 中,一个 Memory 实例可以被看作是一个可调整大小的 ArrayBuffer
(或者在共享内存的情况下,是一个 SharedArrayBuffer
),并且与 ArrayBuffer
一样,单个 Web 应用程序可以创建许多独立的 Memory 对象。您可以使用 WebAssembly.Memory()
构造函数创建一个 Memory 实例,该构造函数接受初始大小和(可选地)最大大小以及一个 shared
属性作为参数,该属性指示它是否是共享内存。
让我们从一个快速示例开始探索。
-
创建另一个新的简单 HTML 页面(复制我们的简单模板),并将其命名为
memory.html
。向页面添加一个<script></script>
元素。 -
现在将以下行添加到脚本顶部,以创建内存实例
jsconst memory = new WebAssembly.Memory({ initial: 10, maximum: 100 });
initial
和maximum
的单位是 WebAssembly 页面——它们的大小固定为 64KB。这意味着上述内存实例的初始大小为 640KB,最大大小为 6.4MB。WebAssembly 内存通过提供一个返回 ArrayBuffer 的缓冲区 getter/setter 来公开其字节。例如,要将 42 直接写入线性内存的第一个字,您可以这样做
jsconst data = new DataView(memory.buffer); data.setUint32(0, 42, true);
请注意使用
true
,这强制使用小端序读写,因为 WebAssembly 内存始终是小端序。然后可以使用以下方法返回相同的值jsdata.getUint32(0, true);
-
现在在您的演示中尝试一下——保存您目前添加的内容,在浏览器中加载它,然后尝试在 JavaScript 控制台中输入上面两行。
增长内存
内存实例可以通过调用 Memory.prototype.grow()
来增长,其中参数再次以 WebAssembly 页面的单位指定。
memory.grow(1);
如果在创建内存实例时提供了最大值,则尝试超出此最大值进行增长将抛出 RangeError
异常。引擎会利用这个提供的上限预留内存,这可以使调整大小更高效。
注意:由于 ArrayBuffer
的 byteLength 是不可变的,所以在成功执行 Memory.prototype.grow()
操作后,缓冲区 getter 将返回一个新的 ArrayBuffer 对象(具有新的 byteLength),并且任何先前的 ArrayBuffer 对象都将“分离”,或与它们之前指向的底层内存断开连接。
就像函数一样,线性内存可以在模块内部定义或导入。同样,模块也可以选择性地导出其内存。这意味着 JavaScript 可以通过创建新的 WebAssembly.Memory
并将其作为导入传入,或者通过接收内存导出(通过 Instance.prototype.exports
)来访问 WebAssembly 实例的内存。
更复杂的内存示例
让我们通过一个更复杂的内存示例来阐明上述断言——一个 WebAssembly 模块导入我们之前定义的内存实例,用一个整数数组填充它,然后对它们求和。您可以在memory.wasm找到它。
-
在与之前相同的目录中制作
memory.wasm
的本地副本。注意:您可以在memory.wat查看模块的文本表示。
-
回到您的
memory.html
示例文件,并像以前一样获取、编译和实例化您的 Wasm 模块——将以下内容添加到脚本底部jsWebAssembly.instantiateStreaming(fetch("memory.wasm"), { js: { mem: memory }, }).then((results) => { // add code here });
-
由于此模块导出了其内存,给定此模块的一个实例(称为 instance),我们可以使用导出的函数
accumulate()
直接在模块实例的线性内存 (mem
) 中创建和填充输入数组。将以下内容添加到您的代码中,在指定位置jsconst summands = new DataView(memory.buffer); for (let i = 0; i < 10; i++) { summands.setUint32(i * 4, i, true); } const sum = results.instance.exports.accumulate(0, 10); console.log(sum);
请注意我们是如何在 Memory 对象的缓冲区上(Memory.prototype.buffer
),而不是在 Memory 本身创建 DataView
视图的。
内存导入的工作原理与函数导入类似,只是 Memory 对象作为值传递而不是 JavaScript 函数。内存导入有两个有用的原因
- 它们允许 JavaScript 在模块编译之前或同时获取并创建内存的初始内容。
- 它们允许单个 Memory 对象被多个模块实例导入,这是在 WebAssembly 中实现动态链接的关键构建块。
注意:您可以在memory.html找到我们的完整演示(也可在线查看)。
表格
WebAssembly Table 是一个可调整大小的引用类型数组,可由 JavaScript 和 WebAssembly 代码访问。虽然 Memory 提供了一个可调整大小的原始字节类型数组,但将引用存储在 Memory 中是不安全的,因为引用是引擎信任的值,出于安全、可移植性和稳定性原因,其字节不能由内容直接读取或写入。
表格具有一个元素类型,它限制了可以存储在表格中的引用类型。在 WebAssembly 的当前迭代中,WebAssembly 代码只需要一种引用类型——函数——因此只有一种有效的元素类型。在未来的迭代中,将添加更多元素类型。
函数引用对于编译像 C/C++ 这样具有函数指针的语言是必需的。在 C/C++ 的本地实现中,函数指针由进程虚拟地址空间中函数代码的原始地址表示,因此,出于上述安全原因,不能直接存储在线性内存中。相反,函数引用存储在表中,它们的索引(整数,可以存储在线性内存中)被传递。
当需要调用函数指针时,WebAssembly 调用者提供索引,然后可以安全地对照表进行边界检查,然后再索引和调用索引的函数引用。因此,表目前是一个相当底层的原语,用于安全且可移植地编译低级编程语言特性。
表格可以通过 Table.prototype.set()
进行修改,该方法更新表格中的一个值,以及 Table.prototype.grow()
,该方法增加可以存储在表格中的值的数量。这允许间接可调用函数集随时间变化,这对于动态链接技术是必需的。这些修改可以通过 JavaScript 中的 Table.prototype.get()
和 Wasm 模块立即访问。
表格示例
让我们看一个简单的表格示例——一个 WebAssembly 模块,它创建并导出一个包含两个元素的表格:元素 0 返回 13,元素 1 返回 42。您可以在 table.wasm 找到它。
-
在新目录中制作
table.wasm
的本地副本。注意:您可以在table.wat查看模块的文本表示。
-
在同一目录中创建一个我们HTML 模板的新副本,并将其命名为
table.html
。 -
和以前一样,获取、编译和实例化您的 Wasm 模块——将以下内容添加到您的 HTML body 底部的
<script>
元素中jsWebAssembly.instantiateStreaming(fetch("table.wasm")).then((results) => { // add code here });
-
现在让我们访问表格中的数据——将以下行添加到您的代码中,在指定位置
jsconst tbl = results.instance.exports.tbl; console.log(tbl.get(0)()); // 13 console.log(tbl.get(1)()); // 42
此代码依次访问存储在表格中的每个函数引用,并实例化它们以将它们持有的值打印到控制台——请注意每个函数引用是如何通过 Table.prototype.get()
调用检索的,然后我们添加了一组额外的括号来实际调用该函数。
注意:您可以在table.html找到我们的完整演示(也可在线查看)。
全局变量
WebAssembly 能够创建全局变量实例,这些实例可以从 JavaScript 访问,并且可以在一个或多个 WebAssembly.Module
实例之间导入/导出。这非常有用,因为它允许动态链接多个模块。
要从 JavaScript 中创建 WebAssembly 全局实例,您可以使用 WebAssembly.Global()
构造函数,它看起来像这样
const global = new WebAssembly.Global({ value: "i32", mutable: true }, 0);
您可以看到这需要两个参数
-
一个包含两个属性的对象,描述全局变量
value
:其数据类型,可以是 WebAssembly 模块中接受的任何数据类型——i32
、i64
、f32
或f64
。mutable
:一个布尔值,定义该值是否可变。
-
一个包含变量实际值的值。这可以是任何值,只要其类型与指定的数据类型匹配。
那么我们如何使用它呢?在下面的示例中,我们将一个全局变量定义为可变的 i32
类型,值为 0。
然后,全局变量的值被更改,首先使用 Global.value
属性更改为 42
,然后使用从 global.wasm
模块导出的 incGlobal()
函数更改为 43(该函数将 1 添加到给定值,然后返回新值)。
const output = document.getElementById("output");
function assertEq(msg, got, expected) {
const result =
got === expected
? `SUCCESS! Got: ${got}\n`
: `FAIL!\nGot: ${got}\nExpected: ${expected}\n`;
output.innerText += `Testing ${msg}: ${result}`;
}
assertEq("WebAssembly.Global exists", typeof WebAssembly.Global, "function");
const global = new WebAssembly.Global({ value: "i32", mutable: true }, 0);
WebAssembly.instantiateStreaming(fetch("global.wasm"), { js: { global } }).then(
({ instance }) => {
assertEq(
"getting initial value from wasm",
instance.exports.getGlobal(),
0,
);
global.value = 42;
assertEq(
"getting JS-updated value from wasm",
instance.exports.getGlobal(),
42,
);
instance.exports.incGlobal();
assertEq("getting wasm-updated value from JS", global.value, 43);
},
);
注意:您可以在 GitHub 上实时运行示例;另请参阅源代码。
多重性
现在我们已经演示了主要 WebAssembly 构建块的用法,这是一个提及多重性概念的好地方。这为 WebAssembly 在架构效率方面提供了许多进步
- 一个模块可以有 N 个实例,就像一个函数字面量可以产生 N 个闭包值一样。
- 一个模块实例可以使用 0-1 个内存实例,这些实例提供实例的“地址空间”。WebAssembly 的未来版本可能允许每个模块实例有 0-N 个内存实例(参见多重内存)。
- 一个模块实例可以使用 0-1 个表格实例——这是实例的“函数地址空间”,用于实现 C 函数指针。WebAssembly 的未来版本可能允许每个模块实例有 0-N 个表格实例。
- 一个内存或表格实例可以被 0-N 个模块实例使用——这些实例都共享相同的地址空间,从而允许动态链接。
您可以在我们的《理解文本格式》文章中看到多重性的实际应用——请参阅修改表格和动态链接部分。
总结
本文带您了解了使用 WebAssembly JavaScript API 将 WebAssembly 模块包含在 JavaScript 上下文中并利用其函数的基本知识,以及如何在 JavaScript 中使用 WebAssembly 内存和表格。我们还触及了多重性概念。