。bWasm文本格式使用S-表达式描述模块,源自Lisp语言,使用了大量圆括号,特别适合描述类似抽象语法树的树形结构。
Wasm文本格式和二进制格式基本是一致的,除了表现形式不同以外,在结构上,两种格式还有几个较大的不同之处。
1、二进制是以段(Section)为单位组织数据的,文本格式则是以域(Field)为单位组织内容,域相当于二进制段中的项目,但不一定会连续出现,WAT编译器会把同类型的域收集起来,合并成二进制段。
2、在二进制格式中,除了自定义段,其他段必须按照ID递增的顺序排列,文本格式中的域则没有这么严格的限制,不过导入域必须出现在函数域、表域、内存域和全局域之前。
3、文本格式的域和二进制的段基本是一一对应的,但是有两种情况例外,第一种是文本格式没有单独的代码域、只有函数域。WAT编译器会将函数域收集起来,分别生成函数段和代码段,第二种是文本格式没有自定义域,每办法描述自定义段。
4、文本格式提供了多种内联写法。例如:函数域、表域、内存域、全局域可以内联导入或导出域,表域可以内联元素域,内存域可以内联数据域,函数域和导入域可以内联类型域。这些内联写法只是“语法糖”,WAT编译器会妥善处理。
类型域
类型域用于定义函数类型,下面例子定义了一个接收两个i32类型参数、返回1个i32类型结果的函数类型。
(module
(type (func (param i32) (param i32) (result i32)))
)
圆括号是WAT语言主要的分隔符,module、type、func、param、result等是WAT语言关键字(以小些字母开头)
可以给函数类型分配一个标识符(以$符开头),换句话说就是给它取个名字,这样在其他地方通过调用名字来引用函数类型,不必直接使用索引,多个参可以写在同一个param里,多个返回值可以写在同一个result块里。
(module
(type $ft1 (func (param i32 i32) (result i32)))
(type $ft2 (func (param f64) (result f64 f64)))
)
导入和导出域
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))) (;; immutable ;;)
)
导入域中需要指明模块名、导入元素名,以及导入元素的具体类型。模块名和元素名用字符串指定,双引号分隔。导入域可以附带一个标识符,这样可以在后面通过名字引入被导入的元素
两种注释方式:
;; xxxxx
(;; xxxxx ;;)
如果某类型只被使用一次,也可以把它内联进导入域中
(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" (table $t))
(export "m1" (memory $m))
(export "g1" (global $g1))
(export "g2" (global $g2))
)
导入域和导出域可以内联在函数、表、内存、全局域中。
下面为导入域内联写法
(module
(type $ft1 (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" "m") 4 16)
(global $g1 (import "env" "g1") i32)
(global $g2 (import "env" "g2") (mut i32))
)
下面为导出域内联写法
(module
(func $f (export "f1") ... )
(table $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开始递增。
以下为内联类型定义,可有助于提高代码的可读性。
(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
(elem $f1 $f2 $f3)
)
)
内存域和数据域
和表相似,模块最多只能导入或定义一块内存,所以内存域最多也只能出现一次,数据域则可以出现多次,内存域需要描述内存的类型(即页数上下限),数据域需要指定内存的偏移量和初始数据。
(module
(memory 4 16)
(data (offset (i32.const 100)) "Hello, ")
(data (offset (i32.const 108)) "World!\n")
)
内存初始数据是以字符串形式指定的,除了普通字符,还可以使用转义序列在字符串中嵌入回车换行等特殊符号、十六进制编码的任意字节,以及Unicode代码点。
和表域相似,内存域中也可以内联一个数据域,无法指定内存页数、也无法指定内存的偏移量(只能从0开始),另外数据域中的数据还可以写成多个字符串。
下面例子为数据域的内联写法:
(module
(memory
(data "Hello, " "World!\n")
)
)
全局域
全局域定义全局变量,需要描述全局变量的类型和可变性,并给定初始值。和其他元素一样,全局域也可以指定标识符,这样就可以在变量指令中使用全局变量的名字而非索引。
(module
(global $g1 (mut i32) (i32.const 100))
(global $g2 (mut i32) (i32.const 100))
(global $g3 f32 (f32.const 3.14))
(global $g4 f64 (f64.const 2.71))
(func
(global.get $g1) (global.set $g2)
)
)
起始域
只须指定一个起始函数名或索引。
(module
(func $main ... )
(start $main)
)