Emscripten の embind

JS が Wasm にバインドされます。

前回の wasm の記事では、C ライブラリを Wasm にコンパイルしてウェブで使用できるようにする方法について説明しました。私(そして多くの読者)から印象に残ったのは、使用する Wasm モジュールのどの関数を使用するかを手動で宣言する必要があるという、粗雑で少し不格好な方法です。このコード スニペットは次のとおりです。

const api = {
    version: Module.cwrap('version', 'number', []),
    create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
    destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};

ここでは、EMSCRIPTEN_KEEPALIVE でマークした関数の名前、戻り値の型、引数の型を宣言します。その後、api オブジェクトのメソッドを使用して、これらの関数を呼び出すことができます。ただし、この方法では Wasm を使用しても文字列はサポートされず、メモリチャンクを手動で移動する必要があり、多くのライブラリ API の使用が非常に面倒になります。もっと良い方法はありませんか?なぜあるのか そうでなければこの記事の内容は?

C++ 名の修飾

これらのバインディングに役立つツールをビルドするにはデベロッパー エクスペリエンスで十分ですが、実際には、より差し迫った理由があります。C または C++ コードをコンパイルするときに、各ファイルが個別にコンパイルされるからです。その後、リンカーが、いわゆるオブジェクト ファイルをすべてまとめて Wasm ファイルに変換します。C では、リンカーが使用する関数の名前をオブジェクト ファイル内で引き続き使用できます。C 関数を呼び出すために必要なのは名前だけです。名前は cwrap() に文字列として指定します。

一方、C++ は関数のオーバーロードをサポートしています。つまり、シグネチャが異なる限り、同じ関数を複数回実装できます(型が異なるパラメータなど)。コンパイラ レベルでは、add のような適切な名前は、リンカーの関数名に含まれるシグネチャをエンコードするものにマングリングされます。その結果、その名前で関数を検索できなくなります。

embind を入力

embind は Emscripten ツールチェーンの一部であり、C++ コードにアノテーションを付けることができる一連の C++ マクロを提供します。JavaScript から使用する関数、列挙型、クラス、値の型を宣言できます。まずは、プレーンな関数を使って簡単に説明します。

#include <emscripten/bind.h>

using namespace emscripten;

double add(double a, double b) {
    return a + b;
}

std::string exclaim(std::string message) {
    return message + "!";
}

EMSCRIPTEN_BINDINGS(my_module) {
    function("add", &add);
    function("exclaim", &exclaim);
}

以前の記事と比較すると、関数に EMSCRIPTEN_KEEPALIVE アノテーションを付ける必要がなくなるため、emscripten.h をインクルードしなくなりました。代わりに、EMSCRIPTEN_BINDINGS セクションには、関数を JavaScript に公開する名前をリストしています。

このファイルをコンパイルするには、前の記事と同じセットアップ(または、必要であれば同じ Docker イメージ)を使用します。embind を使用するには、--bind フラグを追加します。

$ emcc --bind -O3 add.cpp

あとは、新しく作成した Wasm モジュールを読み込む HTML ファイルを作成するだけです。

<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
    console.log(Module.add(1, 2.3));
    console.log(Module.exclaim("hello world"));
};
</script>

ご覧のとおり、cwrap() はもう使用されていません。追加の設定なしですぐに使用できますしかし、さらに重要な点は、文字列を機能させるためにメモリのチャンクを手動でコピーする必要がないことです。embind はこれを型チェックとともに無料で提供しています。

引数の数が正しくないか、引数の型が正しくない関数を呼び出すと、DevTools でエラーが発生する

これは、時として非常に扱いづらい Wasm エラーに対処する代わりに、早期にエラーをキャッチできるため、非常に便利です。

オブジェクト

JavaScript のコンストラクタと関数の多くは、options オブジェクトを使用します。これは JavaScript では便利なパターンですが、Wasm で手動で実現するのは非常に面倒な作業です。embind もここで役立ちます。

たとえば、文字列を処理するこの非常に便利な C++ 関数を思いついたので、すぐにウェブで使いたいと思っています。方法は次のとおりです。

#include <emscripten/bind.h>
#include <algorithm>

using namespace emscripten;

struct ProcessMessageOpts {
    bool reverse;
    bool exclaim;
    int repeat;
};

std::string processMessage(std::string message, ProcessMessageOpts opts) {
    std::string copy = std::string(message);
    if(opts.reverse) {
    std::reverse(copy.begin(), copy.end());
    }
    if(opts.exclaim) {
    copy += "!";
    }
    std::string acc = std::string("");
    for(int i = 0; i < opts.repeat; i++) {
    acc += copy;
    }
    return acc;
}

EMSCRIPTEN_BINDINGS(my_module) {
    value_object<ProcessMessageOpts>("ProcessMessageOpts")
    .field("reverse", &ProcessMessageOpts::reverse)
    .field("exclaim", &ProcessMessageOpts::exclaim)
    .field("repeat", &ProcessMessageOpts::repeat);

    function("processMessage", &processMessage);
}

processMessage() 関数のオプションの構造体を定義しています。EMSCRIPTEN_BINDINGS ブロックで value_object を使用すると、JavaScript でこの C++ 値をオブジェクトとして認識させることができます。この C++ 値を配列として使用したい場合は、value_array を使用することもできます。また、processMessage() 関数をバインドします。残りは embind magic です。これで、ボイラープレート コードなしで JavaScript から processMessage() 関数を呼び出すことができるようになりました。

console.log(Module.processMessage(
    "hello world",
    {
    reverse: false,
    exclaim: true,
    repeat: 3
    }
)); // Prints "hello world!hello world!hello world!"

クラス

完全性を期すために、embind を使用してクラス全体を公開する方法も紹介する必要があります。これにより、ES6 クラスとの相乗効果が大きくなります。パターンが見えてくるはずです。

#include <emscripten/bind.h>
#include <algorithm>

using namespace emscripten;

class Counter {
public:
    int counter;

    Counter(int init) :
    counter(init) {
    }

    void increase() {
    counter++;
    }

    int squareCounter() {
    return counter * counter;
    }
};

EMSCRIPTEN_BINDINGS(my_module) {
    class_<Counter>("Counter")
    .constructor<int>()
    .function("increase", &Counter::increase)
    .function("squareCounter", &Counter::squareCounter)
    .property("counter", &Counter::counter);
}

JavaScript 側では、これはネイティブ クラスとほぼ同じです。

<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
    const c = new Module.Counter(22);
    console.log(c.counter); // prints 22
    c.increase();
    console.log(c.counter); // prints 23
    console.log(c.squareCounter()); // prints 529
};
</script>

C についてはどうでしょうか。

embind は C++ 用に作成されており、C++ ファイルでのみ使用できますが、C ファイルに対してリンクできないわけではありません。C と C++ を混在させるには、入力ファイルを 2 つのグループに分けるだけで済みます。1 つは C ファイル用、もう 1 つは C++ ファイル用で、次のように emcc の CLI フラグを拡張します。

$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp

まとめ

embind により、wasm や C/C++ を使用する際のデベロッパー エクスペリエンスが大幅に向上します。この記事では、embind のすべてのオプションを網羅しているわけではありません。もしご興味があれば、embind のドキュメントをご覧になることをおすすめします。 embind を使用すると、gzip の場合に Wasm モジュールと JavaScript グルーコードの両方が最大 11,000 個(特に小さなモジュールの場合)大きくなる可能性があるので注意してください。Wasm サーフェスが非常に小さい場合は、embind のコストが本番環境よりも高くなる可能性があります。とにかく、ぜひお試しください