Emscripten で Zopfli を移植した際のメモを残します。
思ったより簡単に使えましたが、知らないとハマることも結構多かったです。

導入

自分の環境(Mac)では以下のような感じでやれば OK でした。この辺りは情報が豊富なので適当です。

  • JS Engine は NodeJS だけで良いっぽいです
  • 必要な環境は homebrew 環境なら brew install llvm だけ?
  • あとは emscripten を clone するだけ
    • clang, clang++ の位置が llvm-link と違う場合はシンボリックリンクを張るなどして合わせる

使い方

C プログラムから JS へ変換

$ emcc *.c -o hoge.js

ライブラリの場合

通常だとリンク時最適化(LTO)によりエントリポイント(main関数)から到達可能な関数以外は省略されてしまいます。
そこで emcc のオプションに -s LINKABLE=1 をつけることでリンク時最適化を無効にし、すべての関数がそのまま変換されるようにします。
なお、この状態で -O2 なども併用可能みたいなので積極的に使った方が良いです。
不要な関数の削除などは Closure Compiler での最適化時に行うこととします。

JS から C プログラムへのデータの受け渡し

Emscripten の C の関数呼び出し用のメソッドを使う場合

Emscripten で変換したコードには ccallccallFunc というメソッドがグローバルスコープに作られます。
このメソッドは何をするかというと、配列や文字列などを C の関数に渡すのは面倒な手順が必要なんですが、それを引き受けてくれます。
(何が面倒かというと、Emscripten 側の JavaScript コードでは独自にメモリ管理を行っているので、JS側からそちらのメモリアロケートやコピーを行わなくては行けなかったり、スタックの管理などやることが結構煩雑だったりします。)

ccall の使い方

ccall では C の関数名がそのまま使えます。ただし、Closure Compiler などで関数名が変わると呼べなくなったりします。

var num = ccall("hoge", "number", ["array", "string", "number", "number"], [array, string, 1, 2]);

第一引数は関数名、第二引数は戻り値の型、第三引数は引数の型、第四引数は引数となります。
ちなみにここで型として "number" というのを使っていますが、実際に有効なのは "array", "string" だけでそれ以外なら何でも数値として扱われるようです。

ccallFunc の使い方

ccall, ccallFunc は第一引数以外は同じです。
ccallFunc では第一引数が function オブジェクトとなります。

var num = ccallFunc(_hoge, "number", ["array", "string", "number", "number"], [array, string, 1, 2]);

ファイルを使う場合

  • Uint8Array などを仮想ファイルとして登録する
    • FS.createDataFile 参照
  • C のプログラム側では fread などでファイルから読み込む

JS 側で使い終わったファイルは FS.deleteFile を忘れずに。

JS 実装例
// parent, name, properties, canRead, canWrite
FS.createDataFile('/', 'data', new Uint8Array([1, 2, 3, 4]), 1, void 0);
C 実装例
int main(int argc, char argv[]) {
  const char *filename = "/data";
  FILE *fp;
  unsigned char *buffer;

  fp = fopen(filename, "r");
  buffer = malloc(1024);
  fread(buffer, 1, 1024, fp);

  return 0;
}

Closure Compiler による最適化と Export

そのままだと実行効率もファイルサイズも良くないため、Closure Compiler で最適化を行います。

実行スコープの制限

生成された JavaScript ファイルはグローバルスコープにだだ漏れなので Closure Compiler の output_wrapper でスコープを制限する。

--output_wrapper="(function() {%output%})();"

ただし、このままだと全てスコープ制限されてしまうので、必要なものはグローバルスコープに明示的に出してやる必要があります。
今回は Export 用の js ファイルを作成することにしました。
Closure Library に乗っかるなら goog.exportSymbol, 乗っかりたくない場合は window に代入すれば良いです。

// closure library
goog.exportSymbol('Hoge', Hoge);

// vanilla js
window['Hoge'] = Hoge;

キャストされた値のポインタ参照によるメモリ境界の不具合

Emscripten ではヒープ(実態は ArrayBuffer )を HEAP8, HEAP32 などでアクセスできるようにしています。
例えば、C でポインタの参照演算子を使用すると、unsigned char * のときは HEAP8, unsigned int * の時は HEAP32 で値を取得します。
ここでお気づきの方もいるかとは思いますが、ここに問題があって unsigned int * の場合は HEAP32[address >> 2] としてアクセスされるため、address が 4 の倍数でない場合はうまく動作しません。(4 で割ったあまりが切り捨てられている)

Zopfli では LZ77(LZSS) の最長一致を探す際に、unsigned char * の入力データを size_t が 8 バイトの時は size_t * にキャストして 8 バイトずつ、unsigned int * が 4 バイトの時は unsigned int * にキャストして 4 バイトずつ比較して高速化を試みています。
zopfli.js で使用している Zopfli では、ここの処理が上記の問題に当たっていたため Zopfli のこの最適化を行わないように変更してあります。

emscripten_heap_access

なお、この不具合の起こるヒープへのアクセスは emcc-s SAFE_HEAP=1 オプションをつけることで検出できます。
自分はこのオプションを知らずに自力で調べて大変苦労しました。