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

WebAssembly 入门教程 / 04 - WAT 文本格式

04 - WAT 文本格式

WAT(WebAssembly Text Format)是 Wasm 的人类可读表示。掌握它,你就能直接"看到"Wasm 在做什么。


4.1 WAT 语法基础

WAT 使用 S-expression(S-表达式)语法,类似于 Lisp:

(module                           ;; 模块定义(最外层)
  (func $greet                    ;; 函数定义
    (param $name i32)             ;; 参数
    (result i32)                  ;; 返回值
    local.get $name               ;; 指令
  )
)

基本规则

规则说明
使用 () 括号所有结构都由括号包裹
注释用 ;;单行注释
块注释用 (; ... ;)多行注释
$name 命名$ 开头的标识符
缩进无意义仅为可读性,不影响语义

命名与索引

;; 两种引用函数的方式:
;; 1. 使用命名(可读性好)
call $my_function

;; 2. 使用数字索引(底层表示)
call 0

4.2 值类型

i32    ;; 32 位整数
i64    ;; 64 位整数
f32    ;; 32 位浮点数(IEEE 754 单精度)
f64    ;; 64 位浮点数(IEEE 754 双精度)
v128   ;; 128 位 SIMD 向量(需要 SIMD 提案)
funcref ;; 函数引用
externref ;; 外部引用

多返回值

;; 函数可以返回多个值
(func $divmod (param $a i32) (param $b i32) (result i32 i32)
  local.get $a
  local.get $b
  i32.div_u          ;; 商
  local.get $a
  local.get $b
  i32.rem_u          ;; 余数
)
;; 结果: 返回 [商, 余数] 两个 i32

4.3 函数

基本函数

(module
  ;; 简单的加法函数
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a    ;; 将参数 $a 压栈
    local.get $b    ;; 将参数 $b 压栈
    i32.add         ;; 弹出两个值,相加,压入结果
  )
  (export "add" (func $add))
)

函数中的局部变量

(func $example (param $x i32) (param $y i32) (result i32)
  (local $temp i32)       ;; 声明局部变量
  (local $flag i32)       ;; 可以声明多个

  ;; local.set — 赋值
  local.get $x
  local.get $y
  i32.add
  local.set $temp         ;; temp = x + y

  ;; local.tee — 赋值并保持栈上值
  i32.const 42
  local.tee $flag         ;; flag = 42,同时 42 保留在栈上

  drop                    ;; 丢弃栈顶
  local.get $temp         ;; 返回 temp
)

类型复用

(module
  ;; 定义函数类型
  (type $binop (func (param i32 i32) (result i32)))

  ;; 复用类型定义
  (func $add (type $binop)
    local.get 0
    local.get 1
    i32.add
  )
  (func $mul (type $binop)
    local.get 0
    local.get 1
    i32.mul
  )
)

4.4 控制流

Wasm 的控制流基于结构化控制,有三个核心指令:blockloopif

block — 代码块

(block $label
  ;; ... 指令 ...
  (br $label)        ;; 跳出 block
)
;; br $label 跳到这里

loop — 循环

;; 循环 10 次
(func $count_to_10
  (local $i i32)
  (local.set $i (i32.const 0))

  (block $break
    (loop $continue
      ;; i++
      local.get $i
      i32.const 1
      i32.add
      local.set $i

      ;; 如果 i < 10,继续循环
      (br_if $break (i32.ge_u (local.get $i) (i32.const 10)))
      (br $continue)
    )
  )
)

💡 关键理解blockbr 跳到块之后loopbr 跳到循环开始。这是 Wasm 控制流最重要的一点。

if — 条件分支

(func $abs (param $x i32) (result i32)
  local.get $x
  i32.const 0
  i32.lt_s           ;; x < 0 ?

  (if (result i32)
    (then
      i32.const 0
      local.get $x
      i32.sub         ;; 0 - x = -x
    )
    (else
      local.get $x    ;; 直接返回 x
    )
  )
)

if-else 嵌套

(func $sign (param $x i32) (result i32)
  (if (i32.lt_s (local.get $x) (i32.const 0))
    (then
      (i32.const -1)
    )
    (else
      (if (i32.gt_s (local.get $x) (i32.const 0))
        (then (i32.const 1))
        (else (i32.const 0))
      )
    )
  )
)

br_table — 多路跳转

(func $switch (param $case i32) (result i32)
  (block $default
    (block $case2
      (block $case1
        (block $case0
          local.get $case
          (br_table $case0 $case1 $case2 $default)
          ;; case 0 → 跳到 $case0
          ;; case 1 → 跳到 $case1
          ;; case 2 → 跳到 $case2
          ;; 其他  → 跳到 $default
        )
        ;; $case0:
        (return (i32.const 100))
      )
      ;; $case1:
      (return (i32.const 200))
    )
    ;; $case2:
    (return (i32.const 300))
  )
  ;; $default:
  (i32.const -1)
)

4.5 数值运算指令

整数运算

指令说明栈操作
i32.add加法[a, b] → [a+b]
i32.sub减法[a, b] → [a-b]
i32.mul乘法[a, b] → [a*b]
i32.div_s有符号除法[a, b] → [a/b]
i32.div_u无符号除法[a, b] → [a/b]
i32.rem_s有符号取余[a, b] → [a%b]
i32.rem_u无符号取余[a, b] → [a%b]
i32.and按位与[a, b] → [a&b]
i32.or按位或[a, b] → [a|b]
i32.xor按位异或[a, b] → [a^b]
i32.shl左移[a, b] → [a<<b]
i32.shr_s有符号右移[a, b] → [a>>b]
i32.shr_u无符号右移[a, b] → [a>>>b]
i32.rotl循环左移[a, b] → [rotl(a,b)]
i32.rotr循环右移[a, b] → [rotr(a,b)]
i32.clz前导零计数[a] → [clz(a)]
i32.ctz尾部零计数[a] → [ctz(a)]
i32.popcnt置位计数[a] → [popcnt(a)]

浮点运算

指令说明
f64.add / f64.sub / f64.mul / f64.div基本四则运算
f64.sqrt平方根
f64.min / f64.max最小值/最大值
f64.ceil / f64.floor / f64.trunc / f64.nearest取整
f64.abs / f64.neg绝对值/取反
f64.copysign符号复制

比较指令

;; 整数比较(返回 i32,0 = false,1 = true)
i32.eq      ;; 相等
i32.ne      ;; 不等
i32.lt_s    ;; 有符号小于
i32.lt_u    ;; 无符号小于
i32.gt_s    ;; 有符号大于
i32.gt_u    ;; 无符号大于
i32.le_s    ;; 有符号小于等于
i32.ge_s    ;; 有符号大于等于

;; 浮点比较
f64.eq      ;; 相等
f64.ne      ;; 不等
f64.lt      ;; 小于
f64.gt      ;; 大于
f64.le      ;; 小于等于
f64.ge      ;; 大于等于

类型转换

;; i32 → f64(有符号整数转浮点)
i32.const 42
f64.convert_i32_s    ;; → 42.0

;; f64 → i32(截断浮点为整数)
f64.const 3.14
i32.trunc_f64_s      ;; → 3

;; i32 → i64(扩展)
i32.const -1
i64.extend_i32_s     ;; → -1(符号扩展)
i64.extend_i32_u     ;; → 4294967295(零扩展)

;; i64 → i32(截断)
i64.const 123456789
i32.wrap_i64         ;; → 低 32 位

;; f32 → f64(提升)
f32.const 1.5
f64.promote_f32      ;; → 1.5

;; f64 → f32(截断)
f64.const 1.5
f32.demote_f64       ;; → 1.5

4.6 内存操作指令

加载(Load)

;; 基本加载
i32.load           ;; 从内存加载 i32(4 字节)
i64.load           ;; 从内存加载 i64(8 字节)
f32.load           ;; 从内存加载 f32(4 字节)
f64.load           ;; 从内存加载 f64(8 字节)

;; 窄加载
i32.load8_s        ;; 加载 1 字节,符号扩展为 i32
i32.load8_u        ;; 加载 1 字节,零扩展为 i32
i32.load16_s       ;; 加载 2 字节,符号扩展为 i32
i32.load16_u       ;; 加载 2 字节,零扩展为 i32
i64.load8_s        ;; 加载 1 字节,符号扩展为 i64
i64.load16_s       ;; 等等...
i64.load32_s

;; 带偏移量
i32.load offset=8  ;; 从 (addr + 8) 处加载

;; 带对齐提示
i32.load align=4   ;; 4 字节对齐

存储(Store)

;; 基本存储
i32.store          ;; 存储 i32(4 字节)
i64.store          ;; 存储 i64(8 字节)
f32.store          ;; 存储 f32
f64.store          ;; 存储 f64

;; 窄存储(只写低 8/16 位)
i32.store8         ;; 存储低 8 位
i32.store16        ;; 存储低 16 位

;; 带偏移量和对齐
i32.store offset=16 align=2

内存管理

memory.size        ;; 返回当前内存页数
memory.grow        ;; 增长内存(参数: 要增加的页数,返回: 增长前的页数)
memory.fill        ;; 填充内存区域(类似 memset)
memory.copy        ;; 复制内存区域(类似 memcpy)
memory.init $seg   ;; 从数据段初始化内存
data.drop $seg     ;; 释放数据段

4.7 逐行分析示例

示例 1:数组求和

(module
  (memory (export "memory") 1)

  ;; 假设数组起始地址为 0,每个元素 i32(4 字节)
  ;; 数组布局: [count][elem0][elem1][elem2]...
  ;;           0-3    4-7    8-11   12-15

  (func $array_sum (param $ptr i32) (result i32)
    (local $sum i32)     ;; 累加器
    (local $end i32)     ;; 结束地址
    (local $count i32)   ;; 元素数量

    ;; 读取数组长度
    (local.set $count (i32.load (local.get $ptr)))

    ;; 计算结束地址: ptr + 4 + count * 4
    (local.set $end
      (i32.add
        (i32.add (local.get $ptr) (i32.const 4))
        (i32.mul (local.get $count) (i32.const 4))
      )
    )

    ;; 累加器初始化
    (local.set $sum (i32.const 0))

    ;; 遍历数组
    (local.set $ptr (i32.add (local.get $ptr) (i32.const 4)))  ;; 跳过 count
    (block $break
      (loop $loop
        (br_if $break (i32.ge_u (local.get $ptr) (local.get $end)))

        ;; sum += *ptr
        (local.set $sum
          (i32.add (local.get $sum) (i32.load (local.get $ptr)))
        )

        ;; ptr += 4
        (local.set $ptr (i32.add (local.get $ptr) (i32.const 4)))
        (br $loop)
      )
    )
    (local.get $sum)
  )

  (export "arraySum" (func $array_sum))
)

示例 2:字符串反转

(module
  (memory (export "memory") 1)

  (func $reverse_string (param $start i32) (param $len i32)
    (local $left i32)
    (local $right i32)
    (local $tmp i32)

    (local.set $left (local.get $start))
    (local.set $right
      (i32.sub
        (i32.add (local.get $start) (local.get $len))
        (i32.const 1)
      )
    )

    (block $done
      (loop $swap
        (br_if $done (i32.ge_u (local.get $left) (local.get $right)))

        ;; 交换 left 和 right 处的字节
        (local.set $tmp (i32.load8_u (local.get $left)))
        (i32.store8 (local.get $left) (i32.load8_u (local.get $right)))
        (i32.store8 (local.get $right) (local.get $tmp))

        ;; left++, right--
        (local.set $left (i32.add (local.get $left) (i32.const 1)))
        (local.set $right (i32.sub (local.get $right) (i32.const 1)))
        (br $swap)
      )
    )
  )

  (export "reverseString" (func $reverse_string))
)

4.8 不可达指令与陷阱

;; unreachable — 标记不可达代码,执行时触发 trap
(func $check_positive (param $x i32) (result i32)
  (if (i32.lt_s (local.get $x) (i32.const 0))
    (then (unreachable))  ;; 负数会触发 trap
  )
  (local.get $x)
)

;; nop — 空操作(什么都不做)
nop

常见 trap 原因:

Trap 原因说明
unreachable 执行手动触发的 trap
内存越界访问访问超出内存边界
除以零整数除以零
栈溢出调用栈过深
间接调用类型不匹配call_indirect 的类型检查失败
表访问越界访问超出表边界

4.9 常见模式速查

条件赋值

;; result = (condition) ? a : b
(if (result i32) (local.get $condition)
  (then (local.get $a))
  (else (local.get $b))
)

最小值 / 最大值

;; min(a, b)
(local.get $a)
(local.get $b)
i32.lt_s
(if (result i32) (then (local.get $a)) (else (local.get $b)))

;; 使用内置指令(浮点)
f64.min
f64.max

循环累加

(local $acc i32)
(local $i i32)
(block $break
  (loop $continue
    ;; ... 使用 $i, 更新 $acc ...
    (local.set $i (i32.add (local.get $i) (i32.const 1)))
    (br_if $break (i32.ge_u (local.get $i) (i32.const N)))
    (br $continue)
  )
)

4.10 注意事项

⚠️ block vs loop 的区别block 中的 br 跳到块后,loop 中的 br 跳回循环头部。初学者最常犯的错误是混淆二者。

⚠️ 类型安全:Wasm 是强类型的。你不能把 i32 和 f32 混用,WAT 文本格式和二进制格式都会在编译/验证时检查类型。

⚠️ 栈平衡:每个代码块在入口和出口处的栈状态必须与类型签名匹配。违反会导致验证失败。


4.11 扩展阅读


下一章05 - C/C++ 编译到 Wasm — 使用 Emscripten 将 C/C++ 代码编译为 WebAssembly。