kanejaku.org

WebAssemblyのanyref

Publish date 25 May 2019 Last update 2 Jul 2019

WebAssembly にreference typesがあるとどううれしいのか、を理解しようとして書いたメモ。参照型として二つ提案されているが、ここではanyrefに着目する。

(注: Reference types は提案段階なので各種ブラウザではまだデフォルトで有効にはなっていない。Chrome で試すにはコマンドライン引数に--js-flags=--experimental-wasm-anyrefをつける必要がある。)

任意の JS の値を受け取って、それをそのまま返すidentity()という関数を wasm で作ろう。とってつけた例題だけど骨子を最小限のコードで示すために許してほしい。こういうやつ。

identity(value) === value; // => true

anyrefが存在しない現在の仕様だとこれは wasm だけでは定義できない。なぜなら wasm の関数は引数/戻り値に整数型か浮動小数点型しか指定できないから。JS には数値以外にも文字列型やオブジェクト型がある。文字列やオブジェクトを数値に変換する仕組みが JS 側に必要だ。

任意の JS の値を数値に変換する単純な方法としてぱっと思いつくのは、グローバルな配列をひとつ用意して、そこを値の格納先として使うやり方だろう。JS/wasm 間での値の受け渡しはこの配列のインデックスを使う。要はヒープみたいなもの。必要最小限の実装はこんな感じ。

let heap = [];
function toWasmValue(value) {
  heap.push(value);
  return heap.length - 1;
}
function fromWasmValue(index) {
  return heap[index];
}

Wasm の関数を呼ぶときの流れは、toWasmValue()でインデックスを取得、wasm で定義された関数の呼び出し、戻り値をfromWasmValue()で JS の値に戻す、という感じになる。

(async function() {
  const stream = fetch("identity.wasm");
  const { instance } = await WebAssembly.instantiateStreaming(stream);

  // Wrapper function of `identity`
  function identity(value) {
    const index = toWasmValue(value);
    const ret = instance.exports.identity(index);
    return fromWasmValue(ret);
  }

  console.log(identity(42)); // => 42
  console.log(identity(window)); // => window
})();

Wasm 側のコードはただ受け取ったインデックスを返すだけ。

(module
  (func (export "identity") (param i32) (result i32)
  ;; Just return the argument
  get_local 0))

これでidentity()を定義できたが、値を単純に JS → wasm → JS と引き回したいだけなのになんだか面倒だ。しかも今の実装は実用に耐えない。toWasmValue()を呼ぶたびにヒープが大きくなるし、オブジェクトへの参照が消えないのでリークが起きる。きちんと使えるものを作ろうと思うと、不要になった値をヒープから削除しなければならないし、必要に応じてフラグメンテーションも解消しないといけない。

要はメモリ管理が必要になるのだけど、GC がある JS の上にメモリ管理機構を作るのは冗長だと思える。wasm 側で値を操作しないなら(単純に JS に引き渡すだけなら)、その値への参照を wasm に渡すのを許可してもいいのではないか。そうすれば値の生存期間の管理は JS 側の GC に任せられるし、値の変換も不要になる。

…こんな感じでanyrefが提案されたんだろう、と自分は理解した。

実際にanyrefを使って書き直すと以下のようにだいぶすっきりする。JS 上にヒープを作る必要はない。JS 側の GC が参照の生存管理をする。

(async function() {
  const stream = fetch("identity_anyref.wasm");
  const { instance } = await WebAssembly.instantiateStreaming(stream);

  console.log(instance.exports.identity(42)); // => 42
  console.log(instance.exports.identity(window)); // => window
})();

Wasm 側のコードは引数と戻り値の型を変えるだけ。wat2wasmで以下をコンパイルするときは--enable-reference-typesフラグをつける。

(module
  (func (export "identity") (param anyref) (result anyref)
  ;; Just return the argument
  get_local 0))

anyrefが解きたい課題は分かった。では実際の問題に対してanyrefはどううれしいのか。多分ほとんどの開発者には特段メリットはないんじゃないか。というのも、こういう低レイヤの変換やメモリ管理はすでに Emscripten や wasm-bindgen が面倒を見てくれているから。これらのフレームワーク自体にとってはanyrefがあるとラッパー関数なんかを削減できてうれしいと思う。開発者にとってもランタイムのサイズが減って間接的にうれしいかもしれない。

提案の概要にはここで説明した以外の動機も書いてある。むしろ主眼はそちらかもしれなくて、reference types はほかの提案の土台としての側面が強そう。

補記

anyrefWebAssembly.Globalの型やWebAssembly.Tableの要素型としても指定できる。

参考