WebAssembly 文本格式

391 阅读4分钟

WebAssembly 系列

WebAssembly 基本介绍

WebAssembly 对象

WebAssembly 文本格式

WebAssembly 文本格式

WebAssembly 本身是一种二进制格式,但是为了方便开发者阅读和调试,提供了相应的文本表示。可以认为这是一种对底层字节码的解释,有点类似于汇编语言。

S-表达式

WebAssembly代码中的基本单元是一个模块。在文本格式中,一个模块被表示为一个大的S-表达式。我们可以把一个模块想象为一棵由描述了模块结构和代码的节点组成的树。树上的每个一个节点都有一对括号包围。括号内的第一个标签告诉你该节点的类型,其后跟随的是由空格分隔的属性或孩子节点列表。

(module (memory 1) (func))

以上就是一条S-表达式,表示一棵根节点为“模块(module)”的树,该树有两个孩子节点,分别是属性为1的内存节点和一个函数节点。

对于函数,其表示方式如下:

( func <signature> <locals> <body> )

函数组成:

  • 签名 声明函数需要的参数以及函数的返回值。
  • 局部变量 像JavaScript中的变量,但是显式的声明了类型。
  • 函数体 是一个低级指令的线性列表。

函数变量:

  • 参数格式为: (param <类型>)
  • 返回值格式为: (result <类型>)
  • 局部变量格式为: (local <类型>)

变量类型:

  • i32:32位整数
  • i64:64位整数
  • f32:32位浮点数
  • f64:64位浮点数

获取和设置局部变量:

  • get_local
  • set_local

我们直接来看一个例子:

(func (param i32) (param f32) (local f64)
  get_local 0
  get_local 1
  get_local 2)

上面定义了一个函数,这个函数接收两个参数,类型分别是i32和f32,这个函数内部声明了一个局部变量,类型为f64。get_local/set_local 指令使用数字索引来指向将被存取的值。因此:

  • get_local 0会得到i32类型的参数
  • get_local 1会得到f32类型的参数
  • get_local 2会得到f64类型的局部变量

由于使用数字索引来指向某个条目容易让人混淆,因此,也可以通过别名的方式来访问它们,方法就是在类型声明的前面添加一个使用美元符号($)作为前缀的名字。例如:

(func (param $p1 i32) (param $p2 f32) (local $loc i32) …)

这里,使用get_local $p1 就代替 get_local 0,访问参数i32变量时,就可以通过 $p1 进行访问。

模块实例

以下就是一个模块,模块中定义了一个函数,函数接收两个参数,类型都是 i32,函数的功能是把这康哥参数相加,然后返回一个类型为 i32 的结果。

(module
  (func (param $lhs i32) (param $rhs i32) (result i32)
    get_local $lhs
    get_local $rhs
    i32.add))

正如在一个ES2015模块里面一样,wasm 函数必须通过模块里面的 export 语句显式地导出。另外,上面的函数是一个匿名函数,为了方便应用,我们可以给它取个名称,方法和取变量名一样(美元符号开头)。最终,变成如下:

(module
  (func $add (param $lhs i32) (param $rhs i32) (result i32)
    get_local $lhs
    get_local $rhs
    i32.add)
  (export "add" (func $add)) ;;两个逗号后面是注释
)

上面的 add 是JavaScript 中用来区别这个函数的名字。

导入函数

我们已经见过 JavaScript 调用 WebAssembly 函数,但是 WebAssembly 如何调用 JavaScript 函数呢?事实上,WebAssembly 对 JavaScript 没有任何了解,但是,它有一个可以导入 JavaScript 或 wasm 函数的通用方法。让我们看一个例子:

(module
  (import "console" "log" (func $log (param i32)))
  (func (export "logIt")
    i32.const 13
    call $log))

WebAssembly 使用了两级命名空间,所以,这里的导入语句是说我们要求从 console 模块导入 log 函数。另外,你可以看到在 logIt 函数中,通过call指令调用了 JavaScrpit 导入的函数 log。

导入的函数就像普通函数一样:它们拥有一个 WebAssembly 验证机制,会静态检查的签名,可以被设置一个索引,能够被命名和被调用。

JavaScript 函数没有签名的概念,因此,无论导入的声明签名是什么,任何JavaScript函数都可以被传递过来。一旦一个模块声明了一个导入, WebAssembly.instantiate() 的调用者必须传递一个拥有相应属性的导入对象。

就上面而言,我们需要一个(让我们称之为importObject的)对象,并且importObject.console.log是一个JavaScript函数。

使用起来像下面这样:

var importObject = {
  console: {
    log: function(arg) {
      console.log(arg);
    }
  }
};

fetchAndInstantiate('logger.wasm', importObject).then(function(instance) {
  instance.exports.logIt();
});

让我们回顾一下上一节提到的 WebAssembly.instantiate 函数,它接收的第二个参数是可选的。也就是当 wasm 不需要导入函数的时候。如果 wasm 模块需要导入函数或者内存,那么就必须传入 importObject。属性要和导入对象一样。