Wasm 介绍(七):文本格式

栏目: IT技术 · 发布时间: 4年前

内容简介:前面的文章详细介绍了WebAssembly(简称Wasm)二进制格式和指令集,这篇文章将介绍Wasm文本格式(WebAssembly Text Format,后面简称WAT)。WAT采用了S-表达式写法,整体结构如下所示:

Wasm 介绍(七):文本格式

前面的文章详细介绍了WebAssembly(简称Wasm)二进制格式和指令集,这篇文章将介绍Wasm文本格式(WebAssembly Text Format,后面简称WAT)。

整体结构

WAT采用了S-表达式写法,整体结构如下所示:

(module

(type ... )

(import ... )

(func ... )

(table ... )

(mem ... )

(global ... )

(export ... )

(start ... )

(elem ... )

(data ... )

)

文本格式是二进制格式的另外一种表现形式,但是对人类更加友好。二进制格式更适合机器(比如编译器)生成和(比如Wasm解释器)理解,文本格式则更适合人类编写和阅读。除了表现形式有明显不同,在结构上,两种格式主要有下面这些不同点:

  • 二进制格式是以段(Section)为单位组织数据,文本格式是以域(Field)为单位组织内容。WAT编译器需要把同类型的域收集起来,合并成二进制段。

  • 在二进制格式中,除了自定义段以外,其他段都最多只能出现一次,且必须按段ID递增顺序出现。文本格式没这个限制,域的顺序没那么严格。不过,导入域必须出现在函数域、表域、内存域和全局域之前。另外文本格式没有自定义域,没办法表达自定义段。

  • 域和段基本上是一一对应的,但是没有单独的代码域,代码域和函数域是合并在一起的。

  • 文本格式提供了多种内联形式,方便编写。例如:

    • 函数域、表域、内存域、全局域可以内联导入或导出域。

    • 表域可以内联元素域。

    • 内存域可以内联数据域。

    • 函数域和导入域可以内联类型域

接下来按照段ID自增的顺序介绍各个域。

类型域(Type Field)

类型域定义函数类型,下面这个例子定义了一个接收两个 i32 类型参数、返回一个 i32 类型值的函数类型:

(module

(type (func (param i32) (param i32) (result i32)))

)

我们可以给函数类型分配一个标识符(Identifier)作为它的名字,这样就可以在其他地方通过名字来引用函数类型,而不必直接通过索引。 moduletypefuncparamresult 等属于WAT语言的关键字。标识符必须以 $ 符开头,后面跟一个或多个数字或字母。完整的标识符词法规则请参考Wasm规范6.3.5小节。另外,函数类型的参数也可以简写在同一个 (param) 里。下面的例子展示了标识符和参数的简写形式:

(module

(type $ft1 (func (param i32 i32) (result i32)))

(type $ft2 (func (param f64)))

)

导入和导出域(Import & Export Field)

Wasm模块可以导入或者导出四种类型的元素:函数、表、内存、全局变量。相应的,导入和导出域也分别有四种写法。下面的例子展示了四种导入域的写法:

(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 ;;)

)

由上面的例子可知,在导入域中,需要指明模块名、元素名、以及导入元素的具体类型。模块名和元素名用字符串表示,需要用双引号 " 包围。导入域也可以像类型域那样,带一个标识符,这样就可以在后面通过名字引用被导入的元素。WAT支持两种类型的注释。以 ;; 开头的单行注释,以及以 (;; 开头,以 ;;) 结尾的跨行注释 。

在上面的例子中,类型域是单独出现的,并在导入函数中通过名字进行引用。这种写法对于很多导入函数共用一个类型是非常友好的。如果某个函数类型只被使用一次,为了方便,也可以把它内联进导入域中,像下面这样:

(module

(import "env" "f1"

(func $f1

(param i32 i32) (result i32) ;; inline type

)

)

)

相比导入域,导出域的写法要简单一些。因为导出域只要指定导出名和具体元素索引即可。导出名在整个模块内必须唯一,这点一定要注意。下面的例子展示了四种导出域的写法:

(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") ... )

)

函数域(Function Field)

函数域声明函数的局部变量,并给出函数的指令。编译器会把函数域拆开,把类型索引放在函数段中,局部变量信息和字节码放在代码段中。下面的例子展示了函数域的写法(指令的写法详见后文):

(module

(type $ft1 (func (param i32 i32) (result i32)))

(func $add (type $ft1)

(local i64 i64)


(local.get 3) (drop)

(i32.add (local.get 0) (local.get 1))

)

)

其实函数的参数也是普通的局部变量,同函数域里声明的局部变量一起构成了函数的局部变量空间,索引从0开始递增。

上面给出的是函数域的精简写法,直接引用了函数类型,并且局部变量写在了同一个 (local) 里。我们可以把函数类型内联进函数域并把 (param) 拆成多个,这样就可以给参数起名字。同理,可以把 (local) 拆成多个,这样就可以给局部变量起名字。给参数和局部变量起了名字,就可以在变量指令中通过名字而非索引来定位参数或局部变量,这样有助于提高代码的可读性。我们把上面的例子改写一下,内联类型,并给参数和局部变量分配标识符,如下所示:

(module

(func $f1 (param $a i32) (param $b i32) (result i32)

(local $c i64) (local $d i64)


(local.get $c) (drop)

(i32.add (local.get $a) (local.get $b))

)

)

表和元素域(Table & Element Field)

由于Wasm1.0规范规定模块最多只能有一个表,所以表域最多只能出现一次。元素域可以出现多次,里面可以指定多个函数索引,以及第一个函数索引对应的表索引。下面的例子展示了表和元素域的写法:

(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

)

)

内存和数据域(Memory & Data Field)

和表类似,由于Wasm1.0规范规定模块最多只能有一块内存,所以内存域也是最多只能出现一次。数据域可以出现多次,里面需要用常量指令指定起始内存偏移量(地址),并用字符串指定内存初始值。下面的例子展示了内存和数据域的写法:

(module

(memory 4 16)

(data (offset (i32.const 100)) "Hello, ")

(data (offset (i32.const 108)) "World!\n")

)

内存域中也可以内联一个数据域,但是使用这种形式无法指定内存的页数限制,只能由编译器根据内联数据进行推测。也无法指定内存的起始地址,只能从0开始。另外,初始数据可以写成多个字符串。下面的例子展示了数据域的内联写法:

(module

(memory ;; min: 1, max: 1

(data "Hello, " "World!\n") ;; inline data

)

)

使用转义字符可以很方便的在字符串中嵌入回车换行等特殊符号、十六进制编码的字节、以及Unicode代码点。具体请参考Wasm规范6.3.3小节。

全局域(Global Field)

在全局域中可以指定全局变量的标识符、类型、可变性、以及初始值。下面的例子展示了全局段的写法:

(module

(global $g1 (mut i32) (i32.const 100)) ;; mutable

(global $g2 (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)

)

)

起始域(Start Field)

起始域最为简单,用于指定起始函数索引。下面的例子展示了起始域的写法:

(module

(func $main ... )

(start $main)

)

前面介绍了WAT的整体结构和各种域的写法,下面介绍各种指令的写法。

指令普通形式(Plain Instruction)

指令的普通形式非常直白,对于大部分指令来说,就是操作码后跟立即数。下面的例子展示了除控制指令外其他指令的一般写法:

(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

)

)

可以看到,大部分指令的立即数参数都是不能省略的,以数值或者名字的形式跟在操作码后面。内存读写系列指令是个例外, offsetalign 这两个立即数参数都是可选的,且需要明确指定(数值跟在等号后面)。

blockloopif 这三条结构化控制指令,可以指定可选的结果类型,必须以 end 结尾。 if 指令还可以用 else 分割成两条分支。下面的例子展示了 blockloopifbrbr_if 等控制指令的一般写法:

(module

(func $foo

block $l1 (result i32)

i32.const 123

br $l1

loop $l2

i32.const 123

br_if $l2

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

)

)

br_table 指令的写法和 br 指令差不多,下面是一个例子:

(module

(func

block

block

block

i32.const 3

br_table 0 1 2 0

end

end

end

)

)

指令折叠形式(Folded Instruction)

除了上面介绍的普通形式,指令还可以写成更为精简的折叠形式。可以对普通指令做三步调整,让它变为折叠形式。第一步,给指令加上圆括号。第二步,如果是 blockloopif 指令,把 end 去掉。 if 指令要稍微复杂一些,具体请看下面的例子。第三步(这一步是可选的),如果某条指令(无论是普通还是折叠形式)和它前面的几条指令从逻辑上可以看成一组操作,则可以把前几条指令折叠进该指令。比如说 local.get $alocal.get $bi32.add 这三条指令,逻辑上是一组操作,进行加法计算。那么可以把这三条指令折叠起来,写成 (i32.add (local.get $a) (local.get $b))

折叠指令实际上表达了一颗指令树,WAT编译器会按照后续遍历(从左到右遍历子树,最后根节点)的方式展开折叠指令。我们按照上面的三个步骤改写前面那个包含 foo()max() 函数的例子,改写后的代码应该是下面这样



(module

(func $foo

(block $l1 (result i32)

(i32.const 123)

(br $l1)

(loop $l2

(br_if $l2 (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))

)

)

)

可以看到,代码的确是好看了不少。为了加深对折叠指令的理解,让我们把 max() 函数的 if 指令展开一层,把 i32.gt_s 指令提出来,改写成下面的等价形式:

(module

(func $max (param $a i32) (param $b i32) (result i32)

(i32.gt_s (local.get $a) (local.get $b))

(if $l (result i32)

(then (local.get $a))

(else (local.get $b))

)

)

)

我们可以继续展开 i32.gt_s 指令,把 local.get 指令提出来,改写成下面的等价形式:

(module

(func $max (param $a i32) (param $b i32) (result i32)

(local.get $a) (local.get $b) (i32.gt_s)

(if $l (result i32)

(then (local.get $a))

(else (local.get $b))

)

)

)

到此,WAT的基本语法就都介绍完毕了。

*本文由CoinEx Chain开发团队成员Chase撰写。CoinEx Chain是全球首条基于Tendermint共识协议和Cosmos SDK开发的DEX专用公链,借助IBC来实现DEX公链、智能合约链、隐私链三条链合一的方式去解决可扩展性(Scalability)、去中心化(Decentralization)、安全性(security)区块链不可能三角的问题,能够高性能的支持数字资产的交易以及基于智能合约的Defi应用。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Approximation Algorithms

Approximation Algorithms

Vijay V. Vazirani / Springer / 2001-07-02 / USD 54.95

'This book covers the dominant theoretical approaches to the approximate solution of hard combinatorial optimization and enumeration problems. It contains elegant combinatorial theory, useful and inte......一起来看看 《Approximation Algorithms》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

随机密码生成器
随机密码生成器

多种字符组合密码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具