Emscripten で C/C++ から JS の関数を呼ぶには
Tags: webassembly, emscripten
Published: 2020-03-30 01:17:37

## TL;DR;

* コード中に JavaScript を埋め込む
* 外部関数として定義して、実装を　WASM 出力時に埋め込む

このどちらかで、JavaScriptのユーザ定義関数を呼べます。DOM APIのいくつかは、[`html5.h`](https://emscripten.org/docs/api_reference/html5.h.html) に定義されている関数を通じて呼ぶことができます。

## WebAssembly から JS の関数を呼ぶには（一般的な話）

WebAssembly はソフトウェアモジュールを定義します。ES モジュールと同様に、関数をエキスポートするだけではなく、他のモジュールで定義された関数をインポートできます。

インポートされる関数に関する情報は、wasm ファイルに書かれています。この情報は `import` セクションに「どういう引数 / 返り値の関数が」「どういう名前で与えられるか」という形で書かれています。

~~~LISP
(import "console" "log" (func $log (param i32)))
~~~

この例では、`i32`の値を1つだけ引数としてとり、返り値は返さない関数が、"console"モジュール内の"log"という属性で与えられる、ことが記述されています。また、このインポートされた関数が`$log`として、モジュール内で参照されるということもわかります。引数情報を除くと、次の ES モジュールのインポートと同じような振る舞いをします。

~~~JavaScript
import {log as $log} from "console";
~~~

インポートされる関数の実装は、WASM モジュールのインスタンス化時に与えられます。

~~~JavaScript
const mod = await WebAssembly.instantiateStreaming(stream, {
  console: {
    log: function(ptr){ ... }
  }
});
~~~

このように、[`WebAssembly.instantiate`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate) / [`WebAssembly.instantiateStreaming`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiateStreaming) の第２引数でインポートされる関数の実装を与えます。

以上をまとめると、WebAssembly から JS の関数を呼ぶには次の2点がポイントになります：

1. どのように WebAssembly の方に関数のシグネチャを書くか
2. どのようにインスタンス時に実装を与えるか

この2点はコンパイラ/ツールによって抽象化されていることが多いのですが、それでも元のコードの変更と、JS の実装を行わなければなりません。この視点からコンパイラ/ツールを見ると理解が早まるかと思います。

## Emscripten で利用できる手段

Emscripten では次の2つのアナロジーを使って抽象化しています：

* インラインアセンブラ
* 外部関数の定義と、プログラムのリンク

どちらも C/C++ のプログラマには馴染みある概念かと思います。

### `EM_JS`：インライン JavaScript

インラインアセンブラに相当するものが、[`ES_JS`](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#interacting-with-code-call-javascript-from-native) です。これを利用すると C/C++ の中に JavaScriptt の関数定義を埋め込むことができます。

次の例では、コンソールに"pass"というログを出す関数 `pass` を定義して、`main` 関数から呼び出しています：

~~~C
#include <emscripten.h>

EM_JS(void, pass, (), {
  console.log("pass");
});

int main(int argc, char **argv){
  // 略
  pass();
  // 略
  return 0;
}
~~~

[`EM_JS` は `emscripten.h` に定義されているマクロ](https://emscripten.org/docs/api_reference/emscripten.h.html#inline-assembly-javascript)です。4つのパラメータがあります：

* 返り値の型
* 関数名
* 引数リスト
* 関数本体

関数本体は JavaScript で定義します。

もし1ショットで JavaScript のコードを実行するなら、[`EM_ASM` というマクロの方](https://emscripten.org/docs/api_reference/emscripten.h.html#c.EM_ASM)が適切です。次のように使えます：

~~~C
int main(int argc, char **argv){
  // 略
 EM_ASM({
   console.log("pass");
 });
  // 略
 return 0;
}
~~~

### 外部関数の実装を JavaScript で与える

外部関数の実装を JavaScript で与えることもできます。例えば、先ほどの `pass` 関数を、次のように宣言したとします：

~~~C
extern void pass();

int main(int argc, char **argv){
  // 略
  pass();
  // 略
  return 0;
}
~~~

別のファイルでこの関数を実装して、リンク時に解決するのはよく行われます。[同じことを JavaScript で行おうというのが、この方式です](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#implement-c-in-javascript)。

実装するファイルは次のようになっています。関数を定義した後に、`mergeInto`を呼び出し実装と、シグネチャとの対応づけます：

~~~javascript
function pass(){
  console.log("pass");
}

mergeInto(LibraryManager.library, {
  pass
});
~~~

`mergetInto` は第1引数のオブジェクトの属性に、第2引数の属性をマージする関数です。`LibraryManager.library` には C/C++ からリンクされる JS コードが保持されます。

このファイルを　`--js-library` オプションの値に指定して `emcc` コマンドを実行することで、WASM ファイルと「リンク」できます。

この方式はマングリングされているとうまく動きません。C++ の場合は、次のようにマングリング対象から外しておく必要があります：

~~~c++
extern C{
  extern void pass();
}
~~~

## まとめ：どっちがいいか？

どちらを取るかは、コード量によると思っています。比較的短いコードなら `EM_JS` を、そうでなければリンクする方が開発しやすいでしょう。

JavaScript として実装し、テストも JS の技術で完結できるので、私はリンクする方が好みです。