emscriptenでC++からJavaScriptへ変換しよう

ak10i / 2011-03-11

準備

emscriptenを使ってC/C++のソースコードをJavaScriptのソースコードへ変換するには、次のソフトウェアやそのコマンドが必要です。

まずは上記のソフトウェアを用意しましょう。

LLVM Download Pageにはいくつかのプラットフォーム用にバイナリがあるみたいです。また、MacOS Xではhomebrewで簡単にインストールできるらしいです。(参考: What a JavaScript world - はてなかよっ! )

そういったものが利用できない場合は自分でビルドすることになります。

ツールのビルド(Windows / Visual Studio 2008 / 一部Cygwin使用 の場合)

以下は私の環境(Windows/Visual Studio 2008/一部Cygwin使用)での記録です。CygwinのGCCでビルドしようかとも思ったのですが、V8がCygwinでビルドできないという記事を見かけたのでやめておきました。Visual Studio 2008でビルドしています。

ビルドの手順は次のページを参考にしました。

Clang&LLVMのビルド

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とか色々できてます。

PythonとSConsのインストール

V8のビルドにPythonSConsが必要です。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をダウンロードしました。

コマンドプロンプト(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 ..

V8のビルド

引き続きコマンドプロンプトから(scons.batを使うことになるので)

$ svn checkout http://v8.googlecode.com/svn/trunk/ v8 # Subversionはあらかじめインストールして下さい。
$ cd v8
$ scons d8 #sconsだけだとライブラリファイルができるだけ。emscriptenはd8を必要としているようなので、それだけ作る。
$ dir d8* # d8.exeができてるはず。

参考:

emscriptenのインストール

参考:

CygwinでMercurialを入れてからCygwin Bash上で

$ hg clone https://emscripten.googlecode.com/hg/ emscripten

Gitを入れている人はGitのミラーがあるので、そちらでも大丈夫かもしれません。(スクリプトの改行コードには注意が必要かも)

emscripten/emscripten.pyを実行するとホームディレクトリに.emscriptenというファイルができます。このファイルにコマンドのパスやオプションを正確に設定する必要があります。

PATHを通す

clang.exe、clang++.exe、llvm-*.exe、opt.exe、d8.exe等をPATHが通っている場所に置くか、またはPATHを変更するかして、コマンド名だけで実行できるようにした方が楽です。以下、コマンド名だけで書きますので、PATHを通さないで使いたい人は適宜ディレクトリ指定を補って下さい。

emscriptenのトラブル回避

Windows上だと色々とトラブルが起きます。いくつか遭遇した問題をメモ。

emscripten.pyのpath_from_root関数のプラットフォーム依存

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との相性

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

tests/runner.py

色々と問題があります。面倒なので、私はテストの実行は諦めました。

Hello World

実際にC言語で書いた簡単なプログラムをJavaScriptへ変換してみましょう。

hello.c

// 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上から呼び出す例が次です。

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が参考になると思われます。

その他