WebAssembly 与 JavaScript BigInt 集成

发布 · 标签:WebAssembly ECMAScript

JS-BigInt-Integration 功能使在 JavaScript 和 WebAssembly 之间传递 64 位整数变得容易。本文将解释这意味着什么以及它为什么有用,包括使开发人员的工作更轻松、让代码运行得更快以及加快构建时间。

64 位整数 #

JavaScript 数字是双精度浮点数,即 64 位浮点数。这样的值可以包含任何 32 位整数,但不能包含所有 64 位整数。另一方面,WebAssembly 完全支持 64 位整数,即 i64 类型。当连接两者时会出现问题:例如,如果 Wasm 函数返回 i64,那么当您从 JavaScript 调用它时,VM 会抛出异常,类似于以下内容

TypeError: Wasm function signature contains illegal type

正如错误所说,i64 不是 JavaScript 的合法类型。

从历史上看,对此的最佳解决方案是 Wasm 的“合法化”。合法化意味着将 Wasm 导入和导出转换为使用 JavaScript 的有效类型。实际上,它做了两件事

  1. 用两个 32 位整数替换 64 位整数参数,分别表示低位和高位。
  2. 用一个 32 位整数返回值替换 64 位整数返回值,表示低位,并使用一个 32 位值作为高位。

例如,考虑以下 Wasm 模块

(module
(func $send_i64 (param $x i64)
..))

合法化将把它变成这样

(module
(func $send_i64 (param $x_low i32) (param $x_high i32)
(local $x i64) ;; the real value the rest of the code will use
;; code to combine $x_low and $x_high into $x
..))

合法化是在工具端完成的,在它到达运行它的 VM 之前。例如,Binaryen 工具链库有一个名为 LegalizeJSInterface 的传递,它执行该转换,在 Emscripten 中需要时会自动运行。

合法化的缺点 #

合法化对于许多事情来说已经足够好了,但它确实有一些缺点,比如将 32 位片段组合或拆分为 64 位值的额外工作。虽然这种情况很少发生在热路径上,但当它发生时,速度下降可能是显而易见的 - 我们将在后面看到一些数字。

另一个令人讨厌的是,合法化对用户来说是显而易见的,因为它改变了 JavaScript 和 Wasm 之间的接口。以下是一个例子

// example.c

#include <stdint.h>

extern void send_i64_to_js(int64_t);

int main() {
send_i64_to_js(0xABCD12345678ULL);
}
// example.js

mergeInto(LibraryManager.library, {
send_i64_to_js: function(value) {
console.log("JS received: 0x" + value.toString(16));
}
});

这是一个小型 C 程序,它调用 JavaScript 库 函数(即,我们在 C 中定义一个 extern C 函数,并在 JavaScript 中实现它,作为一种简单且低级的在 Wasm 和 JavaScript 之间进行调用的方式)。该程序所做的只是将 i64 发送到 JavaScript,我们在那里尝试打印它。

我们可以用以下命令构建它

emcc example.c --js-library example.js -o out.js

当我们运行它时,我们没有得到我们期望的结果

node out.js
JS received: 0x12345678

我们发送了 0xABCD12345678,但我们只收到了 0x12345678 😔。这里发生的事情是,合法化将 i64 转换为两个 i32,我们的代码只接收了低 32 位,忽略了另一个发送的参数。为了正确处理,我们需要做类似的事情

  // The i64 is split into two 32-bit parameters, “low” and “high”.
send_i64_to_js: function(low, high) {
console.log("JS received: 0x" + high.toString(16) + low.toString(16));
}

现在运行它,我们得到

JS received: 0xabcd12345678

如您所见,可以与合法化共存。但这可能有点烦人!

解决方案:JavaScript BigInts #

JavaScript 现在有 BigInt 值,它们表示任意大小的整数,因此它们可以正确地表示 64 位整数。自然地,您希望使用它们来表示 Wasm 中的 i64。这正是 JS-BigInt-Integration 功能所做的!

Emscripten 支持 Wasm BigInt 集成,我们可以使用它来编译原始示例(无需任何合法化黑客),只需添加 -s WASM_BIGINT 即可

emcc example.c --js-library example.js -o out.js -s WASM_BIGINT

然后我们可以运行它(请注意,我们目前需要向 Node.js 传递一个标志来启用 BigInt 集成)

node --experimental-wasm-bigint a.out.js
JS received: 0xabcd12345678

完美,正是我们想要的!

这不仅更简单,而且更快。如前所述,在实践中,i64 转换很少发生在热路径上,但当它发生时,速度下降可能是显而易见的。如果我们将上面的示例变成一个基准测试,运行许多 send_i64_to_js 调用,那么 BigInt 版本的速度快了 18%。

BigInt 集成的另一个好处是工具链可以避免合法化。如果 Emscripten 不需要合法化,那么它可能不需要对 LLVM 发出的 Wasm 进行任何操作,这将加快构建时间。如果您使用 -s WASM_BIGINT 构建并且不提供任何其他需要更改的标志,您就可以获得这种加速。例如,-O0 -s WASM_BIGINT 可以工作(但优化构建 运行 Binaryen 优化器,这对大小很重要)。

结论 #

WebAssembly BigInt 集成已在 多个浏览器 中实现,包括 Chrome 85(于 2020 年 8 月 25 日发布),因此您今天就可以尝试它!