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

公開:2018-05-04 06:07
更新:2018-05-04 07:27
カテゴリ:オレオレ言語を作る,binaryen,ゲーム製作,HTML5,ES6,JS,WebGL2.0,WebAssembly,言語,node.js

今作っているオレオレ言語はbinaryen.jsというライブラリでwasmバイナリを出力している。これを使うことによってJS上でwasmオペコードを編集・バイナリ出力・テキスト形式出力などが簡単にできる。

https://github.com/AssemblyScript/binaryen.js

binaryen.jsはbinaryenemscriptenでコンパイルしたjsファイルである。これを今回wasm化してみた。動機はbinaryen.jsのビルドスクリプトにwasmへの出力コードが含まれていたのを発見したからである。

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

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は2.3Mbくらいある結構大きいモジュールだし、wasm化すればサイズを小さくできるのではないかということで、emscriptenをインストールしてビルドしたのである。emscriptenによるコンパイルはWSLで実施した。

ただこのままcompileWasm()を使ってビルドしようとしてもうまくいかなかった。コンパイル自体はできるが実際に動かしてみるとbinaryenをimportするところでエラーが発生し、動かない。

原因を探ると以下であった。

emscriptenでwasmをビルドすると、サポートJSファイルとwasm本体のセットでビルドされる。利用はサポートJSを経由して行うのであるが、このJSファイル中でwasmのインスタンス化を実施して、JSから利用しやすいようなラッパーを被せている。emscriptenのSIDE_MODULESオプションを使えばサポートJS出力なしでビルドできるようだが。

binaryenの場合はさらに--post-jsオプションでヘルパー機能をJSで追加している。でこの--post-jsオプションでくっつけているJSの中で問題が発生していた。wasmモジュールに依存するコードがwasmのインスタンス化を待たずに実行していて、そこで落ちていた。wasmモジュールのインスタンス化は非同期で行われPromiseを返す仕様となっている。このPromiseチェーンが途中で切れていて、Promiseがdoneする前に依存コードの実行が始まっていたのである。

どうしたらよいかいろいろ調べると、emscriptenのModule ObjectonRuntimeInitializedプロパティがあるのを見つけた。これはモジュールの実行準備が完了されるとこのプロパティにセットされた関数を呼び出すというものである。ここでwasmモジュールに依存するコードを実行するようにすれば問題は解消する。

そこで--post-jsで指定されているjsファイルを修正し、関数でくるんで(postSetup())、すぐに実行しないようにした。そして--pre-js用のファイルを新たに作成して以下のコードを組み込んだ。

var old = Module['onRuntimeInitialized'];
Module['onRuntimeInitialized'] = function(){
  postSetup();
  old && old();
};

こうすることで、このエラー自体は回避されたが、実際に利用するうえでまだ問題があった。上記対策ではライブラリ中のエラーは回避され、返されるオブジェクトはPromise-Likeであるので、thenメソッドによって準備完了を知ることができる。


import binaryen from 'binaryen';

binaryen.then((m)=>{
  // DO SOMTHING ..
});

実際には


import binaryen from 'binaryen';

export async function doSomething(){
  await binaryen;
  //  doSomething ...;
}

とこうしたいところなのだが、Promise-Likeのせいかなんなのかわからないが、awaitのところで無限ループとなってしまい動作しない。

しょうがないので、onRuntimeInitializedをフックしようかなと思ったが、このままでは利用側でフックできないことが分かった。 binaryen.jsはMODULARIZE_INSTANCE=1でビルドしており、これでビルドすると以下のようなコードを出力する。

var Binaryen = function(Binaryen) {
  Binaryen = Binaryen || {};
(略)
  var Module; 
  if (!Module) Module = typeof Binaryen !== 'undefined' ? Binaryen : {};
(略)
 return Binaryen;
}();

(BinaryenをエクスポートするUMDコードがここに入る。。)

上のBinaryen関数の引数に{onRuntimeInitialized:(関数)}というオブジェクトをセットして呼び出せばフックできるのだが、上述のようにBinaryen関数は内部で引数なしで実行されてしまうためフックできないのである。これを回避するオプションがないかと探すと、MODULARIZEというオプションがあり、これに1をセットすると関数を実行せず、関数そのものをエクスポートするようになることがわかった。

よってこのオプションを使ってビルドしなおし、利用側は以下のようなコードを書くことで動作させることに成功した。

import binaryen_ from './binaryen-wasm.mjs';

export default async function generateCode(ast) {
  let binaryen;
  await new Promise((resolve,reject)=>{
    binaryen = binaryen_({onRuntimeInitialized:m=>{
     resolve();
    }});
  });
  .
  .
  .

なんかちょっとあれであるが。。

修正したコードは以下である。

https://github.com/sfpgmr/binaryen.js

https://github.com/sfpgmr/binaryen

コードサイズは1.2Mくらいになった。そしてオレオレ言語検証用ページに組み込んでみた。

https://sfpgmr.github.io/sgl2/

レポジトリ:
https://github.com/sfpgmr/sgl2