emscripten のドキュメントを読んだメモ
丸2年前に書いたものを掘り出してきただけなので、(asm.js が出力される話など)古い部分が多々あると思われます。
でも少しは役に立つかなと思ったので、しばらくは残しておく。
見返すと良さそうなリンク
豆知識
1 か 0 をとる -s で指定できるオプションに NO_ をつけると 1 と 0 を逆にできる
-s NODERAWFS=1 でデフォで NODEFS を使うようになり、C/C++ 製のライブラリを Node で動かすとき便利
CLI ツールの場合は -s EXIT_RUNTIME=1 と -s DISABLE_EXCEPTION_CATCHING=0 も指定しておくと見栄えがいい
Introducing Emscripten
C/C++ を始め、LLVM ビットコードに変換できるコードを JS にコンパイルできる
デフォルトでは asm.js という最適化された JS のサブセットが出力される
最近は asm.js の代わりに WASM を吐くようになった
emscripten は(C/C++ に関しては)Clang を使って LLVM ビットコードを出力する
このビットコードを emscripten のコアコンパイラが JS に変換する
出力形式は Node.js 向けの JS と、ブラウザ向けの HTML が選べる
HTML と言っても <script async src="main.js"></script> くらいなので本体は JS
もちろん C/C++ コードをそのまま JS に変換できるとは限らない
入出力やファイルシステム、GUI アプリがやるような無限ループは問題になりやすいらしい
Getting Started
emsdk は emscripten まわりのツールを操作する小さなパッケージマネージャで、これを入れるのが普通 プラットフォームによって入るものが変わってくる、Linux は自分で入れるものが多め
emscripten は基本的に emcc か em++ で呼び出す
つまり emcc での出力は LLVM ビットコードか JS か HTML で、これは -o で決める出力ファイル名の拡張子で指定できる
WASM が出力されるかどうかは Node のバージョンによるらしい
ファイル周り はブラウザに file の API が無いこともあり独特で、ここは難易度が高め -O による最適化は JS にも影響する
Compiling and Running Projects
Building Projects
基本的には ./configure の代わりに emconfigure ./configure で Makefile を作る
その後、 make の代わりに emmake make を実行する
CMake の場合は emconfigure cmake .
eemconfigure は gcc を emcc に置き換えたような Makefile を作る
emmake make は *.a や *.so, *.o, *.bc でリンクされた LLVM ビットコード(bc)を生成する
*.so, *.bc, *.o は名前が違うだけで同じ LLVM ビットコード
*.a だけは制限あるらしく、可能なら *.so を使ったほうがいいようだ
C/C++ のライブラリを JS のライブラリに変換するなら、まず上記のようにライブラリをコンパイルし、ライブラリのメインの bc に対して emcc libx.so -o libx.js のようにする
ところで emscripten はライブラリをビルドする際、それが依存している他のライブラリへの動的なリンクを無視する
このため最後の JS への変換では、間接的に依存しているライブラリの bc も列挙しなければならない
つまり例えば libx.so が内部で y と z を使っているなら、まず3つともをビルドし、 emcc libx.so liby.so libz.so -o libx.js のようにしないと libx の JS 向けの実装が得れない
C/C++ のライブラリを使う C/C++ のアプリを JS に変換するなら、まずライブラリをコンパイルし、これと同様に emcc main.c -I x/include -o main.bc でアプリの bc を作り、 emcc main.bc libx.so liby.so lubz.so -o main.js のようにリンクする
libc や libc++ は emscripten 用のものが勝手にリンクされるため、これらまで予め bc にしておく必要はない
JS を吐くときは、自分のコードの bc とライブラリの bc を指定するのが普通だが、先にこれらを単一の bc にしてから JS に変換することもできる
emcc project.bc libstuff.bc -o final.html は emcc project.bc libstuff.bc -o allproject.bc && emcc allproject.bc -o final.html と同じということ
一覧は emcc --show-ports で確認できる
emscripten の最適化は、ソースコードを bc に変換するときと、bc を JS や WASM に変換するときの2段階ある
両方の段階で同じ最適化フラグ(-O3 や -Os など)やコンパイルオプションを指定するのが良いとされている
ちなみに -O3 は実行時間の最適化で、 -Os はサイズの最適化
デバッグする場合も、JS の出力時だけでなく bc にコンパイルする際にも -g4 などを指定しないと完全な情報は得られない
Building to WebAssembly
WASM は Firefox 52, Chrome 57, Opera 44 以上でサポートされている
Edge も15以上ではサポートされているが、Experimental JavaScript Featuresフラグを有効にする必要がある
emscripten は Binaryen の asm2wasm を使って asm.js を経由して WASM を得ているらしい 将来的には WASM 用の LLVM バックエンドを使う予定らしい
WASM でなく asm.js での出力が欲しい場合は最終出力時に -s WASM=0 を付ければよい
-s BINARYEN_METHOD='native-wasm,asmjs' のようにすると WASM が使えないなら asm.js を使うコードが吐かれるが、形式の違いから最適化はかからないらしい
最適化をかけたいなら、別々にビルドし、ユーザの環境を識別してどちらを使うか決めるコードを自分で書く
WASM は基本的に JS と連携して使うものであり、スタンドアローンで実行するのは難しいが方法はあるとのこと
CLI ツール向けの話?
デバッグはソースマップがあったりコードを簡単にいじれるので asm.js のほうが楽らしい
WASM は asm.js と違いゼロ除算や巨大な浮動小数点数の整数への丸めなどを例外として投げ、これを trap と呼ぶ
trap が起きるモードは allow と呼ばれ、値を丸めて trap を回避する clamp や、 JS と全く同様に trap する js といったモードもある
これは -s BINARYEN_TRAP_MODE='clamp' のように指定できる
デフォルトは allow で、これが一番オーバーヘッドが少ないため理由がないなら変更すべきでないとのこと
asm.js との結果の比較やデバッグには js を指定すると便利
emscripten が吐く WASM は32bitだが、asm.js は64bitがデフォルトなので -s PRECISE_F32=1 で32bitに合わせる必要もある
asm.js は JS のサブセットなのでメインの JS コードと結合できるが、WASM はバイナリなので必ず別で配布する必要がある
WASM に定義された関数は読み込まれるまで呼び出せないので、JS からはコールバックで非同期的に呼び出す形になる
サーバー側でMIME タイプを application/wasm に設定できれば、ダウンロード中に解析が行われるため早く実行できる
余談だが、asm.js が JS のサブセットなのに速いのは、以下のように JS で表現可能な範囲で負荷を減らしているため
ブラウザが asm.js を特別に扱う
先頭を見て AOT コンパイルし、アセンブリ言語にする
| 0 などの型指定子による AOT コンパイルとアセンブリの高速化
配列も要素の型が同じもののみ
型検査や型変換が不要になる
素の JS では全ての数値は浮動小数点数だが、 | 0 つきの値を整数としてアセンブリに変換する
UI 操作や GCなどを捨て、計算に必要な機能のみに絞った単純な体系なため、最適化をかけやすい
配列をヒープに見立てて扱う
WASM は JS をやめてバイナリになったため表現の制約がなくなったため asm.js より高速
並列に解析できるよう設計されているため、コンパイル自体も速い
Running HTML files with emrun
emrun は出力した HTML などをローカルサーバーでホストするコマンド
file:// から始まる URL では CORS に引っかかって実行できないときなどに使える
python3 -m http.server でホストしてもいいが、こちらのほうができることが多いらしい
具体的には以下のような機能がある
stdout と stderr ストリームを拾い、ターミナルやファイルに繋いで出力できる
最後に JS を出力する際に --emrun をつける必要はある
ホストする際にコマンドライン引数を渡せる
adb 経由で PC に接続された Android のブラウザで動かすこともできる
内部的には Python2 の SimpleHTTPServer を使っているらしい
--browser を使うと、 --list_browsers で出る一覧にあるものかフルパスで、起動するブラウザを指定できる
その他のオプションはここに載っている
Android のブラウザでの実行は、adb が通常通り動いていれば --android --browser chrome をつけるだけ
Building projects on Travis CI
少し前のイメージでは -u emscripten は不要
Deploying Emscripten Compiled Pages
emscripten による生成物を実用する際の、WASM などをロードしたりする部分のカスタマイズや最適化などについて
emscripten の出力は、WASM や asm.js など C/C++ をコンパイルした部分と、これを扱うための JS の二部構成になっている
具体的には -o out.html をしたとき、 out.wasm が C/C++ のコンパイル結果で、out.js がランタイム
-s WASM=0 で asm.js を出力する場合は、静的なメモリセクションが記述された out.mem も生成される
この部分は WASM だと out.wasm に埋め込まれている
emscripten のファイル関連の API を使っている場合は out.data も生成され、これもランタイムの JS によって読み込まれる
HTML を出力する場合は --shell-file で <script async src="out.js"></script> を埋め込むファイルを指定できる
埋め込む位置は {{{ SCRIPT }}} で指定する
デフォルトでは asm.js はランタイムの JS と結合された状態で出力されるが、 --separate-asm ででばらばらに出力できる
ブラウザ的には asm.js は分離されている方がパフォーマンスを出せるらしい
--preload-file で指定したファイルは WASM や asm.js が実行される前にロードされる
gzip に圧縮できるものはしてしまって、MIME などの対応をちゃんとすればダウンロードサイズも小さくできる
Porting
Code Portability and Limitations
コンパイルできないコード
ビッグエンディアンなアーキテクチャに依存するもの
レジスタやメモリスタックを操作するもの
アーキテクチャー固有のインラインアセンブリを含むもの
コンパイルに注意が必要で、コンパイルできないかもしれないコード
マルチスレッドを扱うもの
SIMD 命令に依存するもの
libc のネットワークに関する関数を同期的に使っているもの
ファイルを扱うもの
main 関数を無限にループするもの
関数ポインタをキャストするもの
少し古いブラウザについてだが、ブラウザの仕様上動作しないものもある
コンパイルできるが動作が遅いコード
C++ の例外は JS の最適化の邪魔になるため、 -O1 以降ではデフォルトで例外を無視しているらしい
JS の出力時に -s DISABLE_EXCEPTION_CATCHING=0 をつけると例外が投げられるようになる
Emscripten Runtime Environment
SLD を使っているコードは、emscripten が API を提供しているためそのまま動かせる OpenGL や EGL の WebGL でのエミュレーションや、C++ から HTML5 の一部イベントの発生を拾える API も提供されている ファイルを扱う多くの C/C++ のコードは libc や libcxx の同期な関数でファイルにアクセスするが、ブラウザでは直接ユーザーのローカルのファイルにアクセスできず、サポートされているアクセスも Worke rの外では非同期的なもののみに限られる
emscripten では、JS の出力時に --preload-file で指定したファイルが *.data に梱包され、ランタイムの JS が asm.js や WASM が実行される前にこれをメモリ上にロードし、asm.js や WASM はメモリ上の仮想的なファイルを操作するという仕組みでファイルアクセスをサポートしている
つまりデフォルトの MEMFS ファイルシステムでは、ページの再読み込みでファイルのデータが消えてしまう 余談だが FS.js を C/C++ から使うには emscripten.h の EM_JS などを経由すればよい Node.js の fs を使う場合は NODEFS をマウントする