LoginSignup
6
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-06

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

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6