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 列出了几个主要的工具。
| 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 所示。

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.load 和 i32.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 所示。
| 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 |