WebAssembly 导入的全局字符串常量

WebAssembly 导入的全局字符串常量通过消除传统字符串导入所需的大量样板代码,使在 Wasm 模块中处理 JavaScript 字符串更加容易。

本文将解释导入的全局字符串常量的工作原理。

传统字符串导入的问题

让我们从探索 WebAssembly 中字符串导入的传统方式开始。在 Wasm 模块中,您可以使用以下代码片段从名为 "string_constants" 的命名空间导入一些字符串:

wat
(global (import "string_constants" "string_constant_1") externref)
(global (import "string_constants" "string_constant_2") externref)

在您的 JavaScript 中,您将提供要在 importObject 中导入的字符串:

js
importObject = {
  // …
  string_constants: {
    string_constant_1: "hello ",
    string_constant_2: "world!",
    // …
  },
};

在编译/实例化模块以使用其函数之前。

js
WebAssembly.instantiateStreaming(fetch("my-module.wasm"), importObject).then(
  (obj) => obj.instance.exports.exported_func(),
);

出于几个原因,这不太理想:

  1. 每次导入新字符串时,下载大小都会增加,而这种增加不仅仅是字符串本身的大小——对于您需要的每个字符串,都需要 Wasm 模块中导入的全局变量的定义,以及 JavaScript 端的值的定义。对于包含数千个导入字符串的 Wasm 模块,这会累加起来。
  2. 在 Wasm 模块可以实例化之前,所有这些字节也需要时间来解析。
  3. 对于 Wasm 模块的优化,不得不修改配套的 JavaScript 文件(例如在编译时删除未使用的字符串常量)是一个额外的麻烦。

导入名称可以是任何您喜欢的 Unicode 字符串,因此开发人员通常为了方便起见,会将整个字符串设置为导入名称(例如,在调试时)。这将导致我们上面的 Wasm 代码片段重写如下:

wat
(global (import "string_constants" "hello ") externref)
(global (import "string_constants" "world!") externref)

以及配套的 importObject 如下:

js
importObject = {
  // …
  string_constants: {
    "hello ": "hello ",
    "world!": "world!",
    // …
  },
};

查看上面的代码,让浏览器自动处理其中一些样板代码是有意义的,而这正是导入的全局字符串常量功能的作用。

使用导入的全局字符串常量

现在我们将介绍导入的全局字符串常量的使用方式。

JavaScript API

通过在调用编译和/或实例化模块的方法时包含 compileOptions.importedStringConstants 属性来启用导入的全局字符串常量。它的值是 Wasm 引擎将自动填充的导入全局字符串常量的导入命名空间。

js
WebAssembly.compile(bytes, {
  importedStringConstants: "string_constants",
});

就是这样!导入对象中不需要字符串列表。

compileOptions 对象可用于以下函数:

WebAssembly 模块功能

在您的 WebAssembly 模块中,您现在可以导入字符串字面量,指定在 JavaScript 的 importedStringConstants 中指定的相同命名空间。

wat
(global $h (import "string_constants" "hello ") externref)
(global $w (import "string_constants" "world!") externref)

然后,Wasm 引擎会查看 string_constants 命名空间中的所有导入的全局变量,并为每个指定的导入名称创建一个相等的字符串。

关于命名空间选择的说明

上面的示例为了说明目的,使用 "string_constants" 作为导入的全局字符串命名空间。然而,在生产环境中,最佳实践是使用空字符串("")来节省模块文件大小。命名空间会为每个字符串字面量重复,而实际的模块可能有数千个,因此节省量可能很可观。

如果您已经为其他目的使用了 "" 命名空间,您应该考虑为您的字符串使用单字符命名空间,例如 "s""'""#"

命名空间的选择通常由生成 Wasm 模块的工具链的作者决定。一旦您有了 .wasm 文件并想将其嵌入到您的 JavaScript 中,您就不能再自由地选择这个命名空间了;您必须使用 .wasm 文件所期望的。

导入的全局字符串示例

您可以在 一个使用导入的全局字符串的示例 中看到它正在运行——打开您浏览器的 JavaScript 控制台以查看示例输出。该示例定义了一个 Wasm 模块中的函数,该函数将两个导入的字符串连接在一起并将结果打印到控制台,然后导出它,再从 JavaScript 调用导出的函数。

下面显示了示例的 JavaScript。您可以看到我们是如何使用 importedStringConstants 来启用导入的全局字符串的。

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());

我们的 WebAssembly 模块代码的文本表示如下——请注意它是如何导入指定命名空间中的两个字符串,这些字符串稍后在 $concat 函数中使用。

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))))
)

注意: 此示例还使用了 JavaScript String 内置函数。有关这些内容以及上述示例的完整演练,请参阅 WebAssembly JavaScript 内置函数