基本结构
Wasm 规范定义了模块的文本格式(WebAssembly Text Format, 简称 WAT)
Wasm 文本格式使用 S- 表达式描述模块。
使用了大量圆括号,特别适合描述类似抽象语法树 (AST)的树形结构
Wasm 文本格式的整体结构
(module
(type ...);;
(import ...);;
(func ...);;
(table ...);;
(memory ...);;
(global ...);;
(export ...);;
(statr ...);;
(elem ...);;
(data ...);;
)
从整体上看,Wasm 文本格式和二进制格式基本是一致的(除了表现形式明显不同以外,在结构上,两种格式还有几个较大的不同之处)
- 文本格式以域(Field)为单位组织内容。域相当于二进制段中的项目,但不一定要连续出现,WAT 编译器会把同类型的域收集起来,合并成二进制段。
- 在二进制格式中,除了自定义段以外,其他段必须按照 ID 递增的顺序排列,文本格式中的域则没有这么严格的限制。(导入域必须出现在函数域,表域,内存域和全局域之前)
- 文本格式中的域和二进制格式中的段基本是一一对应的,但是有两种情况例外。
第一种是文本格式没有单独的代码域,只有函数域名。 WAT 编译器会将函数域收集起来,分别生成函数段和代码段。
第二种是文本格式没有自定义域,没办法描述自定义段(已经有提案建议增强 WAT 语法,支持表达自定义数据)
- 文本格式提供了多种内联写法。例如:函数域,表域,内存域,全局域可以内敛导入或导出域,表域可以内敛元素,内存域可以内联数据域,函数域和导入域可以内联类型域。这些内联写法只是”语法糖“,WAT编译器会妥善处理
注释
WAT 支持两种类型的注释
单行注释以 ;; 开始,直到行尾
跨行注释以 (;; 开始,以 ;;)结束
;; 单行注释以 ;; 开始,直到行尾
(;;
跨行注释
;;)
类型域
类型域用于定义函数类型
定义一个接收两个 i32 类型参数,返回一个 i32 类型结果的函数类型
(module
(type (func (param i32) (param i32) (result i32)))
)
圆括号是 WAT 语言的主要分隔符(Separator)
module, type, func, param, result 等是 WAT 语言的关键字(keyword, 以小写字母开头)
由于 WAT 语言较为简单,大部分语法规则都可以通过示例代码理解
可以给函数类型分配一个标识符(Identifier, 以 $ 符开头)就是给它取个名字
这样就可以在其他地方通过调用名字来引用函数类型,不必直接使用索引
多个参数可以简写在同一个 param 块里,多个返回值可以简写在同一个 result 块里
(module
(type $ft1 (func (param i32 i32) (result i32))) ;; 命名函数 ft1, 接收两个 int32 整数,返回 int32 整数
(type $ft2 (func (param f64) (result f64 f64))) ;; 命名函数 ft2, 接收一个 float64 浮点数,返回两个 float64 浮点数
)
导入和导出域
导入域
Wasm 模块可以导入或者导出函数,表,内存和全局变量这 4 种类型的元素
导入和导出域也支持这 4 种类型
(module
(type $ft1 (func (param i32 i32) (result i32)))
(import "env" "f1" (func $f1 (type $ft1)))
(import "env" "t1" (table $t 1 8 funcref))
(import "env" "m1" (memory $m 4 16))
(import "env" "g1" (global $g1 i32)) ;; immutable
(import "env" "g2" (global $g2 (mut i32))) (;; mutable ;;)
)
在导入域中,需要指明模块名,导入元素名,以及导入元素的具体类型
模块名和元素名用字符串指定,以双引号分隔
导入域也可以附带一个标识符,这样就可以在后面通过名字引用被导入的元素
上面的例子中,类型域是单独出现的,并在导入函数中通过名字(也可以通过索引)引用。
当多个导入函数有相同的类型时,这种写法可以避免代码重复出现
如果某个函数类型只被使用一次,也可以把它内联进导入域中
(module
(import "env" "f1"
(func $f1
(param i32 i32) (result i32) ;; inline function type
)
)
)
导出域
导出域只须指定导出名和元素索引
更好的做法是通过标识符引用元素,实际索引交给 WAT 编译器去计算。
导出名在整个模块内必须是唯一的,这点一定要注意
(module
;;...
(export "f1" (func $f1))
(export "f2" (func $f2))
(export "t1" (func $t))
(export "m1" (memory $m))
(export "g1" (global $g1))
(export "g2" (global $g2))
)
导入域和导出域可以内联在函数,表,内存,全局域中
导出域的内联写法
(module
(type $f1 (func (param i32 i32)(result i32)))
(func $f1( import "env" "f1") (type $ft1))
(table $t1 (import "env" "t") 1 8 funcref)
(memory $m1 (import "env" "g1") 4 16)
(global $g1 (import "env" "g1") i32)
(global $g2 (import "env" "g2") (mut i32))
)
导出域的内联写法
(module
(func $f (export "f1") ...)
(func $t (export "t") ...)
(memory $m (export "m") ...)
(global $g (export "g1") ...)
)
函数域
函数域定义函数的类型和局部变量,并给出函数的指令。
WAT 编译器会把函数域拆开,把类型索引放在函数段中,把局部变量信息和字节码放在代码段中
简写方式
(module
(type $ft1 (func (param i32 i32) (result i32)))
(func $add (type $ft1)
(local i64 i64)
(i64.add (local.get 2) (local.get 3)) (drop)
(i32.add (local.get 0) (local.get 1))
)
)
函数的参数本质上也是局部变量,同函数域里定义的局部变量一起构成了函数的局部变量空间,索引从 0 开始递增。
可以把函数类型内联进函数域,并把 param 块拆成多个参数,这样就可以给参数起名字
给参数和局部变量起了名字,就可以在变量指令中通过名字而非索引定位参数或局部变量,这样有助于提高代码的可读性
;; 内联类型定义,并给参数和局部变量分配标识符(有助于提高代码的可读性)
(module
(func $add (param $a i32) (param $b i32) (result i32)
(local $c i64) (local $d i64)
(i64.add (local.get $c) (local.get $d)) (drop)
(i32.add (local.get $a) (local.get $b))
)
)
表域和表元素
模块最多只能导入或者定义一张表,表域最多只能出现一次,但元素与可以出现多次
表域需要描述表的类型,包括限制和元素类型(目前只能是 funcref)
元素域可以指定若干个函数索引,以及第一个索引的表内偏移量
(module
(func $f1) (func $f2) (func $f3)
(table 10 20 funcref)
(elem (offset (i32.const 5)) $f1 $f2 $f3)
)
表和内存偏移量以及全局变量的初始值是通过常量指令指定的
表域中可以内联一个元素域(使用这种方式无法指定表的限制,只能由编译器根据内联元素进行推测),也无法指定元素表内偏移量(只能从 0 开始)
(module
(func $f1) (func $f2) (func $f3)
(table funcref ;; min=3, max=3
(elem $f1 $f2 $f3) ;; inline elem, offset=0
)
)
内存域与数据域
和表相似,模块最多只能导入或定义一块内存,所以内存与最多也只能出现一次,数据域则可以出现多次。
内存域需要描述内存的类型(即页数上下限)
数据域需要指定内存的偏移量和初始数据。
;; 内存域和数据域的写法
(module
(memory 4 16)
(data (offset (i32.const 100)) "Hello, ")
(data (offset (i32.const 108)) "World!\n")
)
内存初始数据是以字符串形式指定的
除了普通字符,还可以使用转义序列在字符串中嵌入回车换行等特殊符号,十六进制编码的任意字节,以及 unicode 代码点
和表域相似,内存与中也可以内联一个数据域,但是使用这种方式无法指定内存的页数(只能由编译器根据内联数据进行推测),也无法指定内存的偏移量(只能从 0 开始)。
数据域中的数据还可以写成多个字符串
;; 数据域内联写法
(module
(memory ;; min=1, max=1
(data "Hello, " "World!\n") ;; inline data, offset=0
)
)
全局域
全局域定义全局变量,需要描述全局变量的类型和可变性,并给定初始值
和其他元素一样,全局域也可以指定标识符,这样就可以在变量指令中使用全局变量的名字而非索引
(module
(global $g1 (mut i32 ) (i32.const 100)) ;;mutable
(global $g1 (mut i32) (i32.const 200)) ;; mutable
(global $g3 f32 (f32.const 3.14)) ;; immutable
(global $g4 f64 (f64.const 2.71)) ;; immutable
(func
(global.get $g1) (global.set $g2)
)
)
起始域
起始域写法最为简单,只须指定一个起始函数名或索引
(module
(func $main ...)
(start $main)
)
指令
Wasm 文本格式里,指令有两种写法: 普通形式和折叠形式
普通形式和其二进制编码格式基本一致,非常容易理解
折叠形式则完全是语法糖,更方便人类编写,但是 WAT 编译器会把它们全部展开
普通形式
其他指令
指令的普通形式写法非常直接,对于大部分指令来说,就是操作码后跟立即数
;; 除控制指令外的其他指令的一般写法
(module
(memory 1 2)
(global $g1 (mut i32) (i32.const 0))
(func $f1)
(func $f2 (param $a i32)
i32.const 123
i32.load offset=100 align=4
i32.const 456
i32.store offset=200
global.get $g1
local.get $a
i32.add
call $f1
drop
)
)
大部分指令的立即数(如果有的话)都是不能省略的,必须以数值或者名字的形式跟在操作码后面
内存读写系列指令是个例外,offset 和 align 这两个立即数都是可选的,需要显式指定(名称和数值用等号分开)
结构化控制指令(block,loop 和 if)
可以指定可选的参数和结果类型,必须以 end 结尾。
if 指令还可以用 else 分隔成两条分支。
;; block、loop、if、br 和 br_if
(module
(func $foo
block $11 (result i32)
i32.const 123
br $11
loop $12
i32.const 123
br_if $12
end
end
drop
)
(func $max(param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.gt_s
if(result i32)
local.get $a
else
local.get $b
end
)
)
折叠形式
指令的普通形式较难理解写起来略为繁琐,使用折叠形式可以缓解这个情况
可以对普通指令做三步调整,把它变为折叠形式1 使用圆括号把指令包起来;
2.如果是结构化控制指令,把 end 去掉,if指令要稍微麻烦一些
3.如果某条指令(无论是普通还是折叠形式)和它前面的几条指令从逻辑上可以看成一组操作,则把前几条指令折叠进该指令
折叠指令实际上是把指令从扁平结构变成了树形结构,WAT 编译器会按照后续遍历(从左到右访问子树,最后访问树根)的方式展开折叠指令
;; 按照上面的 3 个步骤改写前面的例子,改写后的代码应该是下面这样(注意 if 指令)
(module
(func $foo
(block $11 (result i32)
(i32.const 123)
(br $11)
(loop $12
(br_if $12 (i32.const 123))
)
)
(drop)
)
(func $max (param $a i32) (param $b i32) (result i32)
(if (result i32)
(i32.gt_s (local.get $a) (local.get $b))
(then (local.get $a))
(else (local.get $b))
)
)
)
(module
(func $max (param $a i32) (param $b i32) (result i32)
(i32.gt_s (local.get $a) (local.get $b))
(if $1 (result i32)
(then (local.get $a))
(else (local.get $b))
)
)
)