WebAssembly ライブラリに JavaScript コードを埋め込んで外部と通信する方法について学びます。
WebAssembly とウェブの統合に取り組む場合は、ウェブ API やサードパーティ ライブラリなどの外部 API を呼び出す方法が必要です。次に、これらの API が返す値とオブジェクト インスタンスを保存する方法と、保存した値を後で他の API に渡す方法が必要になります。非同期 API の場合は、Asyncify を使用して同期 C/C++ コードでプロミスを待機し、オペレーションの完了後に結果を読み取る必要がある場合もあります。
Emscripten には、このようなインタラクション用のツールがいくつか用意されています。
emscripten::val
: C++ で JavaScript 値を保存して操作します。EM_JS
: JavaScript スニペットを埋め込み、C/C++ 関数としてバインドします。EM_ASYNC_JS
:EM_JS
に似ていますが、非同期 JavaScript スニペットを簡単に埋め込むことができます。EM_ASM
: 短いスニペットを埋め込み、関数を宣言せずにインラインで実行します。--js-library
: 多くの JavaScript 関数を 1 つのライブラリとしてまとめて宣言する高度なシナリオに使用します。
この記事では、これらのツールをすべて類似のタスクに使用する方法について説明します。
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++ 側を一時停止する必要があるのは 1 回だけです。
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 構文機能を活用できるようになります。