TL;DR;
- コード中に JavaScript を埋め込む
- 外部関数として定義して、実装を WASM 出力時に埋め込む
このどちらかで、JavaScriptのユーザ定義関数を呼べます。DOM APIのいくつかは、html5.h
に定義されている関数を通じて呼ぶことができます。
WebAssembly から JS の関数を呼ぶには(一般的な話)
WebAssembly はソフトウェアモジュールを定義します。ES モジュールと同様に、関数をエキスポートするだけではなく、他のモジュールで定義された関数をインポートできます。
インポートされる関数に関する情報は、wasm ファイルに書かれています。この情報は import
セクションに「どういう引数 / 返り値の関数が」「どういう名前で与えられるか」という形で書かれています。
(import "console" "log" (func $log (param i32)))
この例では、i32
の値を1つだけ引数としてとり、返り値は返さない関数が、"console"モジュール内の"log"という属性で与えられる、ことが記述されています。また、このインポートされた関数が$log
として、モジュール内で参照されるということもわかります。引数情報を除くと、次の ES モジュールのインポートと同じような振る舞いをします。
import {log as $log} from "console";
インポートされる関数の実装は、WASM モジュールのインスタンス化時に与えられます。
const mod = await WebAssembly.instantiateStreaming(stream, {
console: {
log: function(ptr){ ... }
}
});
このように、WebAssembly.instantiate
/ WebAssembly.instantiateStreaming
の第2引数でインポートされる関数の実装を与えます。
以上をまとめると、WebAssembly から JS の関数を呼ぶには次の2点がポイントになります:
- どのように WebAssembly の方に関数のシグネチャを書くか
- どのようにインスタンス時に実装を与えるか
この2点はコンパイラ/ツールによって抽象化されていることが多いのですが、それでも元のコードの変更と、JS の実装を行わなければなりません。この視点からコンパイラ/ツールを見ると理解が早まるかと思います。
Emscripten で利用できる手段
Emscripten では次の2つのアナロジーを使って抽象化しています:
- インラインアセンブラ
- 外部関数の定義と、プログラムのリンク
どちらも C/C++ のプログラマには馴染みある概念かと思います。
EM_JS
:インライン JavaScript
インラインアセンブラに相当するものが、ES_JS
です。これを利用すると C/C++ の中に JavaScriptt の関数定義を埋め込むことができます。
次の例では、コンソールに"pass"というログを出す関数 pass
を定義して、main
関数から呼び出しています:
#include <emscripten.h>
EM_JS(void, pass, (), {
console.log("pass");
});
int main(int argc, char **argv){
// 略
pass();
// 略
return 0;
}
EM_JS
は emscripten.h
に定義されているマクロです。4つのパラメータがあります:
- 返り値の型
- 関数名
- 引数リスト
- 関数本体
関数本体は JavaScript で定義します。
もし1ショットで JavaScript のコードを実行するなら、EM_ASM
というマクロの方が適切です。次のように使えます:
int main(int argc, char **argv){
// 略
EM_ASM({
console.log("pass");
});
// 略
return 0;
}
外部関数の実装を JavaScript で与える
外部関数の実装を JavaScript で与えることもできます。例えば、先ほどの pass
関数を、次のように宣言したとします:
extern void pass();
int main(int argc, char **argv){
// 略
pass();
// 略
return 0;
}
別のファイルでこの関数を実装して、リンク時に解決するのはよく行われます。同じことを JavaScript で行おうというのが、この方式です。
実装するファイルは次のようになっています。関数を定義した後に、mergeInto
を呼び出し実装と、シグネチャとの対応づけます:
function pass(){
console.log("pass");
}
mergeInto(LibraryManager.library, {
pass
});
mergetInto
は第1引数のオブジェクトの属性に、第2引数の属性をマージする関数です。LibraryManager.library
には C/C++ からリンクされる JS コードが保持されます。
このファイルを --js-library
オプションの値に指定して emcc
コマンドを実行することで、WASM ファイルと「リンク」できます。
この方式はマングリングされているとうまく動きません。C++ の場合は、次のようにマングリング対象から外しておく必要があります:
extern C{
extern void pass();
}
まとめ:どっちがいいか?
どちらを取るかは、コード量によると思っています。比較的短いコードなら EM_JS
を、そうでなければリンクする方が開発しやすいでしょう。
JavaScript として実装し、テストも JS の技術で完結できるので、私はリンクする方が好みです。