WebAssembly关键概念及生命周期
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)交换数据的主要方式
- 类比: 一个简单的、扁平的
ArrayBuffer或SharedArrayBuffer。它没有复杂的结构(如 JS 对象),就是一大块原始的二进制数据 - 关键特性:
- 线性地址空间: 通过简单的整数索引(指针)来访问数据,这非常高效,符合低级语言(如 C/C++、Rust)的内存模型
- 安全: WebAssembly 代码不能访问实例线性内存之外的任何内存。这提供了强大的内存安全隔离
- 可导入/导出: JavaScript 可以创建 Memory 对象并将其导入给 Wasm 实例,也可以从实例导出的 Memory 对象中读取和写入数据。这使得 JS 和 Wasm 之间可以高效地传递大量数据(如图像、音频缓冲区)
1.4 函数
- 定义: WebAssembly 执行的基本单元。函数可以接受一系列值作为参数,并返回一系列值作为结果
- 关键特性:
- 强类型: 所有函数签名都有明确定义的值类型(如
i32,i64,f32,f64) - 两种来源:
- 内部函数: 在模块内部定义
- 导入函数: 从宿主环境(如 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)可以控制表格的增长,但无法篡改其中的函数引用类型,保证了类型安全
- 实现函数指针和动态函数调用: 在 C/C++ 中,函数指针是通过代码在内存中的地址来调用的。但在 Wasm 的安全沙箱中,这是不允许的(因为内存可以被任意修改,导致安全风险)。表格提供了一个安全间接层:函数引用存储在受保护的表格中,调用时使用一个稳定的索引(而不是原始指针)来查找函数。JavaScript 的
1.9 全局变量
- 定义: 在单个 WebAssembly 实例内部全局可访问的变量,并且可以在模块内部或由宿主环境进行可变或不可变的设置
- 类比: 全局变量
- 关键特性:
- 跨多个函数调用保持状态,而不会像局部变量那样在函数返回后消失
- 可以是可变的 (mutable) 或不可变的 (constant)
- 可以导入和导出,这意味着 JavaScript 可以读取或设置 Wasm 实例的全局变量
2、生命周期

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 | const instance = await WebAssembly.instantiate(module, { |
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
- 重新创建
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Oreo's Blog!