WAT 入门

128 阅读2分钟

本文主要参考 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.getlocal.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
  }
);