EdgeでWebAssemblyを動かしてみる
Created at:
A peek into the WebAssembly Browser Previewや
ChakraCore v1.4.0からEnable WebAssembly browser preview support under experimental flag
とあるようにWebAssemblyのプレビューサポートがEdgeとChakraCoreに入ったので試してみようというお話です。
WebAssemblyのバイナリを作る
WebAssemblyはJavaScriptのようにテキストではなくバイナリの形をとります。つまり実行したいJavaScriptのコードをバイナリに変換する必要があります。
とはいえ何でも変換できるわけではありません。
基本的には低レベルなコード、要するにasm.jsのコードを変換することになります。将来的にはWebAPIやその他オブジェクト類も視野にあるようですが現状では扱えません。
Emscriptenなどのコンパイラはasm.jsのコードを吐くので一旦それで十分ということでしょう。
asm.jsなコードをWebAssemblyに変換すればよいので早速ツールを準備します。本当はEmscriptenでC言語のコードを変換する方が面白いのですがいろいろビルドする準備がとても面倒なのでとりあえずasm.jsをダイレクトに変換してみます。
asm.jsをWebAssemblyにコンパイルするためのツールとしてBinaryenというものが提供されています。
このbinaryenはC++で書かれているasm.jsやLLVM Emscripten出力を変換するためのコンパイラツールチェインです。
残念ながらWindows向けバイナリが提供されているわけではないので結局ビルドが必要ですが
Visual Studio 2015とCMakeさえあれば大丈夫です。
Visual StudioとCMakeが揃ったらリポジトリを git clone し、コマンドプロンプトで cmake . を実行すると Binaryen.sln が生成されます。このソリューションファイルを開き、ALL_BUILD プロジェクトをビルドすれば出来上がりです。意外と簡単です。
ビルド後に bin フォルダにいくつかの実行ファイルが生成されます。その中で今回使うのは以下の二つのツールです。
- asm2wasm: asm.js を wasm (wast)にコンパイルするツール
- wasm-as: wast を wasm に変換するツール
asm.jsをWebAssemblyに変換してみる
というわけでasm.jsなコードをWebAssemblyの形に変換します。変換するコードはBinaryenのREADMEにある以下のシンプルな足し算するだけのコードで試してみます。コードは適当な名前(sample.js など)で保存してください。
function () {
"use asm";
function add(x, y) {
x = x | 0;
y = y | 0;
return x + y | 0;
}
return { add: add };
}
Binaryenのフローではasm.jsからWebAssemblyのバイナリフォーマット(.wasm)へは直接変換できないので一度テキストフォーマット(wast)に変換して、それをバイナリフォーマットへ変換するという手順が必要です。
というわけでasm.jsをwast形式に変換するasm2wasmコマンドを実行します。しかしwastに変換するのにasm2wasmなのは納得いかないものがあります。
.\asm2wasm.exe test.js
コマンドを実行するとwastへ変換した結果が標準出力に出力されます。 wastはいわゆるS式で単純なコードであれば何となく雰囲気はつかめるのではないでしょうか。
(module
(import "env" "memory" (memory $0 256 256))
(import "env" "table" (table 0 0 anyfunc))
(import "env" "memoryBase" (global $memoryBase i32))
(import "env" "tableBase" (global $tableBase i32))
(export "add" (func $add))
(func $add (param $x i32) (param $y i32) (result i32)
(return
(i32.add
(get_local $x)
(get_local $y)
)
)
)
)
標準出力に出力されても困るのでファイルに保存するように -o オプションでファイル名を指定して出力しましょう。
.\asm2wasm.exe test.js -o test.wast
これで test.wast に変換結果が書き込まれます。次はバイナリに変換…と行きたいところなのですがその前に出力結果を少し変更します。test.wastファイルを適当なテキストエディタで開いて import セクションを削除します。
本当はモジュールを読み込む際にimportするべきものをちゃんと渡してあげれば動くのではと思うのですがEdgeやChromeでうまく動かせず、間違ってるのか仕様と実装が違うのかよくわからないこともありとりあえずなくていいので削除します。出力されているimportは主にEmscriptenを想定している気がします。
で、importセクションを削るとこんな感じです。メソッドをexportしているだけになりました。
(module
(export "add" (func $add))
(func $add (param $x i32) (param $y i32) (result i32)
(return
(i32.add
(get_local $x)
(get_local $y)
)
)
)
)
wastができたら次はバイナリ形式であるwasm形式に変換します。wasm形式への変換にはwasm-as コマンドを使用します。特に難しいこともないので単に実行するだけです。
.\wasm-as.exe test.wast -o test.wasm
これでWebAssemblyのバイナリが出来上がりです!
WebAssemblyのバイナリをブラウザで実行してみる
というわけでwasmが出来上がったのでさっそく実行してみましょう。今回はEdge (Insider Preview)で実行してみます(Chromeで実行しても大丈夫です)。
適当なHTMLを作って次のようなスクリプトを書いて開いてみましょう。
fetch('test.wasm')
.then(x => x.arrayBuffer())
.then(x => WebAssembly.compile(x))
.then(x => {
const instance = new WebAssembly.Instance(x, {});
const retval = instance.exports.add(1, 2);
alert(retval);
});
1行目と2行目はfetch APIでバイナリを読み込んでいます。WebAssembly関係ないです。
3行目でwasmバイナリはバイトコードみたいなものなのでコンパイルしてモジュールにします。それが WebAssembly.compile
です。
5行目はコンパイルしてモジュールになったものからインスタンス(JavaScript界のオブジェクト)を作ります。WebAssembly.Instance
の第一引数にはモジュール、第二引数にはimportするオブジェクトを指定します。今回はimportするものがないので空です。
6行目はインスタンスからエクスポートされている add
メソッドを呼び出しです。ついにWebAssemblyのメソッドにたどり着きました。
7行目はaddの結果を出力しているだけですね。
このスクリプトを実行すると alert で 3 が表示されてWebAssemblyを動かした実感を得られるかどうかはわかりませんが動くことを確認できます。まあ面白くはないですけど…。
まとめ
手で書いたasm.jsから吐いても面白くないというかそもそもasm.jsも人間が書くものではない感じがするので、基本的にはEmscriptenのような何らかのコンパイラが出力するものという感じがしますね。C#もLLVMバックエンドとなるLLILCがちゃんと動くようになったら行けるかも、というかMSILからC++コードを吐いてEmscriptenを通してasm.jsというのはUnityがすでにやっているのでそういう道もありそうです。
触ってみた感じとしてWebAssembly.Instanceを作った後は単なるJavaScriptのオブジェクトとして使えるので、ライブラリとして提供されるというユースケースは良さそうです。例えばzlibとかそういった圧縮ライブラリや暗号化ライブラリ、エンコーダーやデコーダーといったものはWebAssemblyで提供されることで高速に読み込んで動作するとか。
折を見てEmscriptenで何か変換したりしてもう少し動かしてる感のあることをやりたいですね。