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
は含まれていません。代わりに、関数を JavaScript に公開する名前を一覧表示する EMSCRIPTEN_BINDINGS
セクションがあります。
このファイルをコンパイルするには、前の記事と同じ設定(または、必要に応じて同じ 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 は、型チェックとともに、それを無料で提供します。

これにより、ときどき発生するかなり扱いにくい wasm エラーに対処する代わりに、エラーを早期にキャッチできます。
オブジェクト
多くの JavaScript コンストラクタと関数はオプション オブジェクトを使用します。これは 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
を使用して、この C++ 値を JavaScript でオブジェクトとして認識させることができます。この C++ 値を配列として使用する場合は、value_array
を使用することもできます。また、processMessage()
関数もバインドします。残りは embind の魔法です。これで、ボイラープレート コードなしで 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++ を混在させるには、入力ファイルを C 用と C++ 用の 2 つのグループに分け、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 を使用すると、wasm モジュールと JavaScript グルーコードの両方が、gzip 圧縮時に最大 11k まで大きくなる可能性があります。特に小さなモジュールで顕著です。wasm サーフェスが非常に小さい場合、本番環境では embind のコストがメリットを上回る可能性があります。それでも、ぜひ試してみてください。