X

曜彤.手记

随记,关于互联网技术、产品与创业

  1. externref
  2. funcref
  3. Recap: Table
  4. ref.null 与 ref.is_null

WebAssembly - Reference Types

今天来看的提案是 - “Reference Types”,GitHub 链接在这里。从标准上看,该提案主要新增了一种类型 externref,可用于在 Wasm 模块内引用宿主环境中的“不透明(opaque)”值类型,该类型既可作为普通的值类型,也可作为 WebAssembly Table(后简称 “table”)的元素类型。除此之外,提案还补充了用于操作 table 的更多指令,同时支持了多重 table。

externref

externref 可用于在 Wasm 上下文中引用宿主环境内的“不透明”值类型,这里提到的“不透明”,你可以简单理解为该类型的值仅能够由宿主环境来正确解释,Wasm 运行时只保持对该值的引用,但不做实际解释。借助这种方式,我们第一个能够想到的在 MVP 标准下无法实现的场景,便是通过 externref 来引用宿主环境中的 DOM 元素。这样我们便能够间接地在 Wasm 中“操作”(实际上只是引用)DOM 元素了。比如下面这个例子:

(module
  (import "env" "createSpan" (func $createSpan (result externref)))
  (import "env" "setInnerNumber" (func $setInnerNumber (param externref i32) (result externref)))
  (func (export "spanFactory") (param $param i32) (result externref)
    (call $createSpan)
    (local.get $param)
    (call $setInnerNumber)
  )
) 

由于 Wasm 环境无法对来自宿主的不透明值直接进行解释,因此对这类值的所有操作都需要宿主环境间接完成。在这个例子中,Wasm 模块的导出函数 “spanFactory” 承担了工厂函数的职责,它接收给定的一个 i32 参数,然后在导入的宿主函数 “createSpan” 和 “setInnerNumber” 的帮助下,它可以创建一个 span 类型的 DOM 元素,并将其内联文本设置为给定的 i32 值,最后再将该元素返回。在上述 WAT 代码中可以看到,所有需要 DOM 元素传入传出的地方,都被标记为了 externref 类型。

const createSpan = () => {
  return document.createElement('span');
};
const setInnerNumber = (el, val) => {
  el.innerHTML = val;
  return el;
}

WebAssembly.instantiate(wasmModule, {
  env: {
    setInnerNumber, createSpan
  }
}).then((instance) => {  
  const { spanFactory } = instance.exports;
  document.body.appendChild(spanFactory(100)) 
}); 

Wasm 模块在实例化时需要由宿主环境提供 JavaScript 版本的函数 “createSpan” 和 “setInnerNumber”。使用方法参考上面给出的 JavaScript 代码,这里不再赘述。根据提案描述,所有 JavaScript 环境下的值都可以使用 externref 来引用。

Any JS value can be passed as externref to a Wasm function, stored in a global, or in a table.

funcref

除了新增加的 externref 类型,提案还放宽了标准中原有的 funcref 类型的使用场景。funcref 最开始仅用于标记 table 的元素类型,而该提案使得它也可以被作为正常的值类型使用(可用于 local,global,函数参数和返回值)。为了支持这一特性,提案又补全了相关的指令和使用方式。比如:

(module
  (import "env" "foo" (func $foo (param i32)))
  (table $table 10 funcref)
  (global funcref (ref.func $foo))  ;; Define a global of type "funcref".
  (func $indrectCall
    (table.set $table (i32.const 0) (global.get 0))    
    (table.size $table)  ;; Get table size.
    (call_indirect $table (param i32) (i32.const 0))  ;; Call into table.
  )
  (start $indrectCall)
)

在这段 WAT 代码中,你可以看到一些新指令,比如 table.settable.sizeref.func。这些指令都是为了在 Wasm 代码中更方便地使用 funcrefexternref 类型而加入的。table 相关指令可用于获得指定 table 相关的信息,以及操作其内部的元素数据(如填充、拷贝等)。ref.func 用于获取某个 Wasm 函数的引用,需要注意的是本提案并不支持直接从宿主环境导入函数引用,比如直接用宿主环境的函数作为 (func (export “foo”) (param funcref))) 的参数。而所有函数引用都需要通过 ref.func 指令来获取。

WebAssembly.instantiate(wasmModule, { 
  env: { 
    foo: e => {
      console.log(e);  // 10.
    }
  }
});                 

对应的 JavaScript 代码如上所示。

Recap: Table

funcref 只能通过 table 间接调用这种方式实际上是为了保持与 MVP 标准行为的一致。如果你还记得,Wasm 最初引入 table 是为了支持高级语言中的函数指针。比如下面这段 C 代码:

int foo(int (*fn_ptr)(int, int)) {
  return fn_ptr(1, 2);
}

在编译后通常会得到如下所示的 WAT。这里原 C 代码中函数指针的调用过程变成了通过 “table slot” 的间接调用过程。而 funcref 的行为则保持了同 anyfunc 类型完全一样的特点。在本提案提出之前,这两者实际上是一个东西,而 anyfunc 类型目前已被废弃,不再使用。

(table 3 3 anyfunc)
(elem (i32.const 0) $__wasm_nullptr $add $minus)
;; ...
(func $foo (; 2 ;) (param $0 i32) (result i32)
  (call_indirect (type $FUNCSIG$iii)
   (i32.const 1)
   (i32.const 2)
   (get_local $0)  ;; Dynamic dispatch through table index.
  )
 )

ref.null 与 ref.is_null

最后来看另外两个简单的指令 ref.nullref.is_null。其中前者用来表示值为 null 的引用,而后者则用来检测给定的引用值是否为 null。看下面这个例子:

(module
  (func $bar)
  (global externref (ref.null extern))
  (global funcref (ref.func $bar))
  (func (export "foo") (result i32 i32)
    (ref.is_null (global.get 0))
    (ref.is_null (global.get 1))
  )
)

JavaScript 代码如下。

WebAssembly.instantiate(wasmModule).then((instance) => {  
  const { foo } = instance.exports; 
  console.log(foo());  // "[1, 0]".
});    

“Reference Types” 提案为 Wasm GC 打下了很好的基础,基于引用计数的 GC 实现完全依赖于堆对象引用的正确实现。




评论 | Comments


Loading ...