お手製wasmモジュールから別のwasmモジュールの関数を呼び出す
前提環境
- node ver.12以上
- WebAssemblyをnode.js上で動かすために必要な大まかな知識
動機
自作のコンパイラの吐き出すwasmから,rustで生成したwasmバイナリからexportされている,メモリを操作する関数を呼び出したい.
要約
-
instantiate(wasm [, obj])
: 指定したwasmファイルを読み込み,インスタンス化して返す関数とします.objはオプショナル引数です.[1] -
from_rust.wasm
: rustから生成したwasmバイナリとします -
local.wasm
: 手元で作成したwasmバイナリとします -
exported_fn()
: rustで作成した関数名とします
const from_rust = await instantiate('from_wasm');
const importObj = {
from_rust: WebAssembly.Module.exports(from_rust)
};
const res = await instantiate('local.wasm', importObj)
res.exports.exported_fn()
Top Level awaitを使いたいがためにESM形式を利用しています.適宜Promise
の扱いは改変してください.
既存手法
共有オブジェクトの利用
wasmは複数のインスタンスを同時に建てて,それぞれのインスタンスからエクスポートされている関数をJavaScriptから利用することができます.
const res1 = await instantiate("from_rust.wasm");
const res2 = await instantiate("local.wasm");
//call function exported from each instance
res1.exports.exported_fn();
res2.exports.exported_fn();
各wasmインスタンスは専用のメモリとテーブルを持っており,各wasmインスタンスは自身のもの以外のメモリとテーブルに対してアクセスすることは現状できません.しかし,wasmモジュールをインスタンス化する際にJavaScript側から共有オブジェクトを渡すことで複数のwasmインスタンスが同一のメモリとテーブルにアクセスができるようになります.
+ const mem = {
+ memory: new WebAssembly.Memory({initial: 1})
};
- const res1 = await instantiate("from_rust.wasm");
+ const res1 = await instantiate("from_rust.wasm", mem);
- const res2 = await instantiate("local.wasm");
+ const res2 = await instantiate("local.wasm", mem);
//res1 and res2 can access the same memory
res1.exports.exported_fn();
res2.exports.exported_fn();
(module
(import "mem" "memory" (memory 1))
...
)
(module
(import "mem" "memory" (memory 1))
...
)
見やすさのためテキスト形式にしています.
問題点
- rustから生成したwasmを編集する必要がある
操作したいメモリを共用かつ指定することができるので目的は達成できそうですが,rustから生成したwasmバイナリを編集して共有メモリをインポートするようにする手間が発生します.これではrustファイルを編集して再度コンパイルするたびに生成されたwasmを編集しなければならず,面倒です. -
local.wasm
からfrom_rust.wasm
上の関数を直接呼び出せない
これはwasmの設計理念に関わるので従うほかありません.
テーブルを利用する
テーブルを用いることで複数のwasmインスタンス間で関数を共有することができます.これにより,別インスタンスの関数を呼べない問題を解決できます.以下のMDNの記事に例が記載されています.
以下のコードは記事からの引用です.shared0.wat(module (import "js" "memory" (memory 1)) (import "js" "table" (table 1 anyfunc)) (elem (i32.const 0) $shared0func) (func $shared0func (result i32) i32.const 0 i32.load) )
shared1.wat(module (import "js" "memory" (memory 1)) (import "js" "table" (table 1 anyfunc)) (type $void_to_i32 (func (result i32))) (func (export "doIt") (result i32) i32.const 0 i32.const 42 i32.store ;; store 42 at address 0 i32.const 0 call_indirect (type $void_to_i32)) )
var importObj = { js: { memory : new WebAssembly.Memory({ initial: 1 }), table : new WebAssembly.Table({ initial: 1, element: "anyfunc" }) } }; Promise.all([ fetchAndInstantiate('shared0.wasm', importObj), fetchAndInstantiate('shared1.wasm', importObj) ]).then(function(results) { console.log(results[1].exports.doIt()); // prints 42 });
問題点
- rustから生成したwasmを編集する必要がある
この手法では先ほどと同様に開発者側が関数をテーブルに登録するコードを直接書き加える必要があります.また,呼び出す際には対象の関数がテーブルの何番目に有るかを把握する必要があります.管理対象が増えるのはあまり好ましくありません.
wasmモジュール→jsオブジェクト
ここで一つ思いつくのがWASMモジュールからJSオブジェクトに変換できれば,import objectとして他のwasmモジュールとリンクできるのではないかということです.そして,それが今回行う方法です.JavaScriptのWebAssembly APIには以下の関数があります.
-
WebAssembly.Module.exports()
: wasmモジュールのうちexports
宣言がついているものを全て取得します.
これを利用し,更にJavaScriptのオブジェクトで名前空間を付与することで,元のwasmファイルを一切いじることなく関数を他のwasmモジュールで利用できるようになります.この方法のもう一つの利点は,関数を呼び出す際に関数名指定で呼び出せることです.
const from_rust = await instantiate('from_rust.wasm');
const obj = {
from_rust: WebAssembly.Module.exports(from_rust)
};
instantiateFromWat("local.wat", obj);
(module
(type (;0;) (func (param i32 i32) (result i32)))
(func (;0;) (type 0) (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
(memory (;0;) 17)
(export "memory" (memory 0))
(export "add" (func 0)))
(module
(import "from_rust" "add" (func $add (param i32 i32) (result i32)))
(func (export "load") (result i32)
i32.const 1
i32.const 1
call $add
))
instantiateFromWat()
はテキスト形式のWebAssemblyモジュールからwasmインスタンスを生成する関数と考えてください.
これでrustから生成されたwasmを一切編集することなく目的を達成できました.
Proposal
現在複数のwasmバイナリを動的にリンクさせる提案がされています.5段階中の2段階目(Phase 1: Feature Proposal)の段階なので,実用的になるのはだいぶ先でしょう.
-
ブラウザのWebAssembly APIには
instantiateStreaming()
というものがあります.それと同じようなものと考えてください. ↩︎
Discussion