nmi.jp Twitter → @tkihira
embona - ブラウザで動くBonanzaを作ってみた(その2) embona - ブラウザで動くBonanzaを作ってみた(その4)

embona - ブラウザで動くBonanzaを作ってみた(その3)


2015-01-19
Takuo Kihira

さて前の記事では、ついにBonanzaの起動時のエラー画面まで再現することが出来ました。今回の記事では、Emscriptenでのファイルの「簡単な」扱い方について説明します。

前の記事はこちら→ embona - ブラウザで動くBonanzaを作ってみた(その2)
とりあえず遊んでみたい、という方はその1の記事の最初にリンクを用意しておきました。そちらをご参照ください。

Emscriptenの中でファイルを読み込む

さて、前回 fv.bin がない、というエラーで終了していました。JavaScriptからファイルを読み込めるようにしてあげなければいけません。今回は、お手軽な突破の方法をご紹介します。しっかりとした扱い方は、次の記事(その4)でご紹介します。

JavaScriptはもともとブラウザで動く言語として開発された経緯もあり、JavaScriptそのものにファイルの入出力のAPIは用意されておりません。そこでEmscriptenは、ファイルを合成時にまとめて突っ込むオプションを用意しています。この方法だとビルド時に存在するファイルにしか利用出来ませんが、ゲームの素材などを利用する場合にはこの方法で十分なこともあるでしょう。

というわけでまずその方法でやってみましょう。

bonanza.js : $(OBJS)
    $(CC) $(LDFLAG1) -o bonanza.js $(OBJS) $(LDFLAG2) -s TOTAL_MEMORY=335544320 --embed-file fv.bin

最後の embed-file オプションで埋め込むファイルを指定しました。これでビルドして実行してみると…

$ node bonanza.js
FATAL ERROR: CALL_AND_RETRY_0 Allocation failed - process out of memory
Abort trap: 6
$ ls -al bonanza.js fv.bin
-rw-r--r--  1 tkihira  staff  657931308 Jan 16 19:20 bonanza.js
-rwxr-xr-x@ 1 tkihira  staff  186268248 Jan 16 17:50 fv.bin
$

そりゃそうですよね。そもそものfv.binが186Mもあって、それをテキスト形式で埋め込むもんだから、スクリプトファイルだけで657Mにもなっちゃってます。こんな巨大なスクリプトファイル、メモリ不足で実行出来ないとしても仕方ありません。このオプションを利用出来るのは、ちっちゃいファイルだけです。

というわけで、今回は埋め込むのではなくて、ファイルシステムからロードしてもらうようにしましょう。preload-fileというオプションがあります。これを指定すると環境に合わせてあらかじめロードし、ロードが完了次第プログラムが実行するようになります。

bonanza.js : $(OBJS)
	$(CC) $(LDFLAG1) -o bonanza.js $(OBJS) $(LDFLAG2) -s TOTAL_MEMORY=335544320 --preload-file fv.bin

さてビルドして実行すると…

/Users/tkihira/projects/bonanza_v6.0/src/client/bonanza.js:16
      PACKAGE_PATH = encodeURIComponent(location.pathname.toString().substring
                                        ^
ReferenceError: location is not defined

ありゃりゃ。preloadはhtml出力にしか対応していないのですね。今回は別にnodeにこだわっているわけではないので、htmlを出力するように変更してみます。ビルド時の出力ファイルを、bonanza.jsからbonanza.htmlに書き換えるだけで自動的に切り替わります。

bonanza.js : $(OBJS)
	$(CC) $(LDFLAG1) -o bonanza.html $(OBJS) $(LDFLAG2) -s TOTAL_MEMORY=335544320 --preload-file fv.bin

ビルド完了後、自分は次のようにして実行しました。要は適当なサーバにbonanza.htmlをあげればいいだけです(正確には、bonanza.html、bonanza.js に加え、preloadのファイルを一つにまとめたファイル bonanza.data も同じ階層にアップロードする必要があります。Emscriptenがbonanza.dataをXHRで読み込み、プログラム起動前にそれをメモリ上に適切に配置してくれます。)。

$ make emcc
$ python -m SimpleHTTPServer &
Serving HTTP on 0.0.0.0 port 8000 ...
$ open http://localhost:8000

ブラウザではこんな感じになりました

Cannot enlarge memory arrays. Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value 335544320, (2) compile with ALLOW_MEMORY_GROWTH which adjusts the size at runtime but prevents some optimizations, or (3) set Module.TOTAL_MEMORY before the program runs.

またメモリが足りないようですね。とりあえず動けばいいやということで、メモリが足りなくなったら自動的に拡張してくれる ALLOW_MEMORY_GROWTH オプションをつけちゃいましょう。

bonanza.js : $(OBJS)
	$(CC) $(LDFLAG1) -o bonanza.html $(OBJS) $(LDFLAG2) -s TOTAL_MEMORY=335544320 --preload-file fv.bin -s ALLOW_MEMORY_GROWTH=1

その結果がこちら。</p> Assertion failed: you need to wait for the runtime to be ready (e.g. wait for main() to be called)

このアサーション、Emscriptenを使っていると頻繁に出てくるんですが、大体要因は別のところにあったりします。エラーの大雑把な意味は「初期化する前にメイン関数実行するな」ということなんですが、まずは今回の修正で何が変わったかを考えてみます。JavaScriptのコンソールを確認すると、こんなメッセージが出ていました。

Warning: Enlarging memory arrays, this is not fast! 335544320,671088640

あくまで予想ですが、メモリを自動拡張している処理の中で余計なコードが走って、副産物として上記のエラーが出てしまった可能性がありそうです。Emscriptenはまだまだ枯れていないので、こういった問題は比較的ありそうな気がします。というわけでメモリを自動拡張しないように、最初から増やして(-s TOTAL_MEMORY=671088640)再度挑戦してみます。

 pre-main prep time: 2711 ms
missing function: pthread_attr_setdetachstate
 -1

そうすると今度は-1という謎の出力が出ました。しかし、ログを見る限り pre-main prep は問題なく行っているようで、先ほどのエラーは解決されたことは確認出来ました!すなわち、どうやらついに fv.bin を読み込むことには成功したようです!

エラーを潰してBonanzaを起動させる

そして残念ながら、ついに今まで無視していたpthreadのエラーと直面する時間が来てしまいました。今回は、定義されていない関数を呼び出している、というエラーです。今までビルドの最後で出ていた警告の奴です。

warning: unresolved symbol: pthread_create
warning: unresolved symbol: pthread_attr_setdetachstate

Bonanzaの中でthreadをガッツリ使われていると、threadを使わずに対応させるのにすごく労力がかかりそうなので辛いところです。pthread_attr_setdetachstateでgrepするとini.cで使われているようですので、とりえあずソースを見てみます。使われている前後を見てみると…

#if ! defined(_WIN32) &amp;&amp; ( defined(DFPN_CLIENT) || defined(TLP) )

なんと、TLPとDFPN_CLIENTを定義しなければスレッドを使わない設定になっているようです!これは本当にBonanzaに助けられました。一緒にpthread_createで調べるとlearn1.cとthread.cで使われており、そちらはTLPを定義していなければ使わないようです。なので、これらの2つの定義をMakefileから外してみましょう。

OPT =-DNDEBUG -DMINIMUM -DDFPN -DINANIWA_SHIFT -DMNJ_LAN -DCSA_LAN

これで再度make clean && make emccをしてみます。出力結果は

ERROR: Can't open a file, log/n000.log

Emscriptenのエラーが全て消えて、その1でも登場したアプリ側のエラーになりました!少なくともfv.binは確実に読み込まれているようです。今回のlogフォルダのエラーに関しても、Makefileでログを使わない設定(-DNO_LOGGING)を加えればパス出来そうです!リビルドすると…

ついにブラウザ上でBonanzaが起動しました!標準出力が画面の黒い所に、標準入力がプロンプトダイアログになっているようですね!

ダイアログで move 9999(9999手目まで戦え)、と入力して後のプロンプトは全部閉じれば、ブラウザ上でBonanza vs Bonanzaの戦いを見ることが出来ます。これでEmscriptenでBonanzaを動かす、という目的は達成されました!

無事に目的を達成したのでここで終了しても良かったのですが、せっかくなのでBonanzaを組み込んで実際にBonanzaと戦えるサービスを作ってみましょう。その過程で、Emscriptenのより深い使い方を知ることが出来ます。随分長い記事になりましたが、次が最終記事になります。

次の記事はこちら→ embona - ブラウザで動くBonanzaを作ってみた(その4)