本文主要参考 MDN 教程:理解 WebAssembly 文本格式,介绍 WebAssembly 核心概念 中所讲概念在 WAT 中如何表示。
WAT 文件可通过格式转换工具 wat2wasm 转为 Wasm。
.wat
是 Wasm 文本格式文件,其内容表示为一个 S-表达式。S-表达式是一种表示树的非常古老而简单的文本格式。树中的每个节点用一个 ()
表示,括号内的第一个标签表示节点类型,后续以空格分隔的列表表示该节点的属性或子节点。由于一个 Wasm 文件就是一个 Module,因此 .wat
文件内容就是根节点为 module
的树。
(module (memory 1) (func))
Wasm 树大多是一些指令列表,不会嵌套太深。
函数
一个函数表示为如下节点:
(func <signature> <locals> <body>)
-
signature:函数类型,由一系列参数类型声明
(param)
和返回值类型声明(result)
构成。(func (param i32) (param i32) (result f64) ...)
-
locals:局部变量声明。
(func (param i32) (param i32) (result f64) (local f64) ...)
-
body:由底层指令列表构成的函数体。
变量读写
local.get
、local.set
指令可以读、写局部变量和函数参数。可以通过变量索引来读写,也可以用 $
开头的名字对变量命名后,通过变量名读写。
(func (param $p1 i32) (param $p2 f32) (local $loc f64)
local.get $p1
local.get $p2
local.get $loc)
Stack Machine
Wasm 的执行基于 Stack Machine,即,每种类型的指令都将一定数目的值入栈或出栈。比如:
local.get
将相应的局部变量的值入栈。i32.add
从栈中 pop 出两个i32
值,相加后将i32
结果值入栈。
函数调用时,从空栈开始不断入栈和出栈,最终栈中剩下的值就是函数的返回值。Wasm 会精确校验栈中剩下的值,以保证和返回值类型声明相匹配。
(func (param $p i32) (result i32)
local.get $p
local.get $p
i32.add)
函数调用
Module 内部的函数调用可通过 call
指令实现:
(module
(func $getAnswer (result i32)
i32.const 42)
(func $getAnswerPlus1 (result i32)
call $getAnswer
i32.const 1
i32.add)
)
i32.const
:定义一个 32 位整数并入栈。
Export
Wasm 中的函数可以——通过 export
节点——导出供 JS 使用:
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
(export "add" (func $add))
)
(export "add" (func $add))
:$add
是 Wasm 内部的函数名,"add"
指定暴露给 JS 的函数名。
JS 可通过 instance.exports.add
访问该函数:
WebAssembly.instantiateStreaming(fetch("add.wasm")).then(({ instance }) => {
console.log(instance.exports.add(1, 2)); // 3
});
Wasm 中也可以通过为 func
节点增加一个 export
子节点来导出:
(module
(func (export "add") (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
)
Import
使用 import
节点,可以在 Wasm 中调用 JS:
(module
(import "console" "log" (func $log (param i32)))
(func (export "logIt")
i32.const 13
call $log)
)
(import "console" "log" (func $log (param i32)))
:"console"
指定导入的模块名,"log"
指定该模块下的函数名,函数签名(func $log (param i32))
用于 Wasm 静态检查。
声明了 import
的 Module 在实例化时必须传入相应的对象:
const importObject = {
console: {
log(arg) {
console.log(arg);
},
},
};
WebAssembly.instantiateStreaming(fetch("logger.wasm"), importObject).then(
({ instance }) => {
instance.exports.logIt(); // 13
}
);
Memory
内存通过 memory
节点声明:
(import "js" "mem" (memory 10))
(memory 10)
:至少需要 10 个 Wasm 页。
Table
引用表 Table 通过 table
节点来声明:
(module
(table 2 funcref)
(elem (i32.const 0) $f1 $f2)
(func $f1 (result i32)
i32.const 42)
(func $f2 (result i32)
i32.const 13)
(type $return_i32 (func (result i32)))
(func (export "callByIndex") (param $i i32) (result i32)
local.get $i
call_indirect (type $return_i32))
)
(table 2 funcref)
:引用表中存放了两个函数引用。(elem (i32.const 0) $f1 $f2)
:将$f1
、$f2
存放到引用表中,(i32.const 0)
表示引用表的索引从 0 开始。(type $return_i32 (func (result i32)))
:定义了一个返回i32
的函数类型$return_i32
。call_indirect (type $return_i32)
:从栈中 pop 一个值,以该值为索引调用引用表中相应的函数,并检查该函数是否为$return_i32
类型。
JS 代码如下:
WebAssembly.instantiateStreaming(fetch("table.wasm")).then(({ instance }) => {
console.log(instance.exports.callByIndex(0)); // 42
console.log(instance.exports.callByIndex(1)); // 13
});
Global
全局变量通过 global
节点声明:
(module
(global $g (import "js" "global") (mut i32))
(func (export "getGlobal") (result i32)
global.get $g)
(func (export "incGlobal")
global.get $g
i32.const 1
i32.add
global.set $g)
)
(mut i32)
:该值为 32 位整数,可写。
在 JS 中创建一个 Global 并导入 Wasm:
const global = new WebAssembly.Global({ value: "i32", mutable: true }, 42);
const importObject = {
js: { global },
};
WebAssembly.instantiateStreaming(fetch("global.wasm"), importObject).then(
({ instance }) => {
const { getGlobal, incGlobal } = instance.exports;
console.log(getGlobal()); // 42
incGlobal();
incGlobal();
console.log(getGlobal()); // 44
}
);