WebAssembly ライブラリに JavaScript コードを埋め込んで外部と通信する方法について説明します。
WebAssembly をウェブと統合する際は、ウェブ API やサードパーティ ライブラリなどの外部 API を呼び出す方法が必要です。次に、API が返す値とオブジェクト インスタンスを格納する手段と、格納された値を後で他の API に渡す方法が必要になります。非同期 API の場合は、同期 C/C++ コードで Asyncify を使用して Promise を待機し、オペレーションの完了後に結果を読み取る必要もあります。
Emscripten には、そのようなやり取りを行うためのツールがいくつか用意されています。
emscripten::val
: C++ で JavaScript 値を格納して操作します。EM_JS
: JavaScript スニペットを埋め込み、C/C++ 関数としてバインドします。EM_ASYNC_JS
:EM_JS
に似ていますが、非同期の JavaScript スニペットの埋め込みを容易にします。EM_ASM
: 短いスニペットを埋め込み、関数を宣言せずにインラインで実行します。- 多くの JavaScript 関数を 1 つのライブラリとして宣言する高度なシナリオでは、
--js-library
。
この投稿では、これらの機能を同様のタスクに使用する方法について説明します。
emscripten::val クラス
emcripten::val
クラスは Embind によって提供されます。グローバル API の呼び出し、JavaScript 値の C++ インスタンスへのバインド、C++ 型と JavaScript 型の間の値の変換が可能です。
これを Asyncify の .await()
と併用し、JSON を取得して解析する方法を次に示します。
#include <emscripten/val.h>
using namespace emscripten;
val fetch_json(const char *url) {
// Get and cache a binding to the global `fetch` API in each thread.
thread_local const val fetch = val::global("fetch");
// Invoke fetch and await the returned `Promise<Response>`.
val response = fetch(url).await();
// Ask to read the response body as JSON and await the returned `Promise<any>`.
val json = response.call<val>("json").await();
// Return the JSON object.
return json;
}
// Example URL.
val example_json = fetch_json("https://httpbin.org/json");
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
このコードは適切に機能しますが、多数の中間ステップを実行します。val
に対する各オペレーションで、次の手順を行う必要があります。
- 引数として渡された C++ 値をなんらかの中間形式に変換します。
- JavaScript に移動し、引数を読み取り、JavaScript 値に変換します。
- 関数を実行する
- 結果を JavaScript から中間形式に変換します。
- 変換された結果を C++ に返し、C++ が最後にそれを読み取ります。
また、各 await()
は、WebAssembly モジュールのコールスタック全体をアンワインドし、JavaScript に戻り、待機して、オペレーションが完了したら WebAssembly スタックを復元することで、C++ 側を一時停止する必要があります。
このようなコードは、C++ から何も必要としません。C++ コードは、一連の JavaScript オペレーションのドライバとしてのみ動作します。fetch_json
を JavaScript に移行し、中間ステップのオーバーヘッドも削減できるとしたらどうでしょうか。
EM_JS マクロ
EM_JS macro
を使用すると、fetch_json
を JavaScript に移行できます。Emscripten の EM_JS
を使用すると、JavaScript スニペットによって実装される C/C++ 関数を宣言できます。
WebAssembly 自体と同様に、サポートされているのは数値引数と戻り値のみという制限があります。その他の値を渡すには、対応する API を使用して値を手動で変換する必要があります。こちらの例をご覧ください。
数値を渡しても変換は不要です。
// Passing numbers, doesn't need any conversion.
EM_JS(int, add_one, (int x), {
return x + 1;
});
int x = add_one(41);
JavaScript との間で文字列を渡す場合は、preamble.js から対応する変換関数と割り当て関数を使用する必要があります。
EM_JS(void, log_string, (const char *msg), {
console.log(UTF8ToString(msg));
});
EM_JS(const char *, get_input, (), {
let str = document.getElementById('myinput').value;
// Returns heap-allocated string.
// C/C++ code is responsible for calling `free` once unused.
return allocate(intArrayFromString(str), 'i8', ALLOC_NORMAL);
});
最後に、より複雑で任意の値の型が必要な場合は、前述の val
クラスの JavaScript API を使用できます。これを使用すると、JavaScript 値と C++ クラスを中間ハンドルに変換し、その逆に変換できます。
EM_JS(void, log_value, (EM_VAL val_handle), {
let value = Emval.toValue(val_handle);
console.log(value);
});
EM_JS(EM_VAL, find_myinput, (), {
let input = document.getElementById('myinput');
return Emval.toHandle(input);
});
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
log_value(obj.as_handle()); // logs { x: 1, y: 2 }
val myinput = val::take_ownership(find_input());
// Now you can store the `find_myinput` DOM element for as long as you like, and access it later like:
std::string value = input["value"].as<std::string>();
これらの API を念頭に置いて、fetch_json
の例を書き換えると、JavaScript を終了せずにほとんどの処理を行えます。
EM_JS(EM_VAL, fetch_json, (const char *url), {
return Asyncify.handleAsync(async () => {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
関数の開始点と終了点で、まだいくつかの明示的な変換がありますが、残りは通常の JavaScript コードになりました。val
と異なり、JavaScript エンジンによる最適化が可能になり、すべての非同期処理で C++ 側を一度一時停止するだけで済みます。
EM_ASYNC_JS マクロ
残っているのは Asyncify.handleAsync
ラッパーです。その目的は Asyncify で async
JavaScript 関数を実行できるようにすることだけです。実際、このユースケースは非常に一般的であるため、現在は、これらを結合する専用の EM_ASYNC_JS
マクロが用意されています。
これを使用して fetch
サンプルの最終バージョンを生成する方法は次のとおりです。
EM_ASYNC_JS(EM_VAL, fetch_json, (const char *url), {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
EM_ASM
JavaScript スニペットを宣言するには、EM_JS
を使用することをおすすめします。他の JavaScript 関数のインポートと同様に、宣言されたスニペットを直接バインドするため効率的です。また、すべてのパラメータの型と名前を明示的に宣言できるため、優れたエルゴノミクスを実現できます。
ただし、場合によっては、console.log
呼び出しや debugger;
ステートメントなどのクイック スニペットを挿入し、関数自体をすべて宣言する必要はありません。そのようなまれなケースでは、EM_ASM macros family
(EM_ASM
、EM_ASM_INT
、EM_ASM_DOUBLE
)を使用するほうが簡単な場合があります。これらのマクロは EM_JS
マクロと類似していますが、関数を定義するのではなく、挿入した場所でコードをインライン実行します。
関数のプロトタイプを宣言しないため、戻り値の型を指定し、引数にアクセスする別の方法が必要になります。
戻り値の型を選択するには、正しいマクロ名を使用する必要があります。EM_ASM
ブロックは void
関数のように動作し、EM_ASM_INT
ブロックは整数値を返し、EM_ASM_DOUBLE
ブロックはそれに応じて浮動小数点数を返します。
渡された引数はすべて、JavaScript 本文で $0
、$1
などの名前で使用できます。一般的な EM_JS
や WebAssembly と同様に、引数は数値(整数、浮動小数点数、ポインタ、ハンドル)のみに制限されます。
EM_ASM
マクロを使用して任意の JS 値をコンソールに記録する方法の例を次に示します。
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
// executes inline immediately
EM_ASM({
// convert handle passed under $0 into a JavaScript value
let obj = Emval.fromHandle($0);
console.log(obj); // logs { x: 1, y: 2 }
}, obj.as_handle());
--js-library
最後に、Emscripten では、カスタム独自のライブラリ形式の別のファイルで JavaScript コードを宣言できます。
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
次に、対応するプロトタイプを C++ 側で手動で宣言する必要があります。
extern "C" void log_value(EM_VAL val_handle);
両側で宣言された JavaScript ライブラリは、--js-library option
を介してメインコードにリンクし、対応する JavaScript 実装とプロトタイプを接続することができます。
ただし、このモジュール形式は非標準であり、依存関係のアノテーションを注意深く行う必要があります。そのため、ほとんどの場合は高度なシナリオを想定しています。
まとめ
この投稿では、WebAssembly を使用する際に JavaScript コードを C++ に統合するさまざまな方法を見てきました。
このようなスニペットを含めることで、長い操作シーケンスをより簡潔かつ効率的に表現し、サードパーティ ライブラリ、新しい JavaScript API、さらには C++ や Embind で表現できない JavaScript 構文機能を活用できるようになります。