入门
在 JS 中加载 Wasm
WebAssembly (Wasm) 是一种二进制格式,在浏览器中以接近原生的速度运行。instantiateStreaming 加载并编译 .wasm 文件。导出的是可从 JavaScript 调用的函数。
// Load and instantiate a .wasm module
WebAssembly.instantiateStreaming(fetch('module.wasm'))
.then(result => {
const { instance } = result;
// call exported functions
console.log(instance.exports.add(2, 3)); // 5
});
// Alternative: from ArrayBuffer
const bytes = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]);
WebAssembly.instantiate(bytes).then(result => {
console.log(result.instance.exports);
});WebAssembly 模块与实例
WebAssembly.Module 是编译后的无状态表示,可在多个实例间安全复用。WebAssembly.Instance 持有运行时状态(内存、全局变量)。编译一次、多次实例化的方式非常适合生成并行 worker。
// Two-step: compile then instantiate
const module = await WebAssembly.compile(bytes);
const instance = await WebAssembly.instantiate(module, importObject);
console.log(instance.exports.add(1, 2));
// Module is stateless & shareable across instances
const instance2 = await WebAssembly.instantiate(module, importObject);
// Each instance has its own memory and globals浏览器支持与特性检测
WebAssembly 1.0 在所有现代浏览器中均受支持。较新的提案(SIMD、异常、线程、GC)需要特性检测。WebAssembly.validate 检查字节是否构成有效模块而不进行编译。
// Check basic WebAssembly support
if (typeof WebAssembly !== 'undefined') {
console.log('WebAssembly is supported');
}
// Verify the engine can actually compile
WebAssembly.validate(new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00
])); // true if valid module
// Feature-detect newer proposals
const hasSIMD = typeof WebAssembly.Module !== 'undefined';
const hasExceptions = 'exceptions' in WebAssembly;流式与非流式
instantiateStreaming 在网络流式传输时编译模块,启动更快。服务器必须发送 'Content-Type: application/wasm'。如果 CDN 或静态主机没有该头,则需回退到 ArrayBuffer 路径。
// Streaming (preferred): compiles while downloading
WebAssembly.instantiateStreaming(fetch('m.wasm'), imports);
// Non-streaming: must download fully first
const res = await fetch('m.wasm');
const buf = await res.arrayBuffer();
WebAssembly.instantiate(buf, imports);
// Streaming requires correct MIME type:
// Content-Type: application/wasmWasm 魔数与文件头
Wasm 二进制魔数是 ASCII 字符串 '\0asm',后跟 4 字节小端版本号。该文件头用于在编译前验证文件是否为 Wasm。格式按节组织(Type、Import、Function、Memory 等)。
// Every .wasm file starts with this 8-byte header:
// 0x00 0x61 0x73 0x6d -> "\0asm"
// 0x01 0x00 0x00 0x00 -> version 1
const magic = [0x00, 0x61, 0x73, 0x6d];
const version = [0x01, 0x00, 0x00, 0x00];
// Quick validity check
function isWasm(bytes) {
return bytes[0] === 0x00 && bytes[1] === 0x61
&& bytes[2] === 0x73 && bytes[3] === 0x6d;
}Hello Wasm(最小示例)
一个最小 Wasm 应用:模块从 JS 导入 'log' 函数并导出 'greet' 调用它。WAT (WebAssembly Text) 格式是人类可读的表示,用 wat2wasm 编译为二进制。浏览器只执行二进制形式。
<!-- index.html -->
<script>
WebAssembly.instantiateStreaming(fetch('hello.wasm'))
.then(({ instance }) => {
instance.exports.greet(); // logs "Hello, Wasm!"
});
</script>
;; hello.wat (compiled to hello.wasm via wat2wasm)
(module
(import "console" "log" (func $log (param i32)))
(func (export "greet")
i32.const 72 ;; 'H'
call $log
)
)WAT 文本格式
模块与 S-表达式
WAT (WebAssembly Text 格式) 是 Wasm 的文本表示。一切都被包裹在 (module ...) S-表达式中。指令以栈式顺序书写:先压入操作数,再由操作消费它们。
;; WAT uses s-expressions: (keyword args...)
(module
;; function definitions, exports, etc. go here
)
;; Nested expressions form the instruction sequence
(module
(func $add (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add
)
(export "add" (func $add))
)WAT 中的注释
WAT 支持两种注释样式:';;' 用于单行,'(; ... ;)' 用于块注释。块注释可以嵌套并跨越多行,适合在开发时禁用大段代码。
;; Single-line comment (starts with ;;)
(func (export "f")
i32.const 1 ;; push 1
drop ;; discard
)
(; Block comment
can span
multiple lines ;)
(func (export "g")
nop
)函数定义语法
函数用 (func ...) 声明。参数使用 (param $name type),结果使用 (result type)。'$name' 是可选的,没有它时函数/局部变量通过数字索引引用。多值返回需要 multi-value 提案(已广泛支持)。
;; Named function with params and result
(func $name (param $p1 i32) (param $p2 f64) (result i32)
;; body
)
;; Anonymous function (referenced by index)
(func (param i32) (result i32)
local.get 0
)
;; Multiple results (multi-value proposal)
(func $swap (param i32 i32) (result i32 i32)
local.get 1
local.get 0
)内联导出与导入
WAT 支持在函数、内存、表和全局变量上使用内联 (export "name") 和 (import "module" "field") 注解,是展开 (export ...)/(import ...) 形式的简写。两者编译为相同的二进制输出。
;; Inline export on a function
(func (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add
)
;; Inline import
(func $log (import "env" "log") (param i32))
;; Inline export on memory
(memory (export "mem") 1)
;; Expanded form (equivalent)
(func $add (param i32 i32) (result i32) ...)
(export "add" (func $add))数据段
数据段在实例化时初始化线性内存(active),或通过 memory.init 懒加载(passive)。字符串包含转义序列,如 \00 表示空终止符、\n 表示换行。data.drop 在复制后释放段的存储。
;; Initialize memory with bytes at offset 0
(memory 1)
(data (i32.const 0) "Hello, Wasm!\00")
;; Named data segment (active)
(data $msg (i32.const 0) "Hello\00")
;; Passive data segment (used with memory.init)
(data $passive "lazy-loaded bytes")
;; Drop a data segment to free its memory
(data.drop $msg)wat2wasm 与 wasm2wat 转换
WABT (WebAssembly Binary Toolkit) 提供 wat2wasm(文本→二进制)、wasm2wat(二进制→文本)、wasm-validate 和 wasm-objdump。这些工具对检查、调试和手写 Wasm 模块至关重要。.wat 形式面向人类,.wasm 形式才是浏览器执行的。
# Install the WebAssembly Binary Toolkit (WABT)
# macOS: brew install wabt
# Ubuntu: apt install wabt
# Convert text -> binary
wat2wasm hello.wat -o hello.wasm
# Convert binary -> text (disassemble)
wasm2wat hello.wasm -o hello.wat
# Validate a .wasm file
wasm-validate hello.wasm
# Print module structure
wasm-objdump -x hello.wasm值类型
基本值类型 (i32/i64/f32/f64)
Wasm 只有四种基本值类型:i32、i64、f32、f64。整数的符号性由操作决定(如 i32.div_s 与 i32.div_u),而非类型。这让类型系统保持最小化且可预测。
;; Wasm has exactly four basic value types:
;; i32 - 32-bit integer
;; i64 - 64-bit integer
;; f32 - 32-bit float
;; f64 - 64-bit float
(func (param i32) (param i64) (param f32) (param f64)
(result f64)
local.get 2 ;; f32
f64.promote_f32
)
;; Integers are signed or unsigned PER OPERATION,
;; not per type. Use i32.add_s / i32.div_u etc.WAT 中的数字字面量
WAT 中的数字字面量支持十进制、十六进制 (0x) 和二进制 (0b) 形式。浮点数支持科学计数法、无穷大 (inf) 和 NaN。WAT 字面量默认无符号,负整数用前导 '-' 编码。
;; Integer literals
i32.const 42
i32.const -7
i32.const 0xff ;; hex
i32.const 0b1010 ;; binary
;; Float literals
f64.const 3.14
f64.const 1e10
f64.const -0.0
f64.const inf
f64.const nan
;; 64-bit
i64.const 9007199254740992 ;; 2^53类型转换
转换使用一致的命名方案:extend(小→大)、wrap(大→小)、convert(整型↔浮点)、reinterpret(位级转换)、demote/promote(浮点精度)。有符号变体用 _s,无符号用 _u。
;; Extend i32 to i64
i64.extend_i32_s ;; sign-extend
i64.extend_i32_u ;; zero-extend
;; Wrap i64 to i32
i32.wrap_i64
;; Integer <-> Float
f32.convert_i32_s
f64.convert_i64_u
i32.trunc_f32_s ;; truncate (toward zero)
i32.reinterpret_f32 ;; bit-level reinterpret
;; Demote / promote floats
f32.demote_f64
f64.promote_f32v128 SIMD 类型
v128 类型将 16 字节打包进单个 SIMD 寄存器。通道按 i32x4(4×int32)、f32x4(4×float32)、i16x8、i8x16、f64x2 寻址。SIMD 通过每条指令处理多个值来加速图像/音频/加密工作负载。
;; SIMD: 128-bit vector type (fixed-width SIMD proposal)
(module
(func (param v128) (result v128)
local.get 0
v128.not ;; bitwise NOT
)
(func (result v128)
v128.const i32x4 1 2 3 4
)
)
;; Vector operations: i32x4, i16x8, i8x16, f32x4, f64x2
v128.const f32x4 1.0 2.0 3.0 4.0
i32x4.add引用类型 (funcref/externref)
引用类型提案添加了 funcref(函数指针)和 externref(不透明主机引用)。externref 功能强大:让 Wasm 持有 JS 对象而无需复制或序列化。与表结合,可实现灵活的回调模式。
;; Reference types proposal adds:
;; funcref - reference to a function (in a table)
;; externref - opaque reference to a host (JS) object
(module
(table 1 funcref)
(func (param externref) (result externref)
local.get 0 ;; pass through a JS object
)
)
;; In JS, externref lets you pass arbitrary objects
;; through Wasm without serialization:
instance.exports.store({ hello: 'world' });函数
定义与调用函数
函数是代码的基本单元。call 通过名称/索引调用函数。参数在调用前压入栈,结果在函数返回时留在栈上。栈必须匹配 (result ...) 声明。
(module
(func $add (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add
)
(func $double (param $x i32) (result i32)
local.get $x
local.get $x
i32.add ;; or: i32.const 2 i32.mul
)
(func (export "use")
i32.const 5
i32.const 7
call $add ;; -> 12 on stack
drop
)
)参数与局部变量
参数是从 0 开始索引的不可变局部变量。声明的局部 变量 (local $name type) 被零初始化且可变。使用 local.get、local.set 和 local.tee(设置并留在栈上)。局部变量是内存/全局变量之外 Wasm 唯一的可变存储。
(func $f (param $a i32) (param $b i32) (result i32)
(local $sum i32) ;; mutable local
(local $i i32)
local.get $a
local.get $b
i32.add
local.set $sum ;; store into local
i32.const 0
local.set $i
local.get $sum
)
;; tee: set and keep on stack
local.tee $sum ;; $sum = top; stays on stack多返回值
multi-value 提案允许函数返回多个值(并接受零个)。自 2020 年起所有主流引擎都支持。这消除了通过内存传递出参的需要,使 swap/minmax 等惯用法变得自然。
;; Multi-value: return two i32 values
(func $minmax (param $a i32) (param $b i32) (result i32 i32)
local.get $a
local.get $b
i32.lt_s
if (result i32 i32)
local.get $a
local.get $b
else
local.get $b
local.get $a
end
)
;; Caller receives both on the stack
(func (export "use")
i32.const 3
i32.const 8
call $minmax
;; stack: [min=3, max=8]
drop drop
)递归
Wasm 支持直接递归——函数可以调用自身。MVP Wasm 没有尾调用优化(tail-call 提案添加了 return_call)。深递归有栈溢出风险,因此生产代码优先使用迭代循环。
(module
(func $fact (param $n i32) (result i32)
local.get $n
i32.const 1
i32.le_s
if (result i32)
i32.const 1
else
local.get $n
local.get $n
i32.const 1
i32.sub
call $fact ;; recursive call
i32.mul
end
)
(export "factorial" (func $fact))
)内联导出与命名调用
函数定义上的内联 (export "name") 是独立 (export ...) 声明的简写。call_ref(引用类型提案)直接通过 funcref 调用函数,无需表查找即可实现一等函数值。
(module
;; Inline export on definition
(func $add (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add
)
;; call_ref: call via funcref (reference types)
(func $caller (param $f funcref) (param i32) (result i32)
local.get 1
local.get 0
call_ref (param i32) (result i32)
)
)Start 函数
模块可以声明一个 (start $func) 函数,在实例化时自动运行——用于初始化。它不接受参数也不返回值。每个模块只允许一个 start 函数。
(module
(func $init
;; runs once at instantiation
i32.const 0
i32.const 42
i32.store
)
(start $init) ;; declare as start function
(memory (export "mem") 1)
)
;; The start function runs automatically after
;; instantiation, before instantiate() resolves.
;; It takes no params and returns nothing.内存
声明线性内存
线性内存是一 个连续的类似 ArrayBuffer 的区域,按 64KiB 页增长。(memory 1 10) 声明 initial=1、max=10。内存是在 Wasm 和 JS 之间传递批量数据(字符串、数组)的唯一方式。导出它以便从 JS 访问。
(module
;; 1 page = 64KiB. Declare 1 page, max 10.
(memory $mem 1 10)
(export "memory" (memory $mem))
;; JS-side: read/write the exported memory
;; const buf = new Uint8Array(instance.exports.memory.buffer);
)内存扩容
memory.grow 添加页并返回之前的大小(页数),失败时返回 -1。关键的是,扩容会分离底层的 ArrayBuffer——所有现有的 TypedArray 视图将不可用。在任意 grow 调用后必须重新创建视图。
(func $grow (result i32)
i32.const 1 ;; request 1 more page
memory.grow ;; returns previous size (or -1)
)
;; In JS, growing detaches the old buffer!
const prev = instance.exports.grow();
const buf = new Uint8Array(instance.exports.memory.buffer);
// OLD buf references are now detached — re-create views!加载与存储操作
load/store 按大小(8/16/32/64 位)和符号扩展(_s/_u)有变体。offset=N 向地址操作数添加常量;align= 是提示(必须 ≤ 大小)。所有地址都是线性内存中的字节偏移。
;; Load: read from memory at address (top of stack)
i32.load offset=0 align=2 ;; 4-byte aligned load
i32.load8_u ;; 1 byte, zero-extended
i32.load16_s ;; 2 bytes, sign-extended
f64.load ;; 8-byte float load
;; Store: write value at address
i32.const 100 ;; address
i32.const 42 ;; value
i32.store ;; mem[100] = 42
i32.const 100
i32.const 0xFF
i32.store8 ;; write 1 byte内存大小与对齐
Wasm 中的对齐是性能提示,而非正确性约束——非对齐访问总是有效。'align' 值不得超过操作的自然大小。memory.size 返回当前页数(每页 64KiB)。
;; Each load/store has a natural alignment:
;; i32.load -> 4 bytes
;; i64.load -> 8 bytes
;; f32.load -> 4 bytes
;; i32.load8 -> 1 byte
;; Explicit alignment hint (does not affect correctness)
i32.load align=2 ;; may be slower but still works
;; memory.size returns current pages
(func $pages (result i32)
memory.size
)共享内存与原子操作
共享内存(threads 提案)让多个 Wasm 实例/Worker 共享一个内存以实现并行。原子操作(i32.atomic.load/store/rmw.add/cmpxchg)提供同步。在浏览器中需要跨源隔离(COOP/COEP 头)。
(module
;; Shared memory (threads proposal)
(memory 1 1 shared)
(export "memory" (memory 0))
;; Atomic operations
(func (param i32 i32)
local.get 0 ;; address
local.get 1 ;; value
i32.atomic.store
)
(func (param i32) (result i32)
local.get 0
i32.atomic.load
)
(func (param i32) (result i32)
local.get 0
i32.const 1
i32.atomic.rmw.add ;; atomic read-modify-write
)
)从内存读取字符串
Wasm 没有原生字符串类型——字符串是线性内存中 的字节序列,通常作为(指针,长度)对传递。TextEncoder/TextDecoder 处理 UTF-8 转换。写入前务必确保内存区域足够大。
// Wasm has no string type — strings live in linear memory.
// Decode UTF-8 from a (ptr, len) pair returned by Wasm:
function readString(memory, ptr, len) {
const bytes = new Uint8Array(memory.buffer, ptr, len);
return new TextDecoder('utf-8').decode(bytes);
}
// Writing a string into Wasm memory:
function writeString(memory, str, ptr) {
const bytes = new TextEncoder().encode(str);
const view = new Uint8Array(memory.buffer, ptr, bytes.length);
view.set(bytes);
return bytes.length; // return length
}表
声明表
表是引用数组(通常是 funcref),用于间接调用和动态分派。与内存(持有字节)不同,表持有 Wasm 无法伪造的不透明引用。(elem ...) 段在实例化时初始化表槽。
(module
;; A table of function references, initial size 1
(table 1 funcref)
(export "table" (table 0))
;; With max size
(table $t 4 16 funcref)
;; elem segment: initialize table slots
(elem (i32.const 0) $fn1 $fn2 $fn3)
(func $fn1)
(func $fn2)
(func $fn3)
)funcref 表与 call_indirect
call_indirect 通过索引从表项分派调用。(type ...) 声明是必需的——Wasm 在运行时检查函数签名是否匹配(签名不匹配会陷入)。这是 Wasm 中 vtable 和面向对象分派的基础。
(module
(type $bin (func (param i32 i32) (result i32)))
(table 3 funcref)
(func $add (param i32 i32) (result i32)
local.get 0 local.get 1 i32.add)
(func $sub (param i32 i32) (result i32)
local.get 0 local.get 1 i32.sub)
(func $mul (param i32 i32) (result i32)
local.get 0 local.get 1 i32.mul)
(elem (i32.const 0) $add $sub $mul)
(func (export "dispatch") (param i32 i32 i32) (result i32)
local.get 0 ;; arg a
local.get 1 ;; arg b
local.get 2 ;; table index
call_indirect (type $bin)
)
)表的扩容、读取与设置
table.grow 追加槽并返回之前的大小(或 -1)。table.get/table.set 按索引读/写条目。扩容表支持运行时注册回调。初始大小 + max 限制扩容以防止失控分配。
(func $addSlot (param funcref) (result i32)
local.get 0
i32.const 1 ;; grow by 1
table.grow ;; returns old size, or -1
)
(func $read (param i32) (result funcref)
local.get 0
table.get
)
(func $write (param i32 funcref)
local.get 0 ;; index
local.get 1 ;; funcref
table.set
)externref 表用于宿主对象
externref 表存储对宿 主(JS)对象的不透明引用——Wasm 可以持有并传递它们,但无法检查其内容。非常适合回调注册、JS 句柄表,以及将面向对象的 JS API 桥接到 Wasm。
(module
;; Table holding opaque JS objects
(table 8 externref)
(export "table" (table 0))
(func $store (param i32 externref)
local.get 0
local.get 1
table.set
)
(func $load (param i32) (result externref)
local.get 0
table.get
)
)
// JS: pass arbitrary objects
instance.exports.store(2, { callback: () => 42 });动态分派模式
动态分派模式:将操作存储在表中,然后按索引 call_indirect。结合强制的类型检查,提供安全的 vtable 风格多态。Rust 枚举、C++ 虚函数和解释器都以此实现分派。
(module
(type $op (func (param i32 i32) (result i32)))
(table 4 funcref)
(elem (i32.const 0) $add $sub $mul $div)
(func $add (param i32 i32) (result i32)
local.get 0 local.get 1 i32.add)
(func $sub (param i32 i32) (result i32)
local.get 0 local.get 1 i32.sub)
(func $mul (param i32 i32) (result i32)
local.get 0 local.get 1 i32.mul)
(func $div (param i32 i32) (result i32)
local.get 0 local.get 1 i32.div_s)
(func (export "apply") (param $op i32) (param $a i32) (param $b i32)
(result i32)
local.get $a
local.get $b
local.get $op
call_indirect (type $op)
)
)全局变量
声明全局变量
全局变量是模块级的单值,可以是不可变(默认)或可变(mut)。它们在函数调用间持久存在,用于计数器、配置标志和常量。可变全局变量是内存/表之外唯一的共享可变状态。
(module
;; Immutable global (const)
(global $counter (mut i32) (i32.const 0))
;; Read-only global
(global $pi f64 (f64.const 3.14159265))
(func $increment
global.get $counter
i32.const 1
i32.add
global.set $counter
)
)可变与不可变全局变量
没有 'mut',全局变量不可变,global.set 是编译错误。初始化器必须是常量表达式(如 i32.const、ref.null、不可变导入全局变量的 global.get)。默认选择不可变以确保安全。
(module
;; (global $name type init) -> immutable
;; (global $name (mut type) init) -> mutable
(global $version i32 (i32.const 3))
;; global.set $version ;; ERROR: immutable
(global $state (mut i32) (i32.const 0))
(func (export "toggle")
global.get $state
i32.eqz
if
i32.const 1 global.set $state
else
i32.const 0 global.set $state
end
)
)导入全局变量
导入的全局变量让宿主在实例化时将配置值(或 WebAssembly.Global 对象)注入模块。不可变的导入全局变量可以初始化其他全局变量。WebAssembly.Global 是可变共享全局变量的 JS 可见包装器。
(module
;; Import a global from JS
(global $env (import "env" "version") i32)
;; Mutable imported global
(global $flag (import "env" "flag") (mut i32))
(func (export "ver") (result i32)
global.get $env
)
)
// JS side
const imports = {
env: {
version: 3,
flag: new WebAssembly.Global({ value: 'i32', mutable: true }, 0)
}
};导出与共享全局变量
导出的全局变量在 JS 中作为 WebAssembly.Global 对象访问,具有 .value 属性(可读,如果可变则可写)。在多个模块实例间共享 Global 对象是不触碰内存而协调状态的简洁方式。
(module
(global $g (export "counter") (mut i32) (i32.const 0))
(func (export "inc")
global.get $g
i32.const 1
i32.add
global.set $g
)
)
// JS reads/writes the global directly
const g = instance.exports.counter;
g.value; // 0
instance.exports.inc();
g.value; // 1
g.value = 100; // write directly全局变量的用例
全局变量适合三种模式:(1) 导入的配置常量,(2) 跨实例共享的持久计数器/状态,(3) 缓存的计算结果。对于单值它们比内存更简单,如果通过 JS 共享则对多个实例可见。