以前このブログでも紹介したWebブラウザでCASL2を動かすCASL2 Playgoundですが、バイトコードインタプリタの実装をTypeScriptからC++へ移植しWebAssemblyでJavaScriptから使うように実装してみました、という日記です。
実装してみた理由は、ただWebAssemblyを触ってみたかっただけですw
あと、C++についてほとんど書いたことがないのですが読みたいなぁと思うことがあり、文法を学ぶとっかかりが欲しかったのもあります。例えば、一様初期化というのも読めなかったのですよね。 一様初期化 - cpprefjp C++日本語リファレンス
X x {0};
C++については以下の書籍を読みました。良い本なのか判断できるC++の知識は持ち合わせていないのですが、他の言語を触ったことある人がC++に触れるには良かったように感じました。
WebAssemblyについては以下の書籍を読みました。あまり面白くなかった。。けど、Web上から情報を集める手間なくWebAssemblyにとっかかるには良かったかもしれません。
JavaScriptとC++とのやりとりについては、Emscriptenの公式サイトが良いです。
Interacting with code — Emscripten 3.1.49-git (dev) documentation
変更前
以前の記事で紹介した通り、アセンブル処理をするassemblerと、その結果であるバイト列を解釈して実行するinterpeterを、分割した作りになっている。
変更後
バイト列を解釈して実行するinterpreterをC++で実装し、JavaScript側でアセンブル処理した結果のバイト列をコピーして渡す方式とする。
wasmファイルの生成
C++ソースコードからwasmへのコンパイルはemccというコマンドを使う。CMakeも使ってみて、とりあえずビルドできたものの、CMakeムズカシイ。。
emccによって、wasmファイルとjsファイルが出力される。jsファイルはwasmをダウンロードして使えるようにするためのコードが自動生成されたもの。このjsファイルをHTMLのscriptタグで読み込むようにすればwasmファイルをダウンロードしてくれる。
ブラウザのデベロッパーツールでバイナリであるwasmファイルの内容を見れる。バイナリではなくwat形式と呼ばれるテキストに変換したもので読めるのだけど、これが面白い!なんとS式!
JavaScriptからC++の関数を呼ぶ
JavaScriptからC++のコードを呼び出す方法については、2通り使ってみた。
- Interacting with code — Emscripten 3.1.49-git (dev) documentation
- Embind — Emscripten 3.1.49-git (dev) documentation
後者の方がスマートに呼び出せる。だけど、デバッグのしやすさは前者かもしれない??なぜなら、前者の方式だとJavaScript側から呼べるようにしたC++側の関数名がwat形式のテキストに出力されるのだけど、後者のEmbindを使った方式だとC++側の関数名をwat形式のテキストから探すことができない。もしかしたら何か方法があるのかな?それとも現実的にwat形式のテキストを使ってデバッグすることは滅多にないのかな?
例えば、以下のような makeMachine
関数は、wat形式のテキストを検索すると発見できる。
extern "C" { EMSCRIPTEN_KEEPALIVE int makeMachine(uint8_t* bytes, int size, int startAddress) { ... }
JavaScriptからC++へバイト列を渡す
JavaScriptからC++側の malloc
を呼び出しメモリを確保、確保したメモリ領域にアセンブル結果のバイト列をコピーする。そしてC++側の関数を ccall
で呼ぶときに引数として確保したメモリのポインタを渡す。
// JavaScript // assembleの結果のバイト列をwasmのメモリにコピー const arr = new Uint8Array(assembleResult.buffer); this.pointer = Module._malloc(Uint8Array.BYTES_PER_ELEMENT * arr.length); if (this.pointer == null) { throw new Error("entry_point :: malloc failed"); } Module.HEAP8.set(arr, this.pointer / Uint8Array.BYTES_PER_ELEMENT); // wasmの関数を呼び出して初期化 const setupResult = Module.ccall('makeMachine', 'number', ['number', 'number', 'number'], [this.pointer, tmpMemory.content.byteLength, startMemAddress] );
C++側のバイト列をJavaScriptから参照する
Embindを使ってC++側のバイト列をJavaScript側から参照できるようにする。JavaScriptでは Uint8Array
型になるが、他の部分で処理しやすいように DataView
として持つようにした。
// C++ #include <emscripten.h> #include <emscripten/bind.h> #include <emscripten/val.h> using namespace emscripten; unsigned char *buf; size_t bufSize; val getMemory() { return val(typed_memory_view(bufSize, buf)); } // JavaScriptから直接参照できるようにする EMSCRIPTEN_BINDINGS(interpreter_module) { function("getMemory", &getMemory); }
// JavaScript // wasmのメモリの参照を取得してフィールドに保存 const memoryUint8Array = Module.getMemory(); this.memory = new Memory(); this.memory.content = new DataView(memoryUint8Array.buffer, memoryUint8Array.byteOffset, memoryUint8Array.byteLength);
JavaScript側で malloc
を呼び出してメモリを確保したけど、C++側で確保したメモリをEmbindでJavaScript側へ公開して、そこにアセンブル結果を書き込みする方が良いのかな?
C++側では配列ではなく vector
で保持しようとしたのだけど、JavaScript側からC++側の関数を呼び出したときに保持していたはずのバイト列がなぜか初期化されてしまって上手くいかなかった。
おしまい
色々試行錯誤して動くものはできたけど、まだまだ不明点が多すぎますね。。深掘りしたい気持ちになったときに改めてチャレンジしよう!