强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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)组成:

段编号名称说明必需
0Custom自定义数据(调试信息等)
1Type函数类型签名
2Import导入声明
3Function函数声明(引用类型段)
4Table表声明
5Memory内存声明
6Global全局变量
7Export导出声明
8Start启动函数
9Element表初始化数据
10Code函数体(实际代码)
11Data数据段(初始内存内容)
12Data 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 类型配置

⚠️ 注意instantiateStreamingcompileStreaming 需要服务器返回正确的 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 位浮点f32IEEE 754 单精度32 bit
64 位浮点f64IEEE 754 双精度64 bit

扩展类型(提案)

类型说明状态
v128128 位 SIMD 向量稳定(Phase 4)
funcref函数引用稳定
externref外部引用稳定
anyrefGC 类型提案进行中

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_s vs i32.div_u)。


3.13 扩展阅读


下一章04 - WAT 文本格式 — 深入学习 WAT 语法、指令集和控制流。