Node.js
LLVM
wasm

Node.js でつくる Node.js ミニコンパイラ - Extra01 : WebAssembry 化

はじめに

LLVMを使ったNode.jsミニコンパイラ作りはひと段落しましたが、やってみたいことの一つにWebAssembry(WASM)への変換がありました。

  • ミニNode.js → LLVM IR → WASM

そこでWebAssembry(WASM)入門もかねて、emscriptenを使った変換にチャレンジしてみます。

emscrpiten の導入

インストールはこちらのドキュメントに従って行います。

Mac OS X の場合

私は Mac OS X 10.12 Sierra だったので、次の様にインストールしました。

$ git clone https://github.com/juj/emsdk.git
$ cd emsdk
$ git pull
$ ./emsdk install latest
$ ./emsdk activate latest

利用するには、パスを通してあげる必要があります。一時的な利用であれば、次のようにすれば利用できます

$ source ./emsdk_env.sh

前提環境

Mac OS X にインストールする前提環境はこちらにある通りです。

  • Python V2.7.12 以上 ... システムデフォルトは V2.7.10 だったので、そのままではインストールできませんでした。今回は V3.6.0 を使いました
  • git ... Xcodeのコマンドラインツール一式でインストールできます
  • cmake
  • Node.js

WASMの生成

Node.js でつくる Node.js ミニコンパイラ - 11 : FizzBuzzとフィボナッチ数列 で作ったFizzBuzzのサンプルで試してみましょう。

LLVM IR の生成

$ node mininode_compiler11.js sample/fizzbuzz_func.js

これで、generated.ll が生成されます。

WASM, HTML, JSの生成

emascriptenにパスを通してから、次の様にHTML出力を指定して変換します。

$ emcc -o fizzbuzz.html generated.ll

これで次の3つのファイルが生成されます。

  • fizzbuzz.html
  • fizzbuzz.js
  • fizzbuzz.wasm

WebAssamblyのバイナリは .wasm ファイルですが、それを読み込んで結果を表示するための .html と .js ファイルも一緒に生成されます。

WASM (WebAssembly) の実行

上記で生成した HTML ファイルをブラウザで読み込みませれば、実行結果がブラウザに表示されます(コンソール出力がエミュレートされます)。

Chrome69_wasm.png

この時、ブラウザによって挙動が違いました。Firefoxのみ、直接htmlを開いても実行できます。

  • Chrome 69 ... file://〜 で直接HTMLを開くと、wasmのロードでエラー。Webサーバーに配置して http://〜 で読み込ませればOK
  • FireFox 62 ... file://〜 で直接HTMLを開いても実行可能。http://〜 でももちろんOK
  • Safari 11 ... file://〜 で直接HTMLを開くと、wasmのロードでエラー。Webサーバーに配置して http://〜 で読み込ませればOK

生成結果を覗いてみる

HTMLとJS

長大なHTMLが生成されるので、中身の理解はできていません。が、次のあたりがポイントになりそうです。

    <script type='text/javascript'>
      // ... 省略 ...
      var Module = {
        preRun: [],
        postRun: [],
        print: (function() {
          // ... 省略 ...
        })(),
        printErr: function(text) {
          // ... 省略 ...
        },
        canvas: (function() {
          var canvas = document.getElementById('canvas');
          // ... 省略 ...
          return canvas;
        })(),
        setStatus: function(text) {
          // ... 省略 ...
          statusElement.innerHTML = text;
        },
        totalDependencies: 0,
        monitorRunDependencies: function(left) {
          this.totalDependencies = Math.max(this.totalDependencies, left);
          Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
        }
      };
      // ... 省略 ...
    </script>
    <script async type="text/javascript" src="fizzbuzz.js"></script>

Module に print() などのJavaScriptの関数を用意していて、これをWASM側から呼び出すことで画面への出力を実現していると考えていています。
また、実際にWASMを呼び出す部分は、fizzbuzz.js に書かれているようです。こちらも長大で内容は理解できていません。

WASM

テキスト形式(WAT)に変換

.wasm はバイナリなので、直接中身を理解するのは難しいです。そこでアセンブラのようなテキセスト形式に変換します。
変換には binaryen のツールを利用しますが、emscrpiten 導入時に一緒にインストールされています。

  • emsdkのディレクトリ/clang/e1.38.11_64bit/binaryen/bin/wasm-dis

これを用いて、テキスト形式(WAT)に変換します。

$ emsdkのディレクトリ/clang/e1.38.11_64bit/binaryen/bin/wasm-dis fizzbuzz.wasm > fizzbuzz.wat

ループ部分

変換された .wat ファイルも長大なので、全体の内容は理解できていません。しかし、コンパイル前のjsファイルと見比べると、次のループ処理が対応しているようです。

fizzbuzz_func.js抜粋
while (i <= 100) {
  ret = fizzbuzz(i)
  i = i + 1;
}
.wat抜粋
  (loop $label$2
   (block $label$3
    (set_local $var$7
     (get_local $var$0)
    )
    (set_local $var$8
     (i32.const 100)
    )
    (set_local $var$9
     (i32.le_s
      (get_local $var$7)
      (get_local $var$8)
     )
    )
    (if
     (i32.eqz
      (get_local $var$9)
     )
     (br $label$3)
    )
    (set_local $var$10
     (get_local $var$0)
    )
    (set_local $var$11
     (call $8
      (get_local $var$10)
     )
    )
    (set_local $var$6
     (get_local $var$11)
    )
    (set_local $var$1
     (get_local $var$0)
    )
    (set_local $var$2
     (i32.const 1)
    )
    (set_local $var$3
     (i32.add
      (get_local $var$1)
      (get_local $var$2)
     )
    )
    (set_local $var$0
     (get_local $var$3)
    )
    (br $label$2)
   )
  )

ユーザ定義関数部分

.watのループ処理の中を見ると、 call が使われているところがあります。

     (call $8
      (get_local $var$10)
     )

おそらく、これが関数呼び出しでしょう。となると、$8 というラベルがついた部分が、ユーザ定義関数になっているハズです。

fizzbuzz_func.js抜粋
function fizzbuzz(n) {
  if (n % (3*5) === 0) {
    puts('FizzBuzz');
    return 15;
  }
  else if (n % 3 === 0) {
    puts('Fizz');
    return 3;
  }
  else if (n % 5 === 0) {
    puts('Buzz');
    return 5;
  }
  else {
    putn(n);
    return n;
  }
}

.watの中身を眺めて見ると、 3*5 や 3, 5 で割った余りを計算している箇所が見かけられます。

.wat抜粋
 (func $8 (; 22 ;) (type $1) (param $var$0 i32) (result i32)
  (local $var$1 i32)
  (local $var$2 i32)
  ;;  ... 省略 ....
  (local $var$24 i32)
  (local $var$25 i32)
  (set_local $var$25
   (get_global $global$3)
  )
  (set_global $global$3
   (i32.add
    (get_global $global$3)
    (i32.const 16)
   )
  )
  (if
   (i32.ge_s
    (get_global $global$3)
    (get_global $global$4)
   )
   (call $fimport$14
    (i32.const 16)
   )
  )
  (set_local $var$1
   (get_local $var$0)
  )
  (set_local $var$14
   (get_local $var$1)
  )
  (set_local $var$18
   (i32.const 3)  ;; 定数3
  )
  (set_local $var$19
   (i32.const 5)  ;; 定数5
  )
  (set_local $var$20
   (i32.mul ;; 3 * 5
    (get_local $var$18)
    (get_local $var$19)
   )
  )
  (set_local $var$21
   (i32.and
    (i32.rem_s  ;; 3*5 で割った余りを計算
     (get_local $var$14)
     (get_local $var$20)
    )
    (i32.const -1)
   )
  )
  (set_local $var$22
   (i32.const 0)
  )
  (set_local $var$23
   (i32.eq
    (get_local $var$21)
    (get_local $var$22)
   )
  )
  (if
   (get_local $var$23)
   (block
    (drop
     (call $59
      (i32.const 3784)
     )
    )
    (set_local $var$2
     (i32.const 15)
    )
    (set_global $global$3
     (get_local $var$25)
    )
    (return
     (get_local $var$2)
    )
   )
  )
  (set_local $var$3
   (get_local $var$1)
  )
  (set_local $var$4
   (i32.const 3) ;; 定数3
  )
  (set_local $var$5
   (i32.and
    (i32.rem_s ;; 3で割った余りを計算
     (get_local $var$3)
     (get_local $var$4)
    )
    (i32.const -1)
   )
  )
  (set_local $var$6
   (i32.const 0)
  )
  (set_local $var$7
   (i32.eq
    (get_local $var$5)
    (get_local $var$6)
   )
  )
  (if
   (get_local $var$7)
   (block
    (drop
     (call $59
      (i32.const 3793)
     )
    )
    (set_local $var$8
     (i32.const 3)
    )
    (set_global $global$3
     (get_local $var$25)
    )
    (return
     (get_local $var$8)
    )
   )
  )
  (set_local $var$9
   (get_local $var$1)
  )
  (set_local $var$10
   (i32.const 5) ;; 定数5
  )
  (set_local $var$11
   (i32.and
    (i32.rem_s   ;; 5で割った余りを計算
     (get_local $var$9)
     (get_local $var$10)
    )
    (i32.const -1)
   )
  )
  (set_local $var$12
   (i32.const 0)
  )
  (set_local $var$13
   (i32.eq
    (get_local $var$11)
    (get_local $var$12)
   )
  )
  (if
   (get_local $var$13)
   (block
    (drop
     (call $59
      (i32.const 3798)
     )
    )
    (set_local $var$15
     (i32.const 5)
    )
    (set_global $global$3
     (get_local $var$25)
    )
    (return
     (get_local $var$15)
    )
   )
   (block
    (set_local $var$16
     (get_local $var$1)
    )
    (call $9
     (get_local $var$16)
    )
    (set_local $var$17
     (get_local $var$1)
    )
    (set_global $global$3
     (get_local $var$25)
    )
    (return
     (get_local $var$17)
    )
   )
  )
 )

終わりに

JavaScript から WebAssembly (.wasm) への直接変換は、現在はサポートされていません。 C言語だったり、Rustだったり、Goだったりを使う必要があります。
今回ミニNode.jsコンパイラを使ってLLVM IRを生成することで、.wasmに変換することができました。
これを足がかりに、WebAssemblyの世界の覗いてみたいところです。