WebAssembly を使って自作言語をブラウザで動かしてみよう

今日 Google の開発者ブログで WebAssembly の記事が載っていました.どうやら最新の Chrome では WebAssembly が動くようです.

googledevjp.blogspot.jp

自作言語のコンパイラLLVM フロントエンドとしてつくっているので,これは試さないわけにはいきません.

github.com

というわけで,さっそく試してみます.

準備

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

binaryenLLVM IR などを WebAssembly 形式に落とすコンパイラです.適当に clone してきて cmakemake でビルドすると bin ディレクトリ内にいくつかのコマンドが生成されます.

4. Dachs

誰も試さないと思いますが一応… clone してきて cmakemake すると 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 名).

sqrt2.ll

さらに llc を使ってアセンブラ形式に落とします.

$ /usr/local/opt/llvm/bin/llc sqrt2.ll -march=wasm32
$ cat sqrt2.s

sqrt2.s

これで準備ができました.binaryen を使ってアセンブリを wasm32 形式にコンパイルします.まずは WebAssembly の AST をテキスト+S式で表現した wast 形式にコンパイルします.

$ s2wasm sqrt2.s > sqrt2.wast

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);

    const result = module.exports.sqrt(314);

    document.getElementById('test').innerText = `Result: ${result}`;
  </script>
</html>

index.html

最後に Canary 版の Chrome で index.html を開いてみます.

f:id:rhysd:20160324235304p:plain

画像内の DevTools のコンソールにあるように,Wasm.instantiateModule を使って JavaScript で実行できるモジュールに変換できます.今回はニュートン法平方根が期待通り計算できていることが分かりました.

まとめ

自作言語がブラウザ上で動いているのはちょうたのしい.

  • 追記

勢い余って Vim の wast filetype 対応プラグインつくりました.

github.com