WebAssembly关键概念及生命周期

1、关键概念

1.1 模块

  • 定义: .wasm 二进制文件的编译后形态。它是 WebAssembly 代码的静态的、可移植的表示形式
  • 类比: 一个ES6 模块.js 文件,或者一个动态链接库 (DLL / .so) 的编译后文件。它包含了代码、类型定义、导入导出声明等,但它本身还不是一个可以运行的程序
  • 关键特性:
    • 不可变: 一旦编译完成,模块内容就无法更改
    • 可缓存: 模块可以被高效地缓存,因为其内容不会改变。浏览器可以将其缓存到 IndexedDB 中,下次直接使用,极大加快加载速度
    • 可并行编译: 模块的编译可以在单独的 Web Worker 中进行,不阻塞主线程
    • 可共享: 同一个模块可以被多个实例共享(见下文)

1.2 实例

  • 定义: 一个已被实例化、具有状态的模块。它是一个在运行时、在特定上下文中被激活的模块
  • 类比: 一个 ES6 模块的导入实例(例如 import * as myModule from './myModule.js' 中的 myModule 对象),或者一个加载到内存中的 DLL。它包含了模块的可访问内存、表格、函数等
  • 关键特性:
    • 状态化: 实例拥有自己的状态,例如线性内存和表格的内容
    • 可多次实例化: 同一个模块可以创建多个独立的实例,每个实例都有自己的状态,互不干扰。这非常有用,例如,在同一个页面中运行多个相同的 Wasm 程序(如游戏、模拟器)

1.3 线性内存

  • 定义: 一个连续的、可调整大小的原始字节数组。它是 WebAssembly 代码与外部(如 JavaScript)交换数据的主要方式
  • 类比: 一个简单的、扁平的 ArrayBufferSharedArrayBuffer。它没有复杂的结构(如 JS 对象),就是一大块原始的二进制数据
  • 关键特性:
    • 线性地址空间: 通过简单的整数索引(指针)来访问数据,这非常高效,符合低级语言(如 C/C++、Rust)的内存模型
    • 安全: WebAssembly 代码不能访问实例线性内存之外的任何内存。这提供了强大的内存安全隔离
    • 可导入/导出: JavaScript 可以创建 Memory 对象并将其导入给 Wasm 实例,也可以从实例导出的 Memory 对象中读取和写入数据。这使得 JS 和 Wasm 之间可以高效地传递大量数据(如图像、音频缓冲区)

1.4 函数

  • 定义: WebAssembly 执行的基本单元。函数可以接受一系列值作为参数,并返回一系列值作为结果
  • 关键特性:
    • 强类型: 所有函数签名都有明确定义的值类型(如 i32, i64, f32, f64
    • 两种来源:
      1. 内部函数: 在模块内部定义
      2. 导入函数: 从宿主环境(如 JavaScript)导入。这使得 Wasm 可以调用外部 JS 函数,例如操作 DOM、调用 Web API(console.log, fetch

1.5 导入

  • 定义: 模块在实例化时需要从外部宿主环境获取的依赖项清单
  • 类比: ES6 模块的 import 语句。模块声明它需要什么,实例化时必须由外部提供
  • 可导入的类型: 函数、全局变量、内存、表格
  • 作用: 这是 WebAssembly 与宿主环境交互的桥梁。一个 Wasm 模块本身无法直接访问系统资源或 Web API,它必须通过导入的 JavaScript 函数来间接实现

1.6 导出

  • 定义: 模块向外部宿主环境暴露的函数、内存、表格和全局变量
  • 类比: ES6 模块的 export 语句
  • 作用: JavaScript 可以通过实例的导出对象来调用 Wasm 函数、操作 Wasm 内存等。这是 Wasm 代码被 JavaScript 调用的主要方式

1.7 堆栈

  • 定义: WebAssembly 虚拟机内部的一个后进先出 (LIFO) 数据结构,用于函数调用期间的管理
  • 作用:
    • 存储局部变量和函数参数。
    • 在执行指令时暂存中间计算结果。 Wasm 是一种堆栈机,大多数指令都是从堆栈顶部取出操作数,并将结果压回堆栈顶部(例如,指令 i32.add 会从堆栈顶弹出两个 i32 值,将它们相加,然后将结果压入堆栈)
    • 跟踪函数调用的返回地址
  • 注意: 这个堆栈是虚拟机内部实现的一部分,对开发者不可见,也与“线性内存”是分开的。你不能直接操作它

1.8 表格

  • 定义: 一个可调整大小的、类型化的引用数组(如函数引用)。它是线性内存的一个补充
  • 为什么需要它: 线性内存只能存储原始字节,无法安全地存储像函数引用这样的不透明值。表格解决了这个问题
  • 主要用途:
    • 实现函数指针和动态函数调用: 在 C/C++ 中,函数指针是通过代码在内存中的地址来调用的。但在 Wasm 的安全沙箱中,这是不允许的(因为内存可以被任意修改,导致安全风险)。表格提供了一个安全间接层:函数引用存储在受保护的表格中,调用时使用一个稳定的索引(而不是原始指针)来查找函数。JavaScript 的 call_indirect 指令就是通过表格来实现的
    • 更安全: 宿主(如 JS)可以控制表格的增长,但无法篡改其中的函数引用类型,保证了类型安全

1.9 全局变量

  • 定义: 在单个 WebAssembly 实例内部全局可访问的变量,并且可以在模块内部或由宿主环境进行可变或不可变的设置
  • 类比: 全局变量
  • 关键特性:
    • 跨多个函数调用保持状态,而不会像局部变量那样在函数返回后消失
    • 可以是可变的 (mutable)不可变的 (constant)
    • 可以导入和导出,这意味着 JavaScript 可以读取或设置 Wasm 实例的全局变量

2、生命周期

image-20250901191223392

1. 加载(fetch)

  • 来源:Wasm 文件通常以二进制 .wasm 或经过 Base64/内嵌的形式提供。
  • 方式:浏览器或 Node.js 可以通过 fetch() + WebAssembly.compileStreaming() / instantiateStreaming()加载。
1
const wasmModule = await WebAssembly.compileStreaming(fetch("module.wasm"));

2. 编译(Compilation)

  • 即时编译 (JIT):WebAssembly 模块被编译为宿主平台的原生机器码。
  • 优化:浏览器会在后台做增量编译,先快速生成可运行的机器码,再慢慢优化。
  • 结果:得到一个 WebAssembly.Module 对象(不可变,可复用)。
1
const module = await WebAssembly.compile(bytes);

3. 实例化(Instantiation)

  • 模块 (Module)导入对象 (Imports, 如 JS 函数/内存/表) 绑定,得到一个 实例 (Instance)
  • 实例包含:
    • 内存 (WebAssembly.Memory)
    • 表 (WebAssembly.Table)
    • 全局变量 (WebAssembly.Global)
    • 导出的函数 (exports)
1
2
3
4
5
6
const instance = await WebAssembly.instantiate(module, {
env: {
memory: new WebAssembly.Memory({ initial: 256 }),
abort: () => console.log("Abort called"),
}
});

4. 执行(Execution)

  • 通过 instance.exports 调用导出的函数。
  • 函数运行时会操作线性内存(Linear Memory),可能和 JS 交互。
  • WebAssembly 的函数是 同步执行 的(没有 async 语义),执行期间会阻塞当前线程。
1
instance.exports.add(3, 4); // 7

5. 生命周期管理 / 销毁(Teardown)

  • WebAssembly 本身没有手动销毁 API。
  • 实例、内存、表都受 JS 垃圾回收 (GC) 管理,只要 JS 中没有引用,它们就会被释放。
  • 如果需要“重置”,通常是:
    • 重新创建 WebAssembly.Memory
    • 或重新实例化 WebAssembly.Instance