WebAssembly JavaScript 内置函数

WebAssembly JavaScript 内置函数是 JavaScript 操作的 Wasm 等价物,它提供了一种在 Wasm 模块内部使用 JavaScript 功能的方法,而无需导入 JavaScript 胶水代码来提供 JavaScript 和 WebAssembly 值以及调用约定之间的桥梁。

本文解释了内置函数的工作原理和可用类型,然后提供了一个使用示例。

导入 JavaScript 函数的问题

对于许多 JavaScript 功能,常规导入工作正常。然而,为 StringArrayBufferMap 等基本类型导入胶水代码会带来显著的性能开销。在这种情况下,WebAssembly 和大多数以其为目标的语言都期望一个紧密的内联操作序列,而不是像常规导入函数那样工作的间接函数调用。

具体来说,将函数从 JavaScript 导入到 WebAssembly 模块会因以下原因产生性能问题:

  • 现有 API 需要进行转换以处理 this 值的差异,WebAssembly 函数 import 调用会将其保留为 undefined
  • 某些基本类型使用 JavaScript 运算符,如 ===<,这些运算符无法导入。
  • 大多数 JavaScript 函数对其接受的值的类型极其宽容,而我们希望尽可能利用 WebAssembly 的类型系统来移除这些检查和强制转换。

考虑到这些问题,创建内置定义来将现有的 JavaScript 功能(例如 String 基本类型)适配到 WebAssembly,比导入它并依赖间接函数调用更简单且性能更好。

可用的 WebAssembly JavaScript 内置函数

以下各节详细介绍了可用的内置函数。未来可能会支持其他内置函数。

字符串操作

可用的 String 内置函数有:

"wasm:js-string" "cast"

如果提供的值不是字符串,则抛出错误。大致相当于:

js
if (typeof obj !== "string") throw new WebAssembly.RuntimeError();
"wasm:js-string" "compare"

比较两个字符串值并确定它们的顺序。如果第一个字符串小于第二个字符串,返回 -1;如果第一个字符串大于第二个字符串,返回 1;如果字符串严格相等,返回 0

"wasm:js-string" "concat"

等同于 String.prototype.concat()

"wasm:js-string" "charCodeAt"

等同于 String.prototype.charCodeAt()

"wasm:js-string" "codePointAt"

等同于 String.prototype.codePointAt()

"wasm:js-string" "equals"

比较两个字符串值是否严格相等,如果相等则返回 1,否则返回 0

注意:"equals" 函数是唯一一个对 null 输入不抛出异常的字符串内置函数,因此 Wasm 模块在调用它之前不需要检查 null 值。所有其他函数都没有合理的方式处理 null 输入,因此会对它们抛出异常。

"wasm:js-string" "fromCharCode"

等同于 String.fromCharCode()

"wasm:js-string" "fromCharCodeArray"

从一个 Wasm 的 i16 值数组创建一个字符串。

"wasm:js-string" "fromCodePoint"

等同于 String.fromCodePoint()

"wasm:js-string" "intoCharCodeArray"

将一个字符串的字符编码写入一个 Wasm 的 i16 值数组。

"wasm:js-string" "length"

等同于 String.prototype.length

"wasm:js-string" "substring"

等同于 String.prototype.substring()

"wasm:js-string" "test"

如果提供的值不是字符串,返回 0;如果是字符串,返回 1。大致相当于:

js
typeof obj === "string";

如何使用内置函数?

内置函数的工作方式与从 JavaScript 导入的函数类似,不同之处在于你使用的是在保留命名空间(wasm:)中定义的、用于执行 JavaScript 操作的标准 Wasm 函数等价物。因此,浏览器可以为它们预测并生成最优化的代码。本节总结了如何使用它们。

JavaScript API

内置函数在编译时通过在调用编译和/或实例化模块的方法时指定 compileOptions.builtins 属性作为参数来启用。它的值是一个字符串数组,用于标识你想要启用的内置函数集:

js
WebAssembly.compile(bytes, { builtins: ["js-string"] });

compileOptions 对象可用于以下函数:

WebAssembly 模块特性

在你的 WebAssembly 模块中,你现在可以从 wasm: 命名空间导入在 compileOptions 对象中指定的内置函数(在本例中是 concat() 函数;另请参阅等效的内置定义):

wat
(func $concat (import "wasm:js-string" "concat")
    (param externref externref) (result (ref extern)))

内置函数的特性检测

使用内置函数时,类型检查会比不使用时更严格——某些规则会强制应用于内置函数的导入。

因此,要为内置函数编写特性检测代码,你可以定义一个在启用该特性时无效,而在不启用时有效的模块。然后在验证失败时返回 true,以表示支持。一个能实现此目的的基本模块如下:

wat
(module
  (function (import "wasm:js-string" "cast")))

在没有内置函数的情况下,该模块是有效的,因为你可以导入任何你想要的签名的函数(在本例中:无参数和无返回值)。在有内置函数的情况下,该模块是无效的,因为现在被特殊处理的 "wasm:js-string" "cast" 函数必须具有特定的签名(一个 externref 参数和一个不可为空的 (ref extern) 返回值)。

然后你可以尝试使用 validate() 方法来验证这个模块,但请注意结果是如何用 ! 运算符取反的——记住,如果模块无效,则表示支持内置函数:

js
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}`));

上述模块代码非常简短,所以你可以直接验证字面字节,而无需下载模块。一个特性检测函数可能如下所示:

js
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 文件中)。

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 对象,其中包含:
  • 使用 fetch() 来获取 Wasm 模块 (log-concat.wasm),使用 Response.arrayBuffer 将响应转换为 ArrayBuffer,然后使用 WebAssembly.instantiate() 编译和实例化 Wasm 模块。
  • 调用从 Wasm 模块导出的 main() 函数。

Wasm 模块

我们的 WebAssembly 模块代码的文本表示如下:

wat
(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 全局字符串常量作为参数。

要让你的本地示例正常工作:

  1. 将上面显示的 WebAssembly 模块代码保存到一个名为 log-concat.wat 的文本文件中,与你的 HTML/JavaScript 文件放在同一目录中。

  2. 使用 wasm-as 工具将其编译成 WebAssembly 模块 (log-concat.wasm),该工具是 Binaryen 库的一部分(请参阅构建说明)。你需要启用引用类型和垃圾回收(GC)来运行 wasm-as,以便这些示例成功编译:

    sh
    wasm-as --enable-reference-types -–enable-gc log-concat.wat
    

    或者你可以使用 -all 标志来代替 --enable-reference-types -–enable-gc

    sh
    wasm-as -all log-concat.wat
    
  3. 使用本地 HTTP 服务器,在支持的浏览器中加载你的示例 HTML 页面。

结果应该是一个空白网页,并在 JavaScript 控制台中记录了 "hello world!",这是由一个导出的 Wasm 函数生成的。日志记录是使用从 JavaScript 导入的函数完成的,而两个原始字符串的连接则是由一个内置函数完成的。