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 |