该 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 的有效类型。实际上,它做了两件事
- 用两个 32 位整数替换 64 位整数参数,分别表示低位和高位。
- 用一个 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 日发布),因此您今天就可以尝试它!