25
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

WebAssemblyAdvent Calendar 2019

Day 5

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

Last updated at Posted at 2019-12-04

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 などの仕様に関する議論が進み、ツールとブラウザの実装によって、いずれ解決する問題のようにも感じています。

25
13
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
25
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?