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应用。


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

查看所有标签

猜你喜欢:

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

软件随想录

软件随想录

Joel Spolsky / 阮一峰 / 人民邮电出版社 / 2009 / 49.00元

《软件随想录:程序员部落酋长Joel谈软件》是一部关于软件技术、人才、创业和企业管理的随想文集,作者以诙谐幽默的笔触将自己在软件行业的亲身感悟娓娓道来,观点新颖独特,内容简洁实用。全书分为 36讲,每一讲都是一个独立的专题。 《软件随想录:程序员部落酋长Joel谈软件》从不同侧面满足了软件开发人员、设计人员、管理人员及从事软件相关工作的人员的学习与工作需要。一起来看看 《软件随想录》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

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

在线 XML 格式化压缩工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试