《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(),  // 调用模块导出的函数
);

但这种方式存在诸多不足,具体如下:

  1. 下载体积增大:每新增一个导入字符串,模块下载体积都会增加 ------ 且增量不止字符串本身的大小。Wasm 模块中需定义导入全局变量,JavaScript 侧还需定义对应值,若模块需导入数千个字符串,额外体积会非常可观。
  2. 解析耗时增加:这些额外字节会延长解析时间,必须等解析完成后才能实例化 Wasm 模块。
  3. 优化不便:对 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))))
)
相关推荐
卡戎-caryon2 小时前
【Java SE】06. 数组
java·开发语言
Rain_is_bad2 小时前
初识c语言————数学库函数
c语言·开发语言·算法
lsx2024062 小时前
Eclipse 快捷键
开发语言
数字化顾问4 小时前
从索引失效到毫秒级响应——SQL 优化实战案例:从慢查询到高性能的完整指南之电商大促篇
java·开发语言·数据库
eqwaak04 小时前
实战项目与工程化:端到端机器学习流程全解析
开发语言·人工智能·python·机器学习·语言模型
大飞pkz5 小时前
【设计模式】观察者模式
开发语言·观察者模式·设计模式·c#
upgrador5 小时前
PYTHON:Python 新版本下载安装更新&py文件Pycharm运行指南
开发语言·python
做运维的阿瑞5 小时前
从入门到精通:Django的深度探索之旅
开发语言·后端·python·系统架构·django
czliutz6 小时前
Phpstudy博客网站apache2日志分析python代码
开发语言·python