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
を指定することで add
と sub
のみがエキスポートされた 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_KEEPALIVE
は emscripten.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 などの仕様に関する議論が進み、ツールとブラウザの実装によって、いずれ解決する問題のようにも感じています。