現状の WebAssembly の仕様では JavaScript の関数は Rust へと直接渡せないということなので、効率は悪いですが一つの回避策を考えてみました。
仲介役を JavaScript 側に立たせ、 JavaScript 側ではほぼ任意なインターフェース、 Rust 側では固定的なインターフェースにします。
現状では実用的ではないレベルですが、どうしても避けられない場合はこうしてみるのがいいかもしれません。
もっと使いやすいやり方ができるようになるといいのですが。。。
サンプルプロジェクトはこちら
作るもの
基本例としての単純なメソッド withClosureExample
と、応用例としてのメソッド fold
を作ります。
前者はとても簡単で、 withClosureExample((a1, a2) => a1 + a2, 2, 3)
と渡すと 2 + 3 = 5
に、 withClosureExample((a1, a2) => a1 * a2, 2, 3)
と渡すと 2 * 3 = 6
となるものです。
後者は内部で持つデータに対して畳み込みするものです。例えば、 [2, 3, 5]
の状態で fold((l, r) => l + r, 0)
と渡すと (((0 + 2) + 3) + 5) = 10
に、 fold((l, r) => l * r, 1)
と渡すと (((1 * 2) * 3) * 5) = 30
となるものです。
サンプルでは、 wasm の出力のラップする JS “ラッパーライブラリ” において call_closure
という仲介役を設けることで、クロージャ渡しを擬似的に再現します。
WASM 実装 - lib.rs
Rust で WebAssembly を出力する を参考にしつつ、 extern
と #[no_mangle]
を駆使して実装します。
call_closure
については、引数を変換が必要ない i32
と f64
に固定し、その数を 4 に固定します。
1 | extern "C" { |
こうすると、 JS からは with_closure_examle
で呼べるようになります。ただし、後述しますが call_closure
の実態は指定して呼ぶ必要があります。
応用例については lib.rs を参照してください。
init_data
は内部データ用の配列の初期化、 fold
が上述した関数、 fold_as_sum_in_rust
はベンチマーク用にすべてを Rust で実装した関数です。
ラッパー実装 - lib.js
JS から wasm 関数を利用するには、下のコードのハイライト部分の処理を行い、 results.instance.exports.???
という呼び出しを行います。
ラッパーとしては、まずパラメータを渡し、さらにこれを利用するように包んだ関数を export します。( wasm を利用すると形はどうであれ非同期処理になります)
1 | const closureDict = {}; |
一般的な wasm 利用の重要な点は、 3 行目から 5 行目と 19 行目です。
wasm 内で extern
指定したものは、こうして実装を入れてやらないと実行時にエラーが出てきます。
このサンプルでは、 call_closure
という仲介役を設けると説明しました。本当に簡単な戦略ですが、呼び出し時にクロージャをストアに登録し、 wasm から call_closure
を介して呼んでいます。
その他の点には面白みはあまりありません。
ラッパーの利用側 - index.html
最後に、ラッパーの実行例を示します。
8 | <script type="module"> |
lib.withClosureExample
には複数の種類のクロージャを渡せています。
lib.fold
に関しては、クロージャ利用 “wasm-with-js”、全て JS 利用 “js”、全て wasm 利用 “wasm” の 3 つの動作速度を比較します。
初期化時間やデータの移動処理は時間に含めないよう書きました。
実行速度
wasm を使うからには速度が気になるということでやってみましたが、こんな感じになりました。
計算方法 | 実行結果 | Chrome 68 64bit 速度 | Firefox 61 64bit 速度 |
---|---|---|---|
wasm-with-js | 500000500000 | 121.224853515625ms | 98ms |
js | 500000500000 | 17.569091796875ms | 5ms |
wasm | 500000500000 | 75.505126953125ms | 37ms |
“wasm-with-js” v.s. “js” が JIT によりこのような差になるのは不思議ではないですが、 “js” v.s. “wasm” がこうなるのはちょっと想定外でした。
今回は計算対象が足し算の連続で JIT が効きやすかったからかもしれません。シミュレーションのような複雑な計算をするならば “wasm” が優勢になってくるかもしれません。
まとめ
- wasm の提供する関数へクロージャを渡したい場合の素朴な方法を紹介しました。
- その場合の実行速度を比較しました。