导读
在前面几章中我们初步了解了WebAssembly的工作流程,本章中我们来讲一下WebAssembly中文本格式的概念,本文章适用于以下读者:
- 对WebAssembly如何与底层字节码进行交互的比较感兴趣
- 想编写wasm模块来优化JavaScript性能
- 想自己构建自己的WebAssembly编辑器 如果你只是想在web中加载wasm模块然后使用的话,这个文章可以跳过
文本格式
为什么要有文本格式?
在官方文档里面有句话可以大致描述:
为了能够让人类阅读和编辑WebAssembly,wasm二进制格式提供了相应的文本表示。这是一种用来在文本编辑器、浏览器开发者工具等工具中显示的中间形式。本文用基本语法的方式解释了这种文本表示是如何工作的,以及它是如何与它表示的底层字节码,及在JavaScript中表示wasm的封装对象关联起来的。本质上,这种文本形式更类似于处理器的汇编指令。
也就是说文本格式解决了以下几个问题
- 直接用文本编辑器就可以编写wasm
- 可以在浏览器中调试wasm(后续文章会介绍)
- 可以清楚的看到wasm模块如何工作的
文本格式介绍
让我们先来看下一个最简单文本格式样例1-1:
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
(export "add" (func $add))
)
通过上面代码可以看到这类文本格式很类似汇编,这种格式叫S-表达式
,S-表达式
是一个非常古老和非常简单的用来表示树的文本格式,树上的每个一个节点都有一对括号——( ... )——包围。括号内的第一个标签告诉你该节点的类型,其后跟随的是由空格分隔的属性或孩子节点列表。
比如
(module (memory 1) (func))
表示一棵根节点为模块(module)
的树,有两个子节点,一个内存(memory)
和一个函数(fun)
节点,这是一个最基本的S-表达式的样子,WebAssembly中的文本格式就是以此类语法构造出来的。
func介绍
在上面样例1-1中我们可以看到有func
这个关键字,func表示函数,WebAssembly模块中的所有代码都是划分到函数里面,他的表达形式如下
( func <signature> <locals> <body> )
- 签名
<signature>
:用来声明函数的参数和返回值 - 局部变量
<local>
:用来声明一些会用到的局部变量 - 函数主体
<body>
:用来描述函数过程,是一个指令的线性列表
参数param
和局部变量local
- 参数:
param,形如
(param <类型>)
,可以有多个,在函数体内可以用get_local 参数编号
来获取:
其中的param类型可以有以下类型(func (param i32) (param f32) (param f64) get_local 0 get_local 1 get_local 2)
- i32:32位整数
- i64:64位整数
- f32:32位浮点数
- f64:64位浮点数
另外为了更方便的获取函数参数,可以使用
param $别名 <类型>
的格式给参数指定一个别名
(func (param $p1 i32) (param $p2 f32) (param $p3 f64) get_local $p1 get_local $p2 get_local $p3)
- 局部变量:local,形式跟param一致,基本语法也一致
返回值
形式为(return <类型>)
,目前只支持一个返回值
函数体
下面来看我们之前示例中的这三行
local.get $lhs
local.get $rhs
i32.add
这就是最基础的函数体,这里意思是拿到参数1和参数2,然后调用操作码add
相加,然后返回。更多的操作码可以看这里操作码。
函数调用
为了方便调用函数,也可以采用$名称
的形式给函数起一个别名func $add (param $lhs i32)...
,然后我们再用export关键字把这个函数导出(export "add" (func $add))
这样就可以在JavaScript侧正常使用add函数了。
至此,一个基本的用文本格式写的add函数就写完了,接下来将要介绍一下如何使用文本格式
wat和wasm
WebAssembly的文本格式文件后缀是.wat
,他和.wasm
可以互相转换,这里介绍下如何进行转换
环境准备
首先新建一个.wat文件,用任意的文本编辑将如下文本复制到文件内并保存为add.wat
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
(export "add" (func $add))
)
然后以mac为例,安装转换工具wabt
。其他环境安装可以参考github.com/webassembly…
$ git clone --recursive https://github.com/WebAssembly/wabt
$ cd wabt
$ git submodule update --init
$ mkdir build
$ cd build
$ cmake ..
$ cmake --build .
注意点:
1、一定要创建一个build目录再cmake .. 否则会导致运行目录冲突
2、需机器安装cmake环境
接下来使用命令将刚刚编写的.wat
文件转换为.wasm
$ bin/wat2wasm .add.wat -o add.wasm
如果要反过来的话可以用命令bin/wasm2wat
使用
在上一章 WebAssembly-实践二:在React使用C/C++函数中我们创建了一个react工程,我们现在基于这个项目进行修改
1、复制add.wasm到public目录
2、修改index.html文件,在<body>
和</body>
增加如下代码,稍后将会介绍这个代码的作用
<script>
fetchAndInstantiate('add.wasm').then(function (instance) {
console.log(instance.exports.add(1, 2));
});
function fetchAndInstantiate(url) {
return fetch(url).then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes)
).then(results =>
results.instance
);
}
</script>
3、运行 yarn start
在命令行中就可以看到我们输出的结果3
WebAssembly.instantiate
WebAssembly.instantiate是用来编译和实例化wasm模块的方法,通过fetch我们获取到wasm文件二进制数据后,调用WebAssembly.instantiate来实例化获取instance。然后通过instace就可以调用wasm中声明的函数。
另外官方目前有WebAssembly.instantiateStreaming()方法可以去除fetch再转换arrayBuffer的动作
WebAssembly.instantiate有两个重载方式
-
二级制代码方式
Promise
<ResultObject>
WebAssembly.instantiate(bufferSource, importObject);参数介绍:
- bufferSource 一个包含你想编译的wasm模块二进制代码的array数组或者ArrayBuffer
- importObject 可选参数,一个将被导入到新创建实例中的对象 具体例子可以参考上面的代码
-
模块对象方式
Promise<WebAssembly.Instance> WebAssembly.instantiate(module, importObject); 参数介绍:
- module 将被实例化的module对象
- importObject 可选参数,一个将被导入到新创建实例中的对象 例子:
var importObject = { imports: { imported_func: function(arg) { console.log(arg); } } }; fetch('simple.wasm').then(response => response.arrayBuffer() ).then(bytes => WebAssembly.compile(bytes) ).then(mod => WebAssembly.instantiate(mod, importObject).then(function(instance) { instance.exports.exported_func(); }); );
可以看到这个中间会多一步compile,具体关于
WebAssembly.Module
等概念后面文章会逐步为大家介绍
结束
至此一个对文本格式的基本理解就到这里,更多细节大家可以查看MDN文档 文本格式。
关注我后面将会为大家介绍更多wasm相关内容。