今日 Google の開発者ブログで WebAssembly の記事が載っていました.どうやら最新の Chrome では WebAssembly が動くようです.
自作言語のコンパイラを LLVM フロントエンドとしてつくっているので,これは試さないわけにはいきません.
というわけで,さっそく試してみます.
準備
1. Chrome
直接 V8 をビルドするのは億劫なので Chrome のバイナリを落としてきて使います.Chrome 51.0.2677.0 以降であれば OK です.Canary 版をダウンロードしてきてインストールします.
次に chrome:flags
にアクセスして WebAssembly を有効にします.
2. LLVM
WebAssembly のためのアセンブリを吐くには LLVM の experimental な WebAssembly backend を有効にしてビルドする必要があります.Homebrew のフォーミュラをいじって使います.
$ brew edit llvm # ここで cmake の引数に -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly を追加 # brew install llvm --HEAD
3. binaryen
binaryen は LLVM IR などを WebAssembly 形式に落とすコンパイラです.適当に clone してきて cmake
→ make
でビルドすると bin
ディレクトリ内にいくつかのコマンドが生成されます.
4. Dachs
誰も試さないと思いますが一応… clone してきて cmake
→ make
すると dachs
というバイナリができているはずです.
いざ試してみる
Dachs は Boehm GC を使って配列を割り当てています.本当は BrainFxxk のサンプルを動かしたいなぁと思っていたのですが,バッファを static に取るとうまく動かなくなったので諦めました.
今回はシンプルな sqrt2.dcs を使います.
func abs(n) ret if n > 0.0 then n else -n end end func sqrt'(p, z, x) ret z if abs(p-z) < 0.00001 ret sqrt'(z, z-(z*z-x)/(2.0*z), x) end func sqrt(x) ret sqrt'(0.0, x, x) end func main print(sqrt(10.0)) end
まずはコンパイルします.アセンブリをそのまま吐くオプションは無いので,まずは LLVM IR にコンパイルします.
$ dachs sqrt2.dcs --emit-llvm > sqrt2.ll
ここで少し sqrt2.ll を修正します.
- Dachs は単体で動作する汎用言語のため
main
関数がある前提ですが,WebAssembly は LLVM IR の module をコンパイルして生成するため不要な エントリポイントであるmain
関数を削除します. - mangling をサボっているので Dachs のコンパイラが吐く LLVM IR の関数名がひどくそのままでは使えないので少し関数名を修正します(C と同じ mangle 名).
さらに llc
を使ってアセンブラ形式に落とします.
$ /usr/local/opt/llvm/bin/llc sqrt2.ll -march=wasm32 $ cat sqrt2.s
これで準備ができました.binaryen を使って LLVM IR を wasm32 形式にコンパイルします.まずは WebAssembly の AST をテキスト+S式で表現した wast 形式にコンパイルします.
$ s2wasm sqrt2.s > sqrt2.wast
中を見てみるとS式形式に変換された LLVM IR みたいなものが吐き出されているのが分かります.
(module (memory 1) (export "memory" memory) (export "abs" $abs) (export "sqrt2" $sqrt2) (export "sqrt" $sqrt) (func $abs (param $$0 f64) (result f64) (block $label$0 (br_if $label$0 (i32.or (f64.gt (get_local $$0) (f64.const 0) ) (f64.ne (get_local $$0) (get_local $$0) ;; ...以下略
次にこのS式をシリアライズしたバイナリフォーマット wasm に変換します.ここで最初は llvm-as
を使ってコンパイルしてみたのですがうまくいきませんでした.ちょっと探してみると先人の知恵があったので,sexpr-wasm
を使うとうまくいくと分かりました.
$ sexpr-wasm sqrt2.wast -o sqrt2.wasm
0000000: 0061 736d 0a00 0000 140a 7369 676e 6174 .asm......signat 0000010: 7572 6573 0201 0404 0304 0404 0418 1366 ures...........f 0000020: 756e 6374 696f 6e5f 7369 676e 6174 7572 unction_signatur 0000030: 6573 0300 0100 0a06 6d65 6d6f 7279 0101 es......memory.. 0000040: 0120 0c65 7870 6f72 745f 7461 626c 6503 . .export_table. 0000050: 0003 6162 7301 0573 7172 7432 0204 7371 ..abs..sqrt2..sq 0000060: 7274 7e0f 6675 6e63 7469 6f6e 5f62 6f64 rt~.function_bod 0000070: 6965 7303 2000 0102 0700 0048 9b0e 000c ies. ......H.... 0000080: 0000 0000 0000 0000 980e 000e 000f 0090 ................ 0000090: 0e00 140e 0039 0001 0207 0000 9c12 008a .....9.......... 00000a0: 0e00 0e01 0cf1 68e3 88b5 f8e4 3e14 0e01 ......h.....>... 00000b0: 1412 010e 0189 0e01 8c8a 8b0e 010e 010e ................ 00000c0: 028b 0e01 0c00 0000 0000 0000 c00e 0211 ................ 00000d0: 0014 1201 0c00 0000 0000 0000 000e 000e ................ 00000e0: 00
あとはこれを適当に Uint8Array
に突っ込む JavaScript のコードを書き,HTML ファイルを書きます.
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes" /> </head> <body> <div id="test"></div> </body> <script> const buf = new Uint8Array([0x00, 0x61, 0x73, 0x6d, ...]); const module = window.Wasm.instantiateModule(buf); console.log(module); document.getElementById('test').innerText = `Result: ${module.exports.sqrt(314)}`; </script> </html>
最後に Canary 版の Chrome で index.html を開いてみます.
画像内の DevTools のコンソールにあるように,Wasm.instantiateModule
を使って JavaScript で実行できるモジュールに変換できます.今回はニュートン法で平方根が期待通り計算できていることが分かりました.
まとめ
自作言語がブラウザ上で動いているのはちょうたのしい.