WebAssembly-提高三:理解文本格式

684 阅读5分钟

导读

在前面几章中我们初步了解了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 参数编号来获取:
    (func (param i32) (param f32) (param f64)
      get_local 0
      get_local 1
      get_local 2)
    
    其中的param类型可以有以下类型
    • 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目录

image.png

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 image.png

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相关内容。