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 で変換したコードには ccall
と ccallFunc
というメソッドがグローバルスコープに作られます。
このメソッドは何をするかというと、配列や文字列などを 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 のこの最適化を行わないように変更してあります。
なお、この不具合の起こるヒープへのアクセスは emcc
で -s SAFE_HEAP=1
オプションをつけることで検出できます。
自分はこのオプションを知らずに自力で調べて大変苦労しました。
found you by accident, while I was browsing on Bing for something else, Nonetheless I am here now and would just
like to say kudos for a incredible post and a all
round interesting blog (I also love the theme/design), I don’t have time to browse it all at the moment but I have saved it and also added your RSS feeds, so when I have time I will be back to read more, Please do keep up
the excellent b.