binarien/binaryen.jsのwasm化やwasmのテキストフォーマット変更対応。- オレオレ言語を作る -

公開:2019-03-10 07:46
更新:2020-02-15 04:37
カテゴリ:オレオレ言語,node.js,js,wasm,binaryen

はじめに

少し前にwasm版 binaryen.jsを再ビルドしたときにオレオレ言語のコードジェネレータが動かなくなってハマった。原因はwasmのテキストフォーマットでのインストラクションの記述フォーマットが変わったためだった。

binaryen.js

オレオレ言語はwasmバイナリ出力をbinaryen.jsというライブラリに依存している。

binaryen.jsbinaryenをemscriptenでJSにトランスパイルしたものである。Pure JSなのでブラウザでも使うことができる。

オレオレ言語ではbinaryen.jsを使って以下のステップでオレオレ言語ソースをwasmバイナリ化している。

  1. オレオレ言語ソースをトークン化
  2. パーサで解析してオレオレ言語のASTを作る
  3. ASTを解析しbinaryen IRモジュールに変換する。
  4. binaryen IRモジュールを最適化する
  5. binaryen IRモジュールからwasmバイナリを生成する

3.から5.までの処理はほぼbinaryen.jsの機能に頼っている。binaryen IRまで持ち込めば、最適化からバイナリ出力まではbinaryen.jsがやってくれるので、簡単にwasmバイナリを吐く言語を作ることができる。

binaryen.jsのwasmモジュール化

binaryen.jsはbinaryenをemscriptenでJSにトランスパイルしたものである。しかしemscriptenの現バージョンはc/c++ソースを直接wasmモジュールとして出力することが可能となっている。binaryen.jsもビルド・スクリプトにwasm化するためのパラメータが記載されているが、若干問題がありコメントアウトされている。

https://github.com/AssemblyScript/binaryen.js/blob/master/scripts/build.js#L140

// NOT YET FUNCTIONAL, but possible. Requires some changes to post.js

// console.log("\nCompiling binaryen-wasm.js ...\n");
// compileJs({
//   post: path.join(sourceDirectory, "js", "binaryen.js-post.js"),
//   out: "binaryen-wasm.js"
// });

まずコメントアウトされているソースコードに誤りがある。compileJS()ではなくcompileWasm()が正しい。 そしてcompileWasm()は以下にある。

https://github.com/AssemblyScript/binaryen.js/blob/master/scripts/build.js#L109

/** Compiles the WebAssembly target. */
function compileWasm(options) {
  run("python", [
    path.join(emscriptenDirectory, "em++"),
    "shared.bc"
  ].concat(commonOptions).concat([
    "--post-js", options.post,
    "--closure", "1",
    "-s", "EXPORTED_FUNCTIONS=[" + exportedFunctionsArg + "]",
    "-s", "ALLOW_MEMORY_GROWTH=1",
    "-s", "BINARYEN=1",
    "-s", "BINARYEN_METHOD=\"native-wasm\"",
    "-s", "MODULARIZE_INSTANCE=1",
    "-s", "EXPORT_NAME=\"Binaryen\"",
    "-o", options.out,
    "-Oz"
  ]));
}

上記を修正し、さらにbinaryen.jsのサポートコードを少しいじればwasm化することができるのではないかと思って取り組んだ結果、とりあえずwasm化することができた。その内容は以下に書いた。

binaryenをwasmでコンパイルしなおす

でこれを使ってオレオレ言語を作ってたのだけれども、なぜか2018年の12月くらいに最新のbinaryen.jsでビルドしなおすとコードジェネレータが動かなくなってしまう不具合が発生した。

原因

動かなくなった原因はbinaryen.jsにあるのではなく、wasmのインストラクション記法に変更があったからだった。 wasmにはwasmのS式をwasmモジュールにする機能があるので、以下のwasmモジュールをソースコードで読み込んでコードジェネレータ実行時にwasmバイナリ化していたが、このコードがbinaryen.jsでバイナリ化できなくなってしまったのである。

(module
  (export "i32tof32" $i32tof32)
  (export "i64tof64" $i64tof64)
  (memory $memory 1)
  (export "memory" (memory $memory))

  ;; IEE754 float32のビットパターンを持つ32ビット整数値をf32に変換する
  (func $i32tof32 (param $i i32) (param $minus i32) (result f32)
    (f32.reinterpret/i32
      (i32.xor
          (get_local $i)
          (get_local $minus)
      )
    )
  )
  ;; IEEE754 float64のビットパターンを持つ2つの32ビット値(high,low)を元にして、64bit floatを返す
  (func $i64tof64 (param $low i32) (param $high i32) (param $minus i32) (result f64)
    (f64.reinterpret/i64
      (i64.xor
        (i64.or
          (i64.shl 
            (i64.extend_u(get_local $high))
            (i64.const 32) 
          )
          (i64.extend_u (get_local $low))
        )
        (i64.shl
          (i64.extend_u(get_local $minus))
          (i64.const 32)
        )
      )
    )
  )
)

エラーを追っていくと、インストラクションの記法が2018年の終わりから2019年頭にかけて変わったことを知った。

https://github.com/WebAssembly/spec/issues/884#issuecomment-426433329

これに基づいてソースコードを修正するとコードジェネレータは動くようになった。ちなみに修正後のコードはこれ。

(module
  (export "i32tof32" (func $i32tof32))
  (export "i64tof64" (func $i64tof64))
  (export "i64Neg" (func $i64Neg))
  (memory $memory 1)
  (export "memory" (memory $memory))

  ;; IEE754 float32のビットパターンを持つ32ビット整数値をf32に変換する
  (func $i32tof32 (param $i i32) (param $minus i32) (result f32)
    (f32.reinterpret_i32
      (i32.xor
          (local.get $i)
          (local.get $minus)
      )
    )
  )

  ;; IEEE754 float64のビットパターンを持つ2つの32ビット値(high,low)を元にして、64bit floatを返す
  (func $i64tof64 (param $low i32) (param $high i32) (param $minus i32) (result f64)
    (f64.reinterpret_i64
      (i64.xor
        (i64.or
          (i64.shl 
            (i64.extend_i32_u (local.get $high))
            (i64.const 32) 
          )
          (i64.extend_i32_u (local.get $low))
        )
        (i64.shl
          (i64.extend_i32_u (local.get $minus))
          (i64.const 32)
        )
      )
    )
  )

  ;; 整数値の2の補数をとる
  (func $i64Neg (param $low i32) (param $high i32)
    (i64.store
      (i32.const 0)
      (i64.add
        (i64.xor
          (i64.or 
            (i64.extend_i32_u (local.get $low))
            (i64.shl 
              (i64.extend_i32_u (local.get $high))
              (i64.const 32) 
            )
          )
          (i64.const 0xffffffffffffffff)
        )
        (i64.const 1)
      )
    )
  )
)

確かにまあ、変更前は'\/'がインストラクションに入ってるのが違和感あったもんなあ。。

ちなみに

今回Textフォーマットをbinaryen.jsでバイナリ化したけど、wabt(WebAssembly Binary Toolkit)にあるwat2wasmでもバイナリにすることができる。

binaryen.jsだとJS中でテキストフォーマットをバイナリにすることができる。wat2wasmだとS式だけではなくてリニア形式でもバイナリ化できたと思う。