DEV Community

N. Shimizu
N. Shimizu

Posted on

Emscripten で C/C++ から JS の関数を呼ぶには

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点がポイントになります:

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

この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_JSemscripten.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 の技術で完結できるので、私はリンクする方が好みです。

Top comments (0)