WebAssembly JavaScript 内置函数
WebAssembly JavaScript 内置函数是 JavaScript 操作的 Wasm 等价物,它提供了一种在 Wasm 模块内部使用 JavaScript 功能的方法,而无需导入 JavaScript 胶水代码来提供 JavaScript 和 WebAssembly 值以及调用约定之间的桥梁。
本文解释了内置函数的工作原理和可用类型,然后提供了一个使用示例。
导入 JavaScript 函数的问题
对于许多 JavaScript 功能,常规导入工作正常。然而,为 String
、ArrayBuffer
和 Map
等基本类型导入胶水代码会带来显著的性能开销。在这种情况下,WebAssembly 和大多数以其为目标的语言都期望一个紧密的内联操作序列,而不是像常规导入函数那样工作的间接函数调用。
具体来说,将函数从 JavaScript 导入到 WebAssembly 模块会因以下原因产生性能问题:
- 现有 API 需要进行转换以处理
this
值的差异,WebAssembly 函数import
调用会将其保留为undefined
。 - 某些基本类型使用 JavaScript 运算符,如
===
和<
,这些运算符无法导入。 - 大多数 JavaScript 函数对其接受的值的类型极其宽容,而我们希望尽可能利用 WebAssembly 的类型系统来移除这些检查和强制转换。
考虑到这些问题,创建内置定义来将现有的 JavaScript 功能(例如 String
基本类型)适配到 WebAssembly,比导入它并依赖间接函数调用更简单且性能更好。
可用的 WebAssembly JavaScript 内置函数
以下各节详细介绍了可用的内置函数。未来可能会支持其他内置函数。
字符串操作
可用的 String
内置函数有:
"wasm:js-string" "cast"
-
如果提供的值不是字符串,则抛出错误。大致相当于:
jsif (typeof obj !== "string") throw new WebAssembly.RuntimeError();
"wasm:js-string" "compare"
-
比较两个字符串值并确定它们的顺序。如果第一个字符串小于第二个字符串,返回
-1
;如果第一个字符串大于第二个字符串,返回1
;如果字符串严格相等,返回0
。 "wasm:js-string" "concat"
"wasm:js-string" "charCodeAt"
"wasm:js-string" "codePointAt"
"wasm:js-string" "equals"
-
比较两个字符串值是否严格相等,如果相等则返回
1
,否则返回0
。注意:
"equals"
函数是唯一一个对null
输入不抛出异常的字符串内置函数,因此 Wasm 模块在调用它之前不需要检查null
值。所有其他函数都没有合理的方式处理null
输入,因此会对它们抛出异常。 "wasm:js-string" "fromCharCode"
"wasm:js-string" "fromCharCodeArray"
-
从一个 Wasm 的
i16
值数组创建一个字符串。 "wasm:js-string" "fromCodePoint"
"wasm:js-string" "intoCharCodeArray"
-
将一个字符串的字符编码写入一个 Wasm 的
i16
值数组。 "wasm:js-string" "length"
"wasm:js-string" "substring"
"wasm:js-string" "test"
-
如果提供的值不是字符串,返回
0
;如果是字符串,返回1
。大致相当于:jstypeof obj === "string";
如何使用内置函数?
内置函数的工作方式与从 JavaScript 导入的函数类似,不同之处在于你使用的是在保留命名空间(wasm:
)中定义的、用于执行 JavaScript 操作的标准 Wasm 函数等价物。因此,浏览器可以为它们预测并生成最优化的代码。本节总结了如何使用它们。
JavaScript API
内置函数在编译时通过在调用编译和/或实例化模块的方法时指定 compileOptions.builtins
属性作为参数来启用。它的值是一个字符串数组,用于标识你想要启用的内置函数集:
WebAssembly.compile(bytes, { builtins: ["js-string"] });
compileOptions
对象可用于以下函数:
WebAssembly 模块特性
在你的 WebAssembly 模块中,你现在可以从 wasm:
命名空间导入在 compileOptions
对象中指定的内置函数(在本例中是 concat()
函数;另请参阅等效的内置定义):
(func $concat (import "wasm:js-string" "concat")
(param externref externref) (result (ref extern)))
内置函数的特性检测
使用内置函数时,类型检查会比不使用时更严格——某些规则会强制应用于内置函数的导入。
因此,要为内置函数编写特性检测代码,你可以定义一个在启用该特性时无效,而在不启用时有效的模块。然后在验证失败时返回 true
,以表示支持。一个能实现此目的的基本模块如下:
(module
(function (import "wasm:js-string" "cast")))
在没有内置函数的情况下,该模块是有效的,因为你可以导入任何你想要的签名的函数(在本例中:无参数和无返回值)。在有内置函数的情况下,该模块是无效的,因为现在被特殊处理的 "wasm:js-string" "cast"
函数必须具有特定的签名(一个 externref
参数和一个不可为空的 (ref extern)
返回值)。
然后你可以尝试使用 validate()
方法来验证这个模块,但请注意结果是如何用 !
运算符取反的——记住,如果模块无效,则表示支持内置函数:
const compileOptions = {
builtins: ["js-string"],
};
fetch("module.wasm")
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.validate(bytes, compileOptions))
.then((result) => console.log(`Builtins available: ${!result}`));
上述模块代码非常简短,所以你可以直接验证字面字节,而无需下载模块。一个特性检测函数可能如下所示:
function JsStringBuiltinsSupported() {
let bytes = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 2, 23, 1, 14, 119, 97, 115,
109, 58, 106, 115, 45, 115, 116, 114, 105, 110, 103, 4, 99, 97, 115, 116, 0,
0,
]);
return !WebAssembly.validate(bytes, { builtins: ["js-string"] });
}
注意: 在许多情况下,有替代内置函数特性检测的方法。另一种选择可以是与内置函数一起提供常规导入,支持的浏览器会忽略这些后备方案。
内置函数示例
让我们来看一个基础但完整的例子,以展示如何使用内置函数。这个例子将在一个 Wasm 模块中定义一个函数,该函数将两个字符串连接在一起并将结果打印到控制台,然后导出它。接着我们将从 JavaScript 中调用这个导出的函数。
我们将引用的示例在网页上使用 WebAssembly.instantiate()
函数来处理编译和实例化;你可以在我们的 webassembly-examples
仓库中找到这个和其他示例——请参阅 js-builtin-examples
。
你可以按照以下步骤构建这个例子。此外,你还可以看到它实时运行——打开你浏览器的 JavaScript 控制台以查看示例输出。
JavaScript
示例的 JavaScript 代码如下所示。要在本地测试,请使用你选择的方法将其包含在 HTML 页面中(例如,放在 <script>
标签内,或在通过 <script src="">
引用的外部 .js
文件中)。
const importObject = {
// Regular import
m: {
log: console.log,
},
};
const compileOptions = {
builtins: ["js-string"], // Enable JavaScript string builtins
importedStringConstants: "string_constants", // Enable imported global string constants
};
fetch("log-concat.wasm")
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes, importObject, compileOptions))
.then((result) => result.instance.exports.main());
JavaScript 代码
- 定义一个
importObject
,它在命名空间"m"
下指定一个函数"log"
,以便在实例化期间导入到 Wasm 模块中。它就是console.log()
函数。 - 定义一个
compileOptions
对象,其中包含:builtins
属性,用于启用字符串内置函数。importedStringConstants
属性,用于启用导入的全局字符串常量。
- 使用
fetch()
来获取 Wasm 模块 (log-concat.wasm
),使用Response.arrayBuffer
将响应转换为ArrayBuffer
,然后使用WebAssembly.instantiate()
编译和实例化 Wasm 模块。 - 调用从 Wasm 模块导出的
main()
函数。
Wasm 模块
我们的 WebAssembly 模块代码的文本表示如下:
(module
(global $h (import "string_constants" "hello ") externref)
(global $w (import "string_constants" "world!") externref)
(func $concat (import "wasm:js-string" "concat")
(param externref externref) (result (ref extern)))
(func $log (import "m" "log") (param externref))
(func (export "main")
(call $log (call $concat (global.get $h) (global.get $w))))
)
此代码:
- 导入两个全局字符串常量
"hello "
和"world!"
,使用 JavaScript 中指定的"string_constants"
命名空间。它们被命名为$h
和$w
。 - 从
wasm:
命名空间导入concat
内置函数,将其命名为$concat
并指定它有两个参数和一个返回值。 - 从
"m"
命名空间导入已导入的"log"
函数,如 JavaScript 的importObject
对象中所指定,将其命名为$log
并指定它有一个参数。我们决定在示例中同时包含常规导入和内置函数,以便向你展示两种方法的比较。 - 定义一个将以名称
"main"
导出的函数。此函数调用$log
,并将一个$concat
调用作为参数传递给它。$concat
调用则接收$h
和$w
全局字符串常量作为参数。
要让你的本地示例正常工作:
-
将上面显示的 WebAssembly 模块代码保存到一个名为
log-concat.wat
的文本文件中,与你的 HTML/JavaScript 文件放在同一目录中。 -
使用
wasm-as
工具将其编译成 WebAssembly 模块 (log-concat.wasm
),该工具是 Binaryen 库的一部分(请参阅构建说明)。你需要启用引用类型和垃圾回收(GC)来运行wasm-as
,以便这些示例成功编译:shwasm-as --enable-reference-types -–enable-gc log-concat.wat
或者你可以使用
-all
标志来代替--enable-reference-types -–enable-gc
:shwasm-as -all log-concat.wat
-
使用本地 HTTP 服务器,在支持的浏览器中加载你的示例 HTML 页面。
结果应该是一个空白网页,并在 JavaScript 控制台中记录了 "hello world!"
,这是由一个导出的 Wasm 函数生成的。日志记录是使用从 JavaScript 导入的函数完成的,而两个原始字符串的连接则是由一个内置函数完成的。