Emscriptenを使ったC/C++の関数のエキスポート方法まとめ


TL;DR;

Web/Node.js で使うことだけ考えるなら、Embind が一番楽です。

いくつか手段があるのでまとめます

C/C++ のコードを WebAssembly に変換する際に便利なツール Emscripten ですが、関数をエキスポートするにはいくつかの方法があります。この記事では、そのやり方をまとめます。

main 関数をエキスポートする場合

何もすることはありません。main を定義し、通常の C/C++ のコードのようにコンパイルするだけです。main 関数は自動的にエキスポートされ、ロード時に自動実行されます。

もし自動実行したくない場合は、-s 'INVOKE_RUN=0'を指定します。

% emcc -o hello.html -s 'INVOKE_RUN=0' hello.c

main 関数をエキスポートしない / main 関数が存在しない場合

main 関数をエキスポートしない場合、もしくは存在しない場合は、エキスポートする関数を明示する必要があります。これは Emscripten が dead code elimination(JS では tree shaking と呼ばれている行為)を行うからです。main 関数がエキスポートされる場合、main 関数から到達可能なもののみがWASMに出力されます。

main 関数がエキスポートされない場合、特に main 関数が存在しない場合は、すべての関数が削除され、ユーザーコードが存在しない wasm ファイルが出力されます。これを避けるために、以下のどちらかの方法でエキスポートするシンボルを指定する必要があります:

  • コマンドライン引数で指定する
  • コード中にアノテーションして指定する

コマンドライン引数で指定する方法

EXPORTED_FUNCTIONS にエキスポートする関数のリストを指定できます。

例えば次のようなコードがあったとします:

#include <stdio.h>
int add(int a, int b){
  return a + b;
}
int sub(int a, int b){
  return a - b;
}
int main(int arg, char** argv){
  printf("Hello, world!");
}

このコードをオプションを指定せずにコンパイルすると、main関数のみがエキスポートされた wasm ファイルが作られます。add 関数も sub 関数もエキスポートされないばかりか、wasm ファイルに含まれないため、これらの関数をライブラリ的に利用することはできません。

こんな時、EXPORTED_FUNCTIONS を指定することで addsub のみがエキスポートされた wasm ファイルを作ることができます:

% emcc -o add_sub.js -s "EXPORTED_FUNCTIONS=['_add', '_sub']" add_sub.c

値には、エキスポートする関数名を表す文字列を要素とする配列を指定します。

関数名には、_ をプレフィックスとしてつけます。例えば add をエキスポートするときは _add とします。

-s オプションは settings.jsの値を設定するオプションです。このオプションの値は"でエスケープされていなければなりません。そのため、"'の入り乱れた、かなり手で入力するには厳しい値を入力することになります。

エキスポートするシンボルが少ない場合は、この方式でもいいかもしれません。個人的な経験では、3 つ以上のシンボルをエキスポートするときは、次に説明するアノテーションを使った方式の方がストレスが少ないように感じました。

EMSCRIPTEN_KEEPALIVE でアノテートする方法

次のように EMSCRIPTEN_KEEPALIVE をエキスポートしてほしい関数の前につけておくと、その関数は dead code elimination の対象から外れ、エキスポートされます。

#include <emscripten.h>

int EMSCRIPTEN_KEEPALIVE add(int a, int b){
  return a + b;
}

EMSCRIPTEN_KEEPALIVEemscripten.h で定義されています。このヘッダファイルは、Emscripten でコンパイルする際には解決されますが、それ以外の場合はコンパイルエラーの原因となります。
EMSCRIPTEN_KEEPALIVE を定義すれば、Emscripten 以外の環境でのコンパイルエラーは避けられます。この単純な実装は、次のようになるでしょう。なお__EMSCRIPTEN__ によって、Emscripten でコンパイルされているかどうかが判別できます

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#else
#define EMSCRIPTEN_KEEPALIVE
#endif

int EMSCRIPTEN_KEEPALIVE add(int a, int b){
  return a + b;
}

Embind を使う方法

以上でプリミティブな値を返す関数をエキスポートすることはできるようになりました。Emscripten は文字列の変換もよしなに行ってくれるため、多くのユースケースをカバーできると思います。

一方でユーザ定義型、特に C++ のクラスを JS からも透過的に使いたいという要求もあるでしょう。そんな時に便利なのが Embind です。これは JS の関数やクラスを自動的に定義することで、アノテーションした関数やクラスにを JS からの透過的な操作を可能にするツールです。例えば次のような関数と、クラスがあったとしましょう。2 次元平面上の点と、その点から原点 (0, 0) までの距離を測るコードです:

int abs(int value){
  return value >= 0 ? value : -value;
}

long norm(int x, int y){
  return abs(x) + abs(y);
}

class Point{
public:
  Point(int x, int y) : x(x), y(y)  {
  }
  long norm() const {
    return ::norm(x, y);
  }
  long length() const {
    return norm();
  }

private:
  int x;
  int y;
};

ここで定義されている Point クラスを JS でも使いたい、ラッパーなんて書きたくないという要求があるのは想像できるでしょう。

const mod = await someWASMLoadingFunction(); // 先のコードが WASM 化されているものを、何がしかの方法でロード

const p = new mod.Point(1, 2);
const len = p.length;

そんな時が Embind の使いどきです。次のようにエキスポートする関数、クラスを記述することで、コンパイル時にグルーコードが自動生成され、透過的に(つまりラッパーを書かずに)C++ で定義したコードを JS から利用できます。

#ifdef __EMSCRIPTEN__
#include <emscripten/bind.h>
#endif
// snip

#ifdef __EMSCRIPTEN__
EMSCRIPTEN_BINDINGS(myModule)
{
  emscripten::class_<Point>("Point")
      .constructor<int, int>()
      .function("norm", &Point::norm)
      .property("length", &Point::length);
}
#endif

EMSCRIPTEN_BINDINGS のブロック内に、JS と C++ のクラス間の対応関係を記述します。

まず _class オブジェクトを作成して、JS のクラスと C++ のクラスの対応関係を定義します。

あとは constructor メソッドでコンストラクターを、function メソッドでメソッドを、property メソッドで属性の対応を定義します。

これらのシンボルは emscripten/bind.h に定義されています。これをインクルードした上で、--bind オプション付きでコンパイルすると、上記の対応に基づいた JS クラスが自動的に作成されます。

% emcc --bind -o point.js point.ccp

なお WebAssembly の GC 対応は議論中で、実装はまだありません。また weak reference に対応していない環境もあります。そのため、使い終わったオブジェクトは明示的に delete メソッドを読んでメモリを解放する必要があります。

まとめ

  • クラスをエキスポートするなら、Embind を使う
  • 数をエキスポートするなら、EMSCRIPTEN_KEEPALIVE をつけるか、Embind を使う

と良さそうです。関数の場合、どちらを使っても良さそうです。パフォーマスオーバヘッドについてはわかりませんが、Embind の方が使い勝手は良いように感じています。私は、メモリの解放に気を使わなければならない点を除けば、WASM であること全く気にしなくてもいい点が気に入っています。

メモリの解放に関しては、Reference type などの仕様に関する議論が進み、ツールとブラウザの実装によって、いずれ解決する問題のようにも感じています。