《WebAssembly指南》第九章:WebAssembly 导入全局字符串常量
WebAssembly 导入全局字符串常量能让开发者在 Wasm 模块中更轻松地使用 JavaScript 字符串 —— 因为它省掉了传统字符串导入时需要写的一大堆模板代码。
本文就来讲解导入全局字符串常量的工作原理。
传统字符串导入存在的问题
咱们先看看 WebAssembly 里传统的字符串导入是怎么实现的。在一个 Wasm 模块中,若要从名为 string_constants
的命名空间导入多个字符串,可使用以下代码片段:
// 从 "string_constants" 命名空间导入两个外部引用类型的全局变量(字符串)
(global (import "string_constants" "string_constant_1") externref)
(global (import "string_constants" "string_constant_2") externref)
随后在 JavaScript 中,你需要通过 importObject
提供这些待导入的字符串:
// 定义导入对象,为 Wasm 模块提供所需的外部资源
importObject = {// …(其他命名空间或资源)string_constants: {string_constant_1: "hello ", // 第一个待导入的字符串string_constant_2: "world!", // 第二个待导入的字符串// …(其他需要导入的字符串)},
};
在编译 / 实例化模块并调用其函数之前,还需执行以下步骤:
// 流式加载并实例化 Wasm 模块,传入导入对象
WebAssembly.instantiateStreaming(fetch("my-module.wasm"), importObject).then((obj) => obj.instance.exports.exported_func(), // 调用模块导出的函数
);
但这种方式存在诸多不足,具体如下:
- 下载体积增大:每新增一个导入字符串,模块下载体积都会增加 —— 且增量不止字符串本身的大小。Wasm 模块中需定义导入全局变量,JavaScript 侧还需定义对应值,若模块需导入数千个字符串,额外体积会非常可观。
- 解析耗时增加:这些额外字节会延长解析时间,必须等解析完成后才能实例化 Wasm 模块。
- 优化不便:对 Wasm 模块做优化时(如编译时删除未使用的字符串常量),需同时修改 Wasm 模块和配套的 JavaScript 文件,操作繁琐。
此外,导入名支持任意 Unicode 字符串,因此开发者为方便(如调试),常直接将完整字符串作为导入名。此时 Wasm 代码需改写为:
// 直接用字符串内容作为导入名
(global (import "string_constants" "hello ") externref)
(global (import "string_constants" "world!") externref)
对应的 importObject
也需同步修改:
importObject = {// …(其他内容)string_constants: {"hello ": "hello ", // 导入名与字符串值完全一致"world!": "world!", // 导入名与字符串值完全一致// …(其他字符串)},
};
显然,这些模板代码完全可由浏览器自动处理 —— 而 “导入全局字符串常量” 特性正是为解决这一问题而生。
如何使用导入全局字符串常量
下面详细介绍导入全局字符串常量的具体使用方法。
JavaScript API
要启用导入全局字符串常量,只需在调用编译 / 实例化模块的方法时,添加 compileOptions.importedStringConstants
属性即可。该属性的值为一个命名空间,Wasm 引擎会自动用它填充导入的全局字符串常量。
代码示例如下:
// 编译 Wasm 字节码,通过 compileOptions 启用导入全局字符串常量
WebAssembly.compile(bytes, {importedStringConstants: "string_constants", // 指定字符串导入的命名空间
});
如此一来,importObject
中便无需再罗列字符串了。
目前支持 compileOptions
对象的函数 / 构造函数包括:
WebAssembly.compile()
WebAssembly.compileStreaming()
WebAssembly.instantiate()
WebAssembly.instantiateStreaming()
WebAssembly.validate()
WebAssembly.Module()
构造函数
Wasm 模块相关特性
在 Wasm 模块中,现在可直接导入字符串字面量,只需指定与 JavaScript 中 importedStringConstants
一致的命名空间即可:
// 从 "string_constants" 命名空间导入字符串,并用 $h/$w 作为局部别名
(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 代码
以下是示例的 JavaScript 代码,可清晰看到如何通过 importedStringConstants
启用导入全局字符串:
// 定义常规导入对象,提供日志打印功能
const importObject = {m: {log: console.log, // 为 Wasm 模块提供 console.log 功能},
};// 定义编译选项,启用所需特性
const compileOptions = {builtins: ["js-string"], // 启用 JavaScript 字符串内置功能importedStringConstants: "string_constants", // 启用导入全局字符串常量并指定命名空间
};// 加载、编译并实例化 Wasm 模块,最后调用导出的 main 函数
fetch("log-concat.wasm").then((response) => response.arrayBuffer()) // 将响应转为 ArrayBuffer(字节码).then((bytes) => WebAssembly.instantiate(bytes, importObject, compileOptions)) // 实例化模块.then((result) => result.instance.exports.main()); // 调用模块导出的 main 函数
示例的 Wasm 模块代码(文本形式)
以下是该 Wasm 模块的文本表示,注意其如何从指定命名空间导入字符串,并在 $concat
函数中使用这些字符串:
(module; 从 "string_constants" 命名空间导入两个字符串,分别命名为 $h 和 $w(global $h (import "string_constants" "hello ") externref)(global $w (import "string_constants" "world!") externref); 导入 JavaScript 字符串的拼接函数(func $concat (import "wasm:js-string" "concat")(param externref externref) (result (ref extern))) ; 接收两个 externref 参数,返回 extern 引用; 导入日志打印函数(func $log (import "m" "log") (param externref)) ; 接收一个 externref 参数(待打印的字符串); 定义并导出 main 函数,作为入口点(func (export "main"); 调用 $concat 拼接 $h 和 $w,再将结果传给 $log 打印(call $log (call $concat (global.get $h) (global.get $w))))
)