V8 を wasm の実行環境として C++ アプリケーションに組み込んでみよう。
今回使う V8 のバージョンは 7.8.58。samples/hello-world.cc
に wasm を実行する例があるけれど、この例は JS 上に定義された API を叩くスクリプトを実行する、という体で書かれている。V8 としては JS から使う前提があるからこういう例になっているのだろうけど、JS を使わずに wasm を実行できないだろうか。
検索すると stackoverflow に同じ思いの先人が質問していて、回答を元にコードを例示してくれていた。ありがたい。ただいくつか API の変更があったようで、v7.8.58 では動かない。V8 の変更履歴を参照しつつ改変したら動くようになった。これで JS スクリプトを経由せずに wasm を実行できるようになった。が、依然として JS API 自体には依存しており処理が減っている感じがしない。
さらに言えば前処理と後処理が冗長なのはいいとしても、値の引き回しに v8::Local<T>
を使わないといけないのも釈然としない。今回は JS を使わないのだから、C++ の数値を V8 JS value に変換し、さらにそれから JS value を wasm の数値型に変換する、といったステップを踏むのは無駄に感じる。C++ ⇔ wasm の型変換があればいい。
そう思いつつ V8 のコードを読んでいると c-api.cc というファイルが目についた。冒頭のコメントによると、仕様への提案をベースに書かれたものらしい。wasm を C/C++ に組み込むための提案、だそう。これが求めていたものっぽい。
使い方を調べてみよう。V8 は Wasm C API を third_party/wasm-api
配下に組み込んでいる。V8 を組み込む C++アプリケーションはthird_party/wasm-api/wasm.hh
をインクルードすることで wasm の関数の引数や戻り値を v8::Local<T>
を使うことなくやりとりできるようになる。提案の例を見れば使い方はつかめると思う。要点を抜粋すると以下のような感じ。
wasm::vec<byte_t> binary; // wasm バイナリ、外部からの入力
// 実行環境の初期化
auto engine = wasm::Engine::make();
auto store_ = wasm::Store::make(engine.get());
auto store = store_.get();
// コンパイルとインスタンス生成
auto module = wasm::Module::make(store, binary);
auto instance = wasm::Instance::make(store, module.get(), nullptr);
// 関数の取り出し
auto exports = instance->exports();
assert(exports.size() == 1 && exports[0]->kind == wasm::EXTERN_FUNC && exports[0]->func());
auto run_func = exports[0]->func();
// 実行
wasm::Val args[] = { wasm::Val::i32(14), wasm::Val::i32(9) };
wasm::Val results[1];
run_func->call(args, results);
printf("%d\n", results[0].i32());
(コード全体はここを参照)
V8 API を使った例に比べるとだいぶんすっきりとする。ただ内部ではv8::Context
やらv8::Isolate
なんかを使っているので初期化等にかかる時間はたぶん大差がない。
一方、関数呼び出しの引数と戻り値に関しては V8 JS value への変換がなくなっており、この部分は高速化が見込めるんじゃないだろうか。
今回は V8 を使うのを前提としたけれど、実際のところ wasm を実行したいだけであれば Wasmer を使う、あるいは WASI に則った実行環境を使うほうが良い。JS のオーバヘッドが気になるなら JS をサポートしてない環境を使うのが理にかなっている。