使用 WebAssembly JavaScript API
如果您已经使用 Emscripten 等工具从其他语言编译了一个模块,或自己加载并运行了代码,下一步就是学习更多关于使用 WebAssembly JavaScript API 的其他功能。本文将教你你需要了解的内容。
注意:如果您不熟悉本文中提到的基本概念,需要更多解释,请先阅读WebAssembly 概念,然后再回来。
一些简单的示例
让我们浏览一些示例,这些示例说明了如何使用 WebAssembly JavaScript API,以及如何使用它在网页中加载 Wasm 模块。
注意:您可以在我们的webassembly-examples GitHub 存储库中找到示例代码。
准备示例
- 首先我们需要一个 Wasm 模块!获取我们的
simple.wasm
文件并在本地计算机上的新目录中保存副本。 - 接下来,让我们在与 Wasm 文件相同的目录中创建一个名为
index.html
的简单 HTML 文件(如果您没有很容易获得的 HTML 文件,可以使用我们的简单模板)。 - 现在,为了帮助我们理解这里发生了什么,让我们看看 Wasm 模块的文本表示(我们也在将 WebAssembly 格式转换为 Wasm中遇到过)。wasm
(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)中提供的 value 值记录到控制台。如果您现在保存示例代码并在支持 WebAssembly 的浏览器中加载它,您将看到它的运行效果!
注意:这是一个复杂且冗长的示例,它实现的功能很少,但它确实说明了可能性——在 Web 应用程序中将 WebAssembly 代码与 JavaScript 一起使用。正如我们在其他地方所说的,WebAssembly 的目标不是取代 JavaScript;两者可以协同工作,利用彼此的优势。
在不进行流式传输的情况下加载我们的 Wasm 模块
如果您无法或不想使用上面描述的流式传输方法,则可以使用非流式传输方法WebAssembly.compile()
/ WebAssembly.instantiate()
。
这些方法不会直接访问字节码,因此需要额外的步骤才能将响应转换为ArrayBuffer
,然后才能编译/实例化 Wasm 模块。
等效代码如下所示
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 内存对象包含的一个特定(可能非常小)范围。这允许单个 Web 应用程序使用多个独立的库(每个库在内部使用 WebAssembly)来拥有彼此完全隔离的单独内存。此外,较新的实现还可以创建共享内存,这些内存可以使用postMessage()
在 Window 和 Worker 上下文之间传输,并在多个位置使用。
在 JavaScript 中,内存实例可以被认为是可调整大小的ArrayBuffer
(或SharedArrayBuffer
,在共享内存的情况下),并且与 ArrayBuffers
一样,单个 Web 应用程序可以创建许多独立的内存对象。您可以使用WebAssembly.Memory()
构造函数创建一个,该构造函数以初始大小和(可选)最大大小以及表示其是否为共享内存的 shared
属性作为参数。
让我们通过查看一个快速示例开始探索这一点。
- 创建另一个新的简单 HTML 页面(复制我们的简单模板)并将其命名为
memory.html
。向页面添加一个<script></script>
元素。 - 现在将以下行添加到脚本的顶部,以创建内存实例js
const 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
) 上创建 DataView
视图,而不是在 Memory 本身上。
内存导入的工作方式与函数导入相同,只是 Memory 对象作为值传递而不是 JavaScript 函数。内存导入有两个用途
- 它们允许 JavaScript 在模块编译之前或与模块编译同时获取和创建内存的初始内容。
- 它们允许单个 Memory 对象被多个模块实例导入,这是在 WebAssembly 中实现动态链接的关键构建块。
注意: 您可以在 memory.html 中找到我们的完整演示 (也可以在线查看)。
表
WebAssembly 表格是一个可调整大小的、类型化的引用数组,JavaScript 和 WebAssembly 代码都可以访问。虽然 Memory 提供了一个可调整大小的、类型化的原始字节数组,但将引用存储在 Memory 中是不安全的,因为引用是引擎信任的值,出于安全、可移植性和稳定性原因,其字节不得由内容直接读取或写入。
表格具有元素类型,该类型限制了可以存储在表格中的引用的类型。在 WebAssembly 的当前迭代中,WebAssembly 代码只需要一种类型的引用 - 函数 - 因此只有一种有效的元素类型。在未来的迭代中,将添加更多元素类型。
函数引用对于编译像 C/C++ 这样的具有函数指针的语言是必要的。在 C/C++ 的原生实现中,函数指针由函数代码在进程虚拟地址空间中的原始地址表示,因此,出于上述安全原因,不能直接存储在线性内存中。相反,函数引用存储在表格中,它们的索引(整数,可以存储在线性内存中)则被传递。
当需要调用函数指针时,WebAssembly 调用方提供索引,然后可以在索引和调用索引的函数引用之前,针对表格进行安全边界检查。因此,表格目前是一个相当底层的原语,用于安全且可移植地编译低级编程语言特性。
表格可以通过 Table.prototype.set()
进行修改,该方法更新表格中的一个值,以及 Table.prototype.grow()
,该方法增加表格中可以存储的值的数量。这允许间接可调用的函数集随时间变化,这对于 动态链接技术 来说是必要的。这些修改可以通过 JavaScript 和 Wasm 模块中的 Table.prototype.get()
立即访问。
表格示例
让我们来看一个简单的表格示例 - 一个创建并导出包含两个元素的表格的 WebAssembly 模块:元素 0 返回 13,元素 1 返回 42。您可以在 table.wasm 中找到它。
- 在新的目录中创建
table.wasm
的本地副本。注意: 您可以在 table.wat 中查看模块的文本表示形式。
- 在同一目录中创建我们 HTML 模板 的新副本,并将其命名为
table.html
。 - 与之前一样,获取、编译并实例化您的 Wasm 模块 - 将以下内容添加到 HTML 主体底部的
<script>
元素中jsWebAssembly.instantiateStreaming(fetch("table.wasm")).then((results) => { // add code here });
- 现在让我们访问表格中的数据 - 将以下行添加到代码中,在指示的位置js
const 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
:一个布尔值,用于定义该值是否可变。
- 一个包含变量实际值的 value。只要其类型与指定的数据类型匹配,它就可以是任何值。
那么我们如何使用它呢?在以下示例中,我们将全局变量定义为可变的 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 在 JavaScript 上下文中包含 WebAssembly 模块并使用其函数的基本知识,以及如何在 JavaScript 中使用 WebAssembly 内存和表格。我们还简要介绍了多重性的概念。