WebAssembly

Table of Contents

1. WebAssembly 简介

WebAssembly (often shortened to Wasm) is an open standard that defines a portable binary-code format for executable programs, and a corresponding textual assembly language. WebAssembly became a World Wide Web Consortium recommendation on 5 December 2019 and, alongside HTML, CSS, and JavaScript, is the fourth language to run natively in browsers.

WebAssembly key concepts:

  • Module: Represents a WebAssembly binary that has been compiled by the browser into executable machine code. A Module is stateless. A Module declares imports and exports just like an ES2015module.
  • Memory: A resizable ArrayBuffer that contains the linear array of bytes read and written by WebAssembly’s low-level memory access instructions.
  • Table: A resizable typed array of references (e.g. to functions) that could not otherwise be stored as raw bytes in Memory (for safety and portability reasons).
  • Instance: A Module paired with all the state it uses at runtime including a Memory, Table, and set of imported values. An Instance is like an ES2015 module that has been loaded into a particular global with a particular set of imports.

1.1. 安装环境

https://github.com/WebAssembly/wabt 可以下载 Wasm 相关工具,表 1 列出了几个主要的工具。

Table 1: WebAssembly Binary Toolkit
wat2wasm translate from WebAssembly text format to the WebAssembly binary format
wasm2wat the inverse of wat2wasm, translate from the binary format back to the text format (also known as a .wat)
wasm-objdump print information about a wasm binary. Similiar to objdump.
wasm-interp decode and run a WebAssembly binary file using a stack-based interpreter

2. Text Format

WebAssembly 有两种格式:文本格式和二进制格式。两种格式是等价的,这里先介绍文本格式,后面再介绍二进制格式。

2.1. S-expression

WebAssembly 的顶层代码单元是一个 module。在文本格式中,module 由一个大的 S-expression 组成。

S-expression 的语法比较简洁,各个结构都用小括号表示,括号内的第一个元素决定了当前节点的类型。如 (* 2 (+ 3 4)) 是 S-expression,它表达的结构如图 1 所示。

wasm_s_exp.png

Figure 1: S-expression (* 2 (+ 3 4)) 所表达的结构

2.2. 简单例子

下面 S-expression 是 wasm 的例子:

(module (memory 1) (func))        ;; example of module

它表示 module 下有两个节点:一个 memory 节点、另一个 func 节点。两个逗号 ;; 表示注释。

下面是最简单的 wasm(一个空 module):

(module)

2.3. 函数

Wasm module 中的代码由一个个函数组成。函数有下面形式:

( func <signature> <locals> <body> )

其中,signature 由参数(param)和返回值(result)组成,如:

(func (param i32) (param i32) (result f64) ... )

2.3.1. Stack-Based Virtual Machine

Wasm 虚拟机是“基于栈”的(而不是“基于寄存器”的),其基本理念是每种类型的指令都是在栈上执行数值的“入栈”、“出栈”操作。

例如,下面函数可实现把两个数相加,结果保存在栈上:

(func (param i32) (param i32) (result i32)       ;; 实现两个数相加
  local.get 0
  local.get 1
  i32.add)

local.get 0, local.get 1 的功能是为把它读到的第 0, 1 个局部变量值“压入”到栈上; i32.add 的功能是从栈上“取出”两个 i32 类型值计算它们的和(以 2^32 求模),最后把结果“压入”栈上。

由于 local.get 使用数字索引来指向某个条目容易让人混淆,因此, 也可以通过别名的方式来访问它们,方法就是在类型声明的前面添加一个使用美元符号 $ 作为前缀的名字。 例如前面代码还可以写为:

(func $add (param $p1 i32) (param $p2 i32) (result i32)
  local.get $p1
  local.get $p2
  i32.add)

函数也可以定义别名,如上面例子中的 $add 就是函数的别名。

2.3.2. 函数例子

下面再看一个 Wsam 函数的例子:

(func $f (param $x i32) (result i32) (local $i i32)
  (local.get $x)
  (i32.const 1)
  (i32.add)
  (local.set $i)

  (local.get $x)
  (local.get $i)
  (i32.div_s))

它实现的功能和下面 C 函数类似:

int f(int x) {
  int i = x + 1;
  return i/x;
}

2.3.3. 调用函数

2.3.3.1. 同一模块的函数调用

为函数给定一个索引或名字, call 指令可以调用它。例如,下面的模块包含两个函数: $getAnswer 返回值 42; $getAnswerPlus1 返回,第一个函数结果加 1:

(module
  (func $getAnswer (result i32)
    i32.const 42)
  (func $getAnswerPlus1 (result i32)
    call $getAnswer
    i32.const 1
    i32.add))
2.3.3.2. 从 JavaScript 中调用 Wasm 函数

正如在一个 ES2015 模块里面一样,wasm 函数必须通过模块里面的 export 语句显式地导出,才能被 JavaScript 调用。

(module
  (func $add (param $lhs i32) (param $rhs i32) (result i32)
    get_local $lhs
    get_local $rhs
    i32.add)
  (export "add" (func $add))
)

export 也可以写在函数定义处,如上面代码可写为:

(module
  (func (export "add") (param $lhs i32) (param $rhs i32) (result i32)
    get_local $lhs
    get_local $rhs
    i32.add)
)

假设上面的源码文件名为 add.wat,通过 wat2wasm 转换为了名为 add.wasm 的二进制文件。

在 JavaScript 中,我们可以使用 WebAssembly.instantiateStreaming 来实例化模块,并执行导出的 add 函数:

WebAssembly.instantiateStreaming(fetch('add.wasm'))
  .then(obj => {
    console.log(obj.instance.exports.add(1, 2));  // "3"
  });
2.3.3.3. Wasm 中调用 JavaScript 函数

我们已经见过 JavaScript 调用 WebAssembly 函数,但是 WebAssembly 如何调用 JavaScript 函数呢?让我们看一个例子:

(module
  (import "console" "log" (func $log (param i32)))
  (func (export "logIt")
    i32.const 13
    call $log))

这里的 import 语句意思是从 console 模块导入 log 函数。另外,你可以看到在 logIt 函数中,通过 call 指令调用了 JavaScrpit 导入的函数 log。

2.4. WebAssembly Memory

Wasm 目前只支持 4 种数据类型:i32/i64/f32/f64。

Wasm 没有字符串数据类型。为了处理字符串及其他复杂数据类型,Wasm 提供了 Memory。

按照 Wasm 的定义,Memory 就是一个随着时间增长的字节数组。Wasm 包含诸如 i32.loadi32.store 指令来实现对线性内存的读写。

从 JavaScript 的角度来看,内存就是一个 ArrayBuffer,并且它是可变大小的。字符串就是 Memory 中连续的一些字节。指定 offset 和 length,可确定了字符串的位置。

可以在 JavaScript 中创建 Memory 对象,在 Wasm 中使用(后文将介绍);也可以在 Wasm 中创建 Memory 对象,在 JavaScript 中使用(暂不介绍)。

2.4.1. JavaScript 中创建 Memory 对象,Wasm 中使用

下面介绍在 JavaScript 中创建 Memory 对象,Wasm 中使用的例子。

JavaScript 中的代码如下:

var memory = new WebAssembly.Memory({initial:1});             // 创建 Memory 对象

function consoleLogString(offset, length) {
  var bytes = new Uint8Array(memory.buffer, offset, length);
  var string = new TextDecoder('utf8').decode(bytes);
  console.log(string);
}

var importObject = { console: { log: consoleLogString }, js: { mem: memory } };

WebAssembly.instantiateStreaming(fetch('logger2.wasm'), importObject)
  .then(obj => {
    obj.instance.exports.writeHi();               // 输出内存中的字符串 Hi
  });

Wasm 中的代码如下:

(module
  (import "console" "log" (func $log (param i32 i32)))
  (import "js" "mem" (memory 1))
  (data (i32.const 0) "Hi")        ;; 往内存中写入 Hi
  (func (export "writeHi")         ;; 导出为 writeHi 方法
    i32.const 0  ;; pass offset 0 to log
    i32.const 2  ;; pass length 2 to log
    call $log))  ;; 最终会调用 JS 中的 consoleLogString 方法

2.5. WebAssembly Tables(call_indirect)

Table 是 Wasm 中一个令人困惑的概念。

为了了解为什么表格是必须的,我们首先需要观察前面介绍过的 call 指令,它接受一个静态函数索引,并且只调用了一个函数。但是,如果被调用者是一个“运行时”才有的值呢?

为此,Wasm 增加了 call_indirect 指令。你可能会想,把“被调用者”的地址保存在 Memory 某个位置就行了, call_indirect 时指定 Memory 中的位置即可。但这是不安全的,Memory 会把存储的原始内容作为字节暴露出去,并且这会使得 wasm 内容能够任意的查看和修改原始函数地址,而这在网络上是不被允许的。

解决方案,是在一个新的 Table 中存储“函数地址”,传递表格的索引给 call_indirect 。因此, call_indirect 的操作数可以是一个 i32 类型索引值。

Wasm 中使用 table 可以创建 Table,使用 elem 可以往 Table 中写入内容。如:

(module
  (table 2 funcref)
  (func $f1 (result i32)
    i32.const 42)
  (func $f2 (result i32)
    i32.const 13)
  (elem (i32.const 0) $f1 $f2)          ;; 往 table 中写了两个函数地址
  (type $return_i32 (func (result i32)))
  (func (export "callByIndex") (param $i i32) (result i32)
    local.get $i
    call_indirect (type $return_i32))   ;; 通过 call_indirect 调用 Table 中的函数
)

在 JavaScrpit 中可以这样调用上面代码中导出的函数 callByIndex:

WebAssembly.instantiateStreaming(fetch('wasm-table.wasm'))
  .then(obj => {
    console.log(obj.instance.exports.callByIndex(0)); // returns 42
    console.log(obj.instance.exports.callByIndex(1)); // returns 13
    console.log(obj.instance.exports.callByIndex(2)); // returns an error, because there is no index position 2 in the table
  });

3. Binary Format

使用 wat2wasm 可以把文本格式的 Wasm 代码转换为二进制格式。二进制格式可以参考:WebAssembly Binary Format

4. Instruction and Binary Opcode

Wasm 指令和 Opcode 的对应关系如表 2 所示。

Table 2: Index of Instructions
Instruction Binary Opcode
unreachable 0x00
nop 0x01
block bt 0x02
loop bt 0x03
if bt 0x04
else 0x05
(reserved) 0x06
(reserved) 0x07
(reserved) 0x08
(reserved) 0x09
(reserved) 0x0A
end 0x0B
br 0x0C
br_if l 0x0D
br_table l*l 0x0E
return 0x0F
call x 0x10
call_indirect x 0x11
(reserved) 0x12
(reserved) 0x13
(reserved) 0x14
(reserved) 0x15
(reserved) 0x16
(reserved) 0x17
(reserved) 0x18
(reserved) 0x19
drop 0x1A
select 0x1B
(reserved) 0x1C
(reserved) 0x1D
(reserved) 0x1E
(reserved) 0x1F
local.get x 0x20
local.set x 0x21
local.tee x 0x22
global.get x 0x23
global.set x 0x24
(reserved) 0x25
(reserved) 0x26
(reserved) 0x27
i32.load memarg 0x28
i64.load memarg 0x29
f32.load memarg 0x2A
f64.load memarg 0x2B
i32.load8_s memarg 0x2C
i32.load8_u memarg 0x2D
i32.load16_s memarg 0x2E
i32.load16_u memarg 0x2F
i64.load8_s memarg 0x30
i64.load8_u memarg 0x31
i64.load16_s memarg 0x32
i64.load16_u memarg 0x33
i64.load32_s memarg 0x34
i64.load32_u memarg 0x35
i32.store memarg 0x36
i64.store memarg 0x37
f32.store memarg 0x38
f64.store memarg 0x39
i32.store8 memarg 0x3A
i32.store16 memarg 0x3B
i64.store8 memarg 0x3C
i64.store16 memarg 0x3D
i64.store32 memarg 0x3E
memory.size 0x3F
memory.grow 0x40
i32.const i32 0x41
i64.const i64 0x42
f32.const f32 0x43
f64.const f64 0x44
i32.eqz 0x45
i32.eq 0x46
i32.ne 0x47
i32.lt_s 0x48
i32.lt_u 0x49
i32.gt_s 0x4A
i32.gt_u 0x4B
i32.le_s 0x4C
i32.le_u 0x4D
i32.ge_s 0x4E
i32.ge_u 0x4F
i64.eqz 0x50
i64.eq 0x51
i64.ne 0x52
i64.lt_s 0x53
i64.lt_u 0x54
i64.gt_s 0x55
i64.gt_u 0x56
i64.le_s 0x57
i64.le_u 0x58
i64.ge_s 0x59
i64.ge_u 0x5A
f32.eq 0x5B
f32.ne 0x5C
f32.lt 0x5D
f32.gt 0x5E
f32.le 0x5F
f32.ge 0x60
f64.eq 0x61
f64.ne 0x62
f64.It 0x63
f64.gt 0x64
f64.le 0x65
f64.ge 0x66
i32.clz 0x67
i32.ctz 0x68
i32.popcnt 0x69
132.add 0x6A
132.sub 0x6B
132.mul 0x6C
i32.div_s 0x6D
i32.div_u 0x6E
i32.rem_s 0x6F
i32.rem_u 0x70
132.and 0x71
i32.or 0x72
132.xor 0x73
i32.shl 0x74
i32.shr_s 0x75
i32.shr_u 0x76
132.rotl 0x77
i32.rotr 0x78
i64.clz 0x79
i64.ctz 0x7A
i64.popcnt 0x7B
i64.add 0x7C
164.sub 0x7D
i64.mul 0x7E
i64.div_s 0x7F
i64.div_u 0x80
i64.rem_s 0x81
i64.rem_u 0x82
i64.and 0x83
i64.or 0x84
i64.xor 0x85
i64.shl 0x86
i64.shr_s 0x87
i64.shr_u 0x88
i64.rotl 0x89
i64.rotr 0x8A
f32.abs 0x8B
f32.neg 0x8C
f32.ceil 0x8D
f32.floor 0x8E
f32.trunc 0x8F
f32.nearest 0x90
f32.sqrt 0x91
f32.add 0x92
f32.sub 0x93
f32.mul 0x94
f32.div 0x95
f32.min 0x96
f32.max 0x97
f32.copysign 0x98
f64.abs 0x99
f64.neg 0x9A
f64.ceil 0x9B
f64.floor 0x9C
f64.trunc 0x9D
f64.nearest 0x9E
f64.sqrt 0x9F
f64.add 0xA0
f64.sub 0xA1
f64.mul 0xA2
f64.div 0xA3
f64.min 0xA4
f64.max 0xA5
f64.copysign 0xA6
i32.wrap_i64 0xA7
i32.trunc_f32_s 0xA8
i32.trunc_f32_u 0xA9
i32.trunc_f64_s 0xAA
i32.trunc_f64_u 0xAB
i64.extend_i32_s 0xAC
i64.extend_i32_u 0xAD
i64.trunc_f32_s 0xAE
i64.trunc_f32 0xAF
i64.trunc_f64_s 0xB0
i64.trunc_f64_u 0xB1
f32.convert_i32_s 0xB2
f32.convert_i32_u 0xB3
f32.convert_i64_s 0xB4
f32.convert_i64_u 0xB5
f32.demote_f64 0xB6
f64.convert_i32_s 0xB7
f64.convert_i32_u 0xB8
f64.convert_64_s 0xB9
f64.convert_64_u 0xBA
f64.promote_f32 0xBB
i32.reinterpret_f32 0xBC
i64.reinterpret_f64 0xBD
f32.reinterpret_i32 0xBE
f64.reinterpret_i64 0xBF
i32.extend8_s 0xC0
i32.extend16_s 0xC1
i64.extend8_s 0xC2
i64.extend16_s 0xC3
i64.extend32_s 0xC4
i32.trunc_sat_f32_s 0xFC 0x00
i32.trunc_sat_f32_u 0xFC 0x01
i32.trunc_sat_f64_s 0xFC 0x02
i32.trunc_sat_f64_u 0xFC 0x03
i64.trunc_sat_f32_s 0xFC 0x04
i64.trunc_sat_f32_u 0xFC 0x05
i64.trunc_sat_f64_s 0xFC 0x06
i64.trunc_sat_f64_u 0xFC 0x07

参考:WebAssembly Index of Instructions

5. 参考

Author: cig01

Created: <2020-03-28 Sat>

Last updated: <2020-06-26 Fri>

Creator: Emacs 27.1 (Org mode 9.4)