ak10i / 2011-03-11
emscriptenを使ってC/C++のソースコードをJavaScriptのソースコードへ変換するには、次のソフトウェアやそのコマンドが必要です。
clang / clang++ (gccとかg++とかclとかに相当するものですね)
opt (llvmのコードを最適化するプログラム(Optimizer)です)
llvm-link (リンカ)
llvm-dis (逆アセンブラ)
d8 (JavaScriptをコンソールから実行するプログラムです。emscripten自体が一部JavaScriptで書かれているようで、その実行に必要です。SpiderMonkeyでも良いそうですが、ビルドが簡単そうだったV8にしました)
emscripten.py等 (llvmの逆アセンブラテキストからJavaScriptへ変換するプログラムです)
Python (emscriptenの一部はPythonで書かれています。また、V8をビルドするときにも必要です)
Visual Studioとかgccとか (上記をビルドするのに必要です。また、Clangでのコンパイル時に参照するインクルードファイルとして必要になる(?))
SubversionとかMercurialとか (ソースコードを入手するためにあった方が便利です。無くてもブラウザ経由でダウンロードできるかも(?))
まずは上記のソフトウェアを用意しましょう。
LLVM Download Pageにはいくつかのプラットフォーム用にバイナリがあるみたいです。また、MacOS Xではhomebrewで簡単にインストールできるらしいです。(参考: What a JavaScript world - はてなかよっ! )
そういったものが利用できない場合は自分でビルドすることになります。
以下は私の環境(Windows/Visual Studio 2008/一部Cygwin使用)での記録です。CygwinのGCCでビルドしようかとも思ったのですが、V8がCygwinでビルドできないという記事を見かけたのでやめておきました。Visual Studio 2008でビルドしています。
ビルドの手順は次のページを参考にしました。
Clang Getting Started : http://clang.llvm.org/get_started.html
emscripten GettingStarted : http://code.google.com/p/emscripten/wiki/GettingStarted
LLVM Download Pageより2.8のllvmならびにclangのソースコードをダウンロードしました。
展開。clang-2.8.tgzはtoolsの下にclang-2.8ではなくclangという名前で展開します。
$ tar xvfz llvm-2.8.tgz $ tar xvfz clang-2.8.tgz $ mv clang-2.8 llvm-2.8/tools/clang
ビルド用のディレクトリを作ります。
$ mkdir llvm-2.8/cbuild $ cd llvm-2.8/cbuild
CMakeでVisual Studio用の.slnと.vcprojを作ります。
私はWindows GUI版のCMakeをインストールして作成しました。
ソースディレクトリ(llvm-2.8)の指定、ビルドディレクトリ(llvm-2.8/cbuild)の指定、Configure、Generateの4ステップでビルドディレクトリの中にVisual Studio用の.slnと.vcprojができあがります。
できあがったllvm-2.8/cbuild/LLVM.slnをVisual Studioで開いて、Release版でビルドしました。ビルドが終わると llvm-2.8/cbuild/bin/Release にclang.exeとかllvm-*.exeとかopt.exeとか色々できてます。
V8のビルドにPythonとSConsが必要です。CygwinのPythonでは問題(native.ccの生成がうまくできなかったり、GCCでビルドされてしまったり)があったのでWindows版のPythonを別途インストールしました。Pythonは2.7.1と3.2がありましたが、2.7.1 Windows Installerを使いました。
環境変数PATH=を変更してインストールしたPythonが先に使われるようにします。Cygwinのbinよりも先に指定しないとダメです。
SConsのサイトからSConsをダウンロードします。この文書を書いた時点ではscons-2.0.1.zipをダウンロードしました。
Scons: http://www.scons.org/
コマンドプロンプト(cmd.exe)を開き、SConsを展開してできたディレクトリへ移動し、python setup.py を実行します。
$ python --version #インストールしたバージョンとなることを確認する。cygwinのpythonが使われないように注意。 $ unzip scons-2.0.1.zip # unzipが無ければエクスプローラで展開してください。 $ cd scons-2.0.1 $ python setup.py $ scons --version #インストールされていればちゃんと表示される。 $ cd ..
引き続きコマンドプロンプトから(scons.batを使うことになるので)
$ svn checkout http://v8.googlecode.com/svn/trunk/ v8 # Subversionはあらかじめインストールして下さい。 $ cd v8 $ scons d8 #sconsだけだとライブラリファイルができるだけ。emscriptenはd8を必要としているようなので、それだけ作る。 $ dir d8* # d8.exeができてるはず。
参考:
参考:
emscriptenのソースコードの入手先について: Source Checkout - emscripten: http://code.google.com/p/emscripten/source/checkout
CygwinでMercurialを入れてからCygwin Bash上で
$ hg clone https://emscripten.googlecode.com/hg/ emscripten
Gitを入れている人はGitのミラーがあるので、そちらでも大丈夫かもしれません。(スクリプトの改行コードには注意が必要かも)
emscripten/emscripten.pyを実行するとホームディレクトリに.emscriptenというファイルができます。このファイルにコマンドのパスやオプションを正確に設定する必要があります。
clang.exe、clang++.exe、llvm-*.exe、opt.exe、d8.exe等をPATHが通っている場所に置くか、またはPATHを変更するかして、コマンド名だけで実行できるようにした方が楽です。以下、コマンド名だけで書きますので、PATHを通さないで使いたい人は適宜ディレクトリ指定を補って下さい。
Windows上だと色々とトラブルが起きます。いくつか遭遇した問題をメモ。
Windows版のpythonでemscripten.pyを実行したとき、以下のようなエラーになります。(2011-03-11時点)
C:\home\k-aki\work\emscripten\hello>python ..\emscripten\emscripten.py hello.o.ll c:\home\k-aki\work\emscripten\v8\d8.exe Traceback (most recent call last): File "..\emscripten\emscripten.py", line 11, in <module> exec(open(path_from_root('tools', 'shared.py'), 'r').read()) IOError: [Errno 22] invalid mode ('r') or filename: '\\C:home\\k-aki\\work\\emscripten\\emscripten\\tools\\shared.py'
emscripten.pyの中のpath_from_root関数に問題があって、
abspath = os.path.abspath(os.path.dirname(__file__)) def path_from_root(*pathelems): return os.path.join(os.path.sep, *(abspath.split(os.sep) + list(pathelems)))
この部分のせいで\C:home\〜のような不正なファイル名になってしまいます。
- return os.path.join(os.path.sep, *(abspath.split(os.sep) + list(pathelems))) + return os.path.join(abspath, *pathelems)
とすれば一応回避できます。
Cygwinのpythonからemscriptenを実行するとうまく動かないことがあります。でも何回か実行するとうまくいくときもあります。原因はよく分かりません。d8との相性でしょうか。
$ ../emscripten/emscripten.py hello.o.ll 1 [main] python 2324 C:\app\cygwin\bin\python.exe: *** fatal error - unable to remap \\?\C:\app\cygwin\lib\python2.6\lib-dynload\time.dll to same address as parent: 0x2D0000 != 0x3C0000 Stack trace: Frame Function Args 0022AA68 6102792B (0022AA68, 00000000, 00000000, 00000000) 0022AD58 6102792B (6117DC60, 00008000, 00000000, 6117F977) 0022BD88 61004F3B (611A6FAC, 612426FC, 002D0000, 003C0000) End of stack trace 1 [main] python 3580 fork: child 2324 - died waiting for dll loading, errno 11 Traceback (most recent call last): File "../emscripten/emscripten.py", line 43, in <module> emscripten(sys.argv[1], sys.argv[3] if len(sys.argv) == 4 else "{}") File "../emscripten/emscripten.py", line 22, in emscripten subprocess.Popen(JS_ENGINE + [COMPILER], stdin=subprocess.PIPE).communicate(settings+'\n'+data)[0] File "/usr/lib/python2.6/subprocess.py", line 633, in __init__ errread, errwrite) File "/usr/lib/python2.6/subprocess.py", line 1049, in _execute_child self.pid = os.fork() OSError: [Errno 11] Resource temporarily unavailable
色々と問題があります。面倒なので、私はテストの実行は諦めました。
実際にC言語で書いた簡単なプログラムをJavaScriptへ変換してみましょう。
// hello.c #include <stdio.h> int main() { puts("Hello World"); }
これをコンパイルします。
$ clang -emit-llvm -c hello.c $ llvm-dis -show-annotations hello.o
hello.oとそれを逆アセンブルしたhello.o.llができます。続いてそれをemscriptenで処理します。
$ python emscripten.py hello.o.ll > hello.o.js # LLVMのコードからJavaScriptへ変換 $ d8 hello.o.js # コンソールでJavaScriptを実行 Hello World
hello.o.jsが最終的なJavaScriptファイルです。これをd8で実行するとHello Worldと表示されます。
emscripten.pyの実行でエラーになる場合はホームディレクトリに生成される.emscripten内のJS_ENGINEとJS_ENGINE_PARAMS、TEMP_DIRあたりが正しく設定されているか確認して下さい。
Windowsだと色々な原因で動かないことがあるので、上に書いた「emscriptenのトラブル回避」を参照して下さい。
ところで、このstdio.hってどこのstdio.hなんだろうと思ってプリプロセッサ出力(clangの-Eオプション)を見てみたら、Visual Studioのstdio.hでした。いいのかな。
Web上で実行する例も置いておきます。
<!DOCTYPE html> <html> <head> <title>Hello</title> <script> var arguments = ["hello"]; document.open(); function print(str) { document.write(str); } </script> <script src="hello.o.js"></script> <script> document.close(); </script> </head> <body> </body> </html>
変数argumentsと関数printを定義してからhello.o.jsを実行しています。argumentsの中身はmainへargc、argvとして渡されます。print関数はputsの中から呼ばれます。
参考: http://code.google.com/p/emscripten/wiki/GettingStarted#Running_Emscriptened_Code_on_the_Web
今回emscriptenに注目した理由の一つにJavaScriptだと演算子オーバーライドができなくて色々と不便だと言うことがありました。試しに二次元ベクトルクラスの例を書いてみました。ちゃんと動くでしょうか。
$ clang++ -emit-llvm -c ball.cpp $ llvm-dis -show-annotations ball.o $ python emscripten.py ball.o.ll > ball.o.js
できあがったのはこちら。
_stepやそこから呼ばれる__ZN4Ball4stepEdを見ると、operator*に相当する__ZmldRK4Vec2、operator+=に相当する__ZN4Vec2pLERKS_、さらにoperator*内からはコンストラクタに相当する__ZN4Vec2C1Eddが一つ一つ律儀に呼ばれていることが分かります。無駄が多いですね。
それもそのはず、最適化をしていませんでした。clang++ -O3オプションを指定してできあがったのがこちら。
_step関数を見比べると、最適化した方はインライン展開されているのが分かります。
Web上から呼び出す例が次です。
ball.html(要canvas要素サポート)
HTML内に書いてあるJavaScript関数では、タイマーをセットし、一定間隔でstepを呼び出してボールの位置を更新し、その位置を取得してcanvas要素を更新しています。詳しくは直接htmlのソースを見てください。
HTML側からC++のオブジェクトを参照するのは少し面倒です。今回の例ではグローバル変数としてballがあり、例えばX軸に沿った位置は(C++上では)ball.pos.xなわけですが、JavaScriptのコードから直接_ball.pos.xというような形ではアクセスできません。ball-opted.o.jsの中には_ballという変数は存在しますが、これはHEAPという配列のインデックス番号となっています。_getBallPosXという関数の中身を見ればそれが分かります。
このようなことを考慮すると、今回のような書き方(stepを呼び出して、その後getBallPosX、getBallPosYで結果を取得するようなやり方)は良くないかもしれません。オブジェクトが増えたときに取得用関数をいちいち定義しなければならないのは面倒です。関数stepの中でボール状態の更新だけで無くcanvasへの描画指示まで行うという手もあります。モデルとビューの分離を気にするのであれば、コールバックを伴うような取得関数を定義するのも手でしょう。C++からJavaScriptで書かれている関数を呼ぶのは比較的簡単なので。Hello Worldのときのprint関数みたいな感じですね。
このあたりのC++とWebとの連携についてはemscripten/demosやemscripten/testsが参考になると思われます。
例外は使える?
コンテナやアルゴリズムは使える?
mainが終わったときに終了処理がされる?
raytraceのデモはどうなってる?