WebAssembly 入门教程 / 03 - 基础概念
03 - 基础概念
理解 WebAssembly 的核心抽象,是从"会用"到"深入理解"的关键一步。
3.1 WAT 文本格式与二进制格式
WebAssembly 有两种等价的表示方式:
| 格式 | 文件扩展名 | 用途 | 可读性 |
|---|---|---|---|
| WAT(WebAssembly Text Format) | .wat | 人类可读的文本表示 | ✅ 高 |
| Wasm(WebAssembly Binary) | .wasm | 机器可执行的二进制 | ❌ 低 |
WAT 示例
(module
(func $add (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add
)
(export "add" (func $add))
)
等价的 Wasm 二进制(十六进制)
00 61 73 6d ; 魔数 \0asm
01 00 00 00 ; 版本 1
01 07 ; 类型段: 7 字节
01 ; 1 个函数类型
60 ; func
02 7f 7f ; 2 个 i32 参数
01 7f ; 1 个 i32 返回值
03 02 ; 函数段
01 00 ; 函数 0 使用类型 0
07 07 ; 导出段
01 ; 1 个导出
03 61 64 64 ; "add"
00 00 ; func index 0
0a 09 ; 代码段
01 ; 1 个函数体
07 ; 函数体大小
00 ; 0 个局部
20 00 ; local.get 0
20 01 ; local.get 1
6a ; i32.add
0b ; end
WAT ↔ Wasm 转换
# WAT → Wasm
wat2wasm hello.wat -o hello.wasm
# Wasm → WAT
wasm2wat hello.wasm -o hello.wat
# 使用 wasm-tools(Rust 工具链)
wasm-tools parse hello.wat -o hello.wasm
wasm-tools print hello.wasm -o hello.wat
魔数与版本
每个合法的 Wasm 二进制文件以 8 字节头部开头:
字节 0-3: 0x00 0x61 0x73 0x6D → "\0asm"(魔数)
字节 4-7: 0x01 0x00 0x00 0x00 → 版本 1
3.2 模块(Module)
模块(Module)是 WebAssembly 的基本编译单元,类似于 ELF 或 DLL 文件。
模块结构
一个 Wasm 模块由以下段(Section)组成:
| 段编号 | 名称 | 说明 | 必需 |
|---|---|---|---|
| 0 | Custom | 自定义数据(调试信息等) | ❌ |
| 1 | Type | 函数类型签名 | ✅ |
| 2 | Import | 导入声明 | ❌ |
| 3 | Function | 函数声明(引用类型段) | ❌ |
| 4 | Table | 表声明 | ❌ |
| 5 | Memory | 内存声明 | ❌ |
| 6 | Global | 全局变量 | ❌ |
| 7 | Export | 导出声明 | ❌ |
| 8 | Start | 启动函数 | ❌ |
| 9 | Element | 表初始化数据 | ❌ |
| 10 | Code | 函数体(实际代码) | ❌ |
| 11 | Data | 数据段(初始内存内容) | ❌ |
| 12 | Data Count | 数据段数量 | ❌ |
模块的两种使用方式
// 方式 1:一次性实例化
const { instance } = await WebAssembly.instantiate(buffer, imports);
// 方式 2:先编译再实例化(可复用已编译模块)
const module = await WebAssembly.compile(buffer);
const instance1 = await WebAssembly.instantiate(module, imports1);
const instance2 = await WebAssembly.instantiate(module, imports2);
💡 关键区别:
compile()只编译不实例化,可以将编译后的模块发送给 Web Worker,实现一次编译、多处实例化。
3.3 线性内存(Linear Memory)
线性内存是 WebAssembly 中唯一的内存模型——一块连续的、可增长的字节数组。
内存模型特点
| 特性 | 说明 |
|---|---|
| 连续 | 地址从 0 开始,连续递增 |
| 按字节寻址 | 最小访问单位是 1 字节 |
| 小端序 | 多字节值使用 Little-Endian |
| 可增长 | 可以按 64KB 的"页"(Page)为单位增长 |
| 有上限 | 最多 4GB(32-bit Wasm,2^32 字节) |
| 隔离 | 每个实例拥有独立的内存空间 |
内存操作
(module
(memory (export "memory") 1) ;; 申请 1 页(64KB)内存
(func $store (param $addr i32) (param $value i32)
local.get $addr
local.get $value
i32.store ;; 将 value 存储到 addr
)
(func $load (param $addr i32) (result i32)
local.get $addr
i32.load ;; 从 addr 读取一个 i32
)
(export "store" (func $store))
(export "load" (func $load))
)
内存大小与增长
内存大小单位:
┌─────────────────────────────────────┐
│ 1 Page = 64 KiB = 65,536 Bytes │
│ │
│ 最小: 1 页 (64 KiB) │
│ 最大: 65,536 页 (4 GiB) │
└─────────────────────────────────────┘
;; 声明内存:初始 1 页,最大 16 页
(memory $mem 1 16)
;; 在 JS 中增长内存
;; memory.grow(pages) → 返回增长前的页数,失败返回 -1
(func $grow (param $pages i32) (result i32)
local.get $pages
memory.grow
)
内存增长注意事项
⚠️ 重要:当
memory.grow()被调用时,底层ArrayBuffer可能被替换(因为需要一块更大的连续内存)。此时,之前通过new Uint8Array(memory.buffer)创建的视图将失效。必须重新创建视图。
const memory = new WebAssembly.Memory({ initial: 1 });
let view = new Uint8Array(memory.buffer);
// 调用 Wasm 内部的 grow...
// 之后必须重新创建视图
view = new Uint8Array(memory.buffer);
3.4 表(Table)
表(Table)是一个带类型的数组,目前主要存储函数引用(funcref)或外部引用(externref)。表允许间接调用函数,类似 C 中的函数指针。
为什么需要表?
Wasm 不能直接将函数指针放入线性内存(因为函数不是地址空间中的数据)。表就是为此设计的间接层。
表声明与使用
(module
;; 声明函数类型
(type $callback (func (param i32) (result i32)))
;; 声明表:初始 2 个元素,类型为 funcref
(table $tbl 2 funcref)
;; 两个函数
(func $double (type $callback)
local.get 0
i32.const 2
i32.mul
)
(func $triple (type $callback)
local.get 0
i32.const 3
i32.mul
)
;; 初始化表
(elem (i32.const 0) $double $triple)
;; 间接调用:根据索引调用表中的函数
(func $call_by_index (param $idx i32) (param $val i32) (result i32)
local.get $val
local.get $idx
call_indirect (type $callback)
)
(export "callByIndex" (func $call_by_index))
)
表类型
| 类型 | 说明 |
|---|---|
funcref | 函数引用,用于间接调用 |
externref | 外部引用(不透明指针),可用于存储 JS 对象引用 |
表 vs 内存存储函数
传统方式(无表):
函数地址 → 放入内存 → 跳转
⚠️ Wasm 不支持直接内存跳转
Wasm 方式(有表):
函数引用 → 放入表 → call_indirect 查表调用
✅ 安全、类型检查
3.5 导入与导出(Import & Export)
导入和导出是 Wasm 模块与宿主环境(如 JavaScript)之间的桥梁。
导入(Import)
Wasm 模块可以声明需要从宿主环境导入的资源:
(module
;; 导入一个外部函数
(import "env" "log" (func $log (param i32)))
;; 导入外部内存
(import "env" "memory" (memory 1))
;; 导入外部全局变量
(import "env" "counter" (global $counter (mut i32)))
(func $main
i32.const 42
call $log
)
(export "main" (func $main))
)
// JavaScript 提供导入
const imports = {
env: {
log: (value) => console.log('Wasm says:', value),
memory: new WebAssembly.Memory({ initial: 1 }),
counter: new WebAssembly.Global({ value: 'i32', mutable: true }, 0)
}
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('module.wasm'), imports
);
instance.exports.main();
导出(Export)
模块可以导出函数、内存、表和全局变量:
(export "add" (func $add))
(export "memory" (memory 0))
(export "table" (table 0))
(export "counter" (global $counter))
导入/导出的模块名与字段名
导入格式: (import "模块名" "字段名" (资源描述))
示例:
(import "env" "log" (func $log ...))
↑ ↑
模块名 字段名
在 JS 中对应:
imports["env"]["log"] = someFunction;
导入导出的 4 种资源类型
| 资源 | 可导入 | 可导出 | 说明 |
|---|---|---|---|
| 函数 | ✅ | ✅ | 最常用 |
| 内存 | ✅ | ✅ | 共享内存场景 |
| 表 | ✅ | ✅ | 间接调用 |
| 全局变量 | ✅ | ✅ | 全局状态 |
3.6 实例化(Instantiation)
实例化是将编译好的 Wasm 模块与具体导入值绑定,创建可执行实例的过程。
实例化流程
1. 获取 .wasm 字节码
│
▼
2. WebAssembly.compile() ← 编译阶段(可选,可复用)
│
▼
3. WebAssembly.instantiate(module, imports) ← 实例化阶段
│
▼
4. instance.exports.xxx() ← 调用导出函数
完整示例
// 方式 1:一步完成编译+实例化
async function instantiate1() {
const response = await fetch('module.wasm');
const buffer = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(buffer, imports);
return instance;
}
// 方式 2:分步(推荐,可复用已编译模块)
async function instantiate2() {
const response = await fetch('module.wasm');
const buffer = await response.arrayBuffer();
const module = await WebAssembly.compile(buffer);
const instance1 = await WebAssembly.instantiate(module, imports);
const instance2 = await WebAssembly.instantiate(module, imports);
return [instance1, instance2];
}
// 方式 3:instantiateStreaming(最高效,直接从网络流编译)
async function instantiate3() {
const { instance } = await WebAssembly.instantiateStreaming(
fetch('module.wasm'), imports
);
return instance;
}
实例化 API 对比
| API | 说明 | 优点 | 缺点 |
|---|---|---|---|
WebAssembly.instantiate(buffer, imports) | 一步完成 | 简单 | 不能复用模块 |
WebAssembly.instantiate(module, imports) | 先编译后实例化 | 可复用 | 两步操作 |
WebAssembly.instantiateStreaming(...) | 流式编译+实例化 | 最快 | 需要正确的 MIME 类型 |
WebAssembly.compile(buffer) | 仅编译 | 可在 Worker 间传递 | 需要额外实例化 |
WebAssembly.compileStreaming(...) | 流式编译 | 高效 | 需要正确的 MIME 类型 |
MIME 类型配置
⚠️ 注意:
instantiateStreaming和compileStreaming需要服务器返回正确的 MIME 类型application/wasm。
# Nginx 配置
types {
application/wasm wasm;
}
// Express.js 配置
express.static.mime.define({ 'application/wasm': ['wasm'] });
3.7 值类型(Value Types)
WebAssembly 支持 4 种基本数值类型:
| 类型 | WAT 关键字 | 说明 | 位宽 |
|---|---|---|---|
| 32 位整数 | i32 | 有符号/无符号 32 位整数 | 32 bit |
| 64 位整数 | i64 | 有符号/无符号 64 位整数 | 64 bit |
| 32 位浮点 | f32 | IEEE 754 单精度 | 32 bit |
| 64 位浮点 | f64 | IEEE 754 双精度 | 64 bit |
扩展类型(提案)
| 类型 | 说明 | 状态 |
|---|---|---|
v128 | 128 位 SIMD 向量 | 稳定(Phase 4) |
funcref | 函数引用 | 稳定 |
externref | 外部引用 | 稳定 |
anyref 等 | GC 类型提案 | 进行中 |
3.8 调用栈与控制流
Wasm 使用基于栈的计算模型:
;; 计算 (3 + 4) * 2
i32.const 3 ;; 栈: [3]
i32.const 4 ;; 栈: [3, 4]
i32.add ;; 栈: [7]
i32.const 2 ;; 栈: [7, 2]
i32.mul ;; 栈: [14]
栈操作过程
操作 栈状态
───────────── ─────────────
i32.const 3 → [3]
i32.const 4 → [3, 4]
i32.add → [7] // 弹出两个 i32,压入结果
i32.const 2 → [7, 2]
i32.mul → [14] // 弹出两个 i32,压入结果
💡 注意:Wasm 的栈式执行是验证时的概念,实际运行时 JIT/AOT 编译器会将其优化为寄存器操作。
3.9 全局变量(Global)
(module
;; 不可变全局变量
(global $PI f32 (f32.const 3.14159))
;; 可变全局变量
(global $counter (mut i32) (i32.const 0))
(func $increment (result i32)
global.get $counter
i32.const 1
i32.add
global.set $counter
global.get $counter
)
(export "increment" (func $increment))
(export "PI" (global $PI))
)
3.10 元素段与数据段
数据段(Data Section)
初始化线性内存的内容:
(module
(memory (export "memory") 1)
;; 在地址 0 处初始化字符串 "Hello"
(data (i32.const 0) "Hello\00")
;; 在地址 100 处初始化一个 i32 值
(data (i32.const 100) "\2a\00\00\00") ;; 小端序 42
)
元素段(Element Section)
初始化表的内容:
(module
(table 2 funcref)
(func $f1 (result i32) (i32.const 1))
(func $f2 (result i32) (i32.const 2))
;; 将 $f1 和 $f2 填入表的索引 0 和 1
(elem (i32.const 0) $f1 $f2)
)
3.11 完整示例:字符串处理
(module
(memory (export "memory") 1)
;; 写入字符串到内存
(data (i32.const 0) "Hello, WebAssembly!\00")
;; 计算字符串长度
(func $strlen (param $ptr i32) (result i32)
(local $len i32)
(local.set $len (i32.const 0))
(block $break
(loop $loop
;; 如果当前字节为 0,跳出循环
(br_if $break
(i32.eqz (i32.load8_u (local.get $ptr)))
)
;; 指针前进 1
(local.set $ptr (i32.add (local.get $ptr) (i32.const 1)))
;; 长度加 1
(local.set $len (i32.add (local.get $len) (i32.const 1)))
(br $loop)
)
)
(local.get $len)
)
;; 导出字符串指针和长度函数
(export "strlen" (func $strlen))
)
const { instance } = await WebAssembly.instantiateStreaming(fetch('string.wasm'));
const { memory, strlen } = instance.exports;
const str = new TextDecoder().decode(
new Uint8Array(memory.buffer, 0, strlen(0))
);
console.log(str); // "Hello, WebAssembly!"
3.12 注意事项
⚠️ 线性内存不等于堆内存:虽然 Wasm 的线性内存常被用作"堆",但它只是一个字节数组,没有内置的内存分配器。分配器(如 malloc/free)需要在应用层实现或由工具链提供。
⚠️ 32 位地址限制:当前 MVP 的 Wasm 使用 32 位地址,最多支持 4GB 内存。64 位地址(memory64)是一个进行中的提案。
⚠️ 无符号 vs 有符号:Wasm 的 i32/i64 没有区分有符号/无符号,而是通过指令区分解释方式(如
i32.div_svsi32.div_u)。
3.13 扩展阅读
- WebAssembly Core Specification
- MDN: WebAssembly Concepts
- WebAssembly 设计文档
- Understanding the Binary Encoding
下一章:04 - WAT 文本格式 — 深入学习 WAT 语法、指令集和控制流。