Android対応


nmoshがAndroidで動くようになった。事前にCMakeで準備していたので、まぁリコンパイルすれば動くだろうと甘いことを考えていたら、結局三が日を丸ごと消費することになってしまった。
SDLはCMakeでビルドし、cairoはGStreamer SDKからとってきている。なので、以前( http://d.hatena.ne.jp/mjt/20120815/p1 )書いたSDLのテストコードは全く修正無しで動作する。
今のところ、直近ではJNIブリッジを追加する予定は無いので、Android固有の機能を使ったプログラムを書く方法は提供できない。KawaのようにSchemeから直接Javaのクラスファイルを出力するほうが良さそうだし。。

Boehm GCAndroid対応

最大の壁はBoehm GCを動くようにすることで、実際プロンプト自体は難なく出たけれど

謎のクラッシュがなかなか解決できず、SDLによる最初の出力まで9時間掛けている
Boehm GC自体は既にAndroid向けのworkaroundはそれなりに入っていて、GCのコード自体の修正は殆ど無かった。時系列にすると:

  • とりあえずNO_GCを有効にしてプロンプトが出るまでを確認
  • GCを有効にすると謎のクラッシュで死ぬようになる
  • GC_ASSERTIONSを入れると、mark.cで死ぬようになる。ここで、GC_mark_stackに何らかの理由で異常なエントリが入っていることが解る。
  • とりあえず、マークスタックにエントリを入れる可能性のある処理にGC_log_printfを入れ、
  • a) 最初のstatic main data(= 実行ファイルのデータセクション)に登録しているデータが異常に大きいことがわかる
  • b) ついでに、AndroidではDT_DEBUGによる共有ライブラリのディスカバリが動いていないことも解る

問題はaとbの2つ。とりあえず、aに関しては仮定を置いてnmoshでは使わないことでどうにかしたつまり、Androidではnmoshは.soとしてビルドされ、プロセスの起動後にロードされるので、プロセスの.dataとか.bssにはnmoshヒープへのポインタが絶対に含まれないと仮定する。こうすることでaの処理自体が省略できる。
(ついでに、ルートセットを小さくできるという副次的なメリットが有る。Androidは32bitプラットフォームなので、GC対象のヒープを小さく保つことは、保守的GCであるBoehm GCにとって重要なポイント...)
bは深刻な問題で、libnmosh.soの中に有るstaticな変数がGCに無視されてしまう。例えば、

static VM* myVM;

のように、GCで自動的に回収されるタイプのオブジェクトをstatic変数から指してもGCに認識されず、(それが使用中でも)回収されてしまうことになる。
今回は専用のハンドラを用意することで、libnmosh.soのデータセクションを登録するようにした。

Androidのプロセス中からリンクしている.soを辿る方法

BoehmGCは、GCが発動する度にプロセスにロードされている動的リンクライブラリを列挙し、それらのデータセクションをrootセットに入れるように実装されている。
常識的なLinuxの場合は、_DYNAMIC変数に関連する構造体へのポインタが有るのでそこから辿ることができる。しかし、これを埋めるにはDT_DEBUGが必要で( http://sourceware.org/ml/binutils/2006-10/msg00183.html )、どうもAndroid NDKでビルドした.soにはこれが含まれていないように見える。
というわけで、不正なテクニックを駆使してリンクしている.soを列挙しないといけない。

bionic(Androidのlibc実装)に含まれるランタイムリンカは、dlopenしたときに構造体soinfoを返す。この構造体は(たぶん)bionicに固有のもので、soinfoを見れば、個々のモジュールがロードされたオフセットが解るという寸法。
このため、自分自身(libnmosh.so)をdlopenすることでsoinfoを得る。
ただし、soinfoはNDK的なABIの一部とは見做されておらず、ヘッダもNDKに含まれていない。なので、今後のNDKリリースで自然に壊れる可能性は大いにある。今回は諸般の事情でヘッダを手作りしたが、実物はBionicのリポジトリにある。
ちなみに正攻法は/procを使う方法で、BoehmGCにはこちらも実装されているが、THREADを有効にしたときに上手く動かないと注意書きが有る。

パフォーマンス

すこぶる悪い。

01-05 09:29:59.160: D/BDWGC(8894): GC #7098 freed 8694848 bytes, heap 27068 KiB (+ 0 KiB unmapped)
01-05 09:29:59.160: D/BDWGC(8894): World-stopped marking took 133 msecs (134 in average)
01-05 09:29:59.160: D/BDWGC(8894): In-use heap: 62% (9655 KiB pointers + 7187 KiB other)

27MiBのヒープのFull GCに133msec掛かっている。これはBoehmGCが遅いというよりは、そもそもアロケーションが多すぎるという構造的な問題も有る。。nmoshのFFI(pffi)は関数を呼ぶたびに必ずヒープに呼び出しパケットを確保するので、GCが頻繁に起る。

その他Android向け特別対応

  • staticなGCオブジェクトの排除

staticなGCオブジェクトが有るとGC_INIT()以前にGCが呼ばれてしまうので、さまざまな面で不都合が多かった。(一般に、C++コンストラクタで起るクラッシュはデバッグしづらい。あと、Android NDKのgdbをそのまま使うと、アプリケーションが起動後2秒以内にクラッシュすると正常にアタッチできない)
最後に残っていたnon-generative-rtdを格納するためのeq-hashtableオブジェクトをstaticからただのポインタにし、mosh_init内で初期化するようにした。https://github.com/okuoku/mosh/commit/59f6668a5e8c5040a07a5799e4e1b06ab6935900

nmoshには(host-os)手続きがあり、OS固有のコードをディスパッチできるようになっている。
Androidlinuxカーネルなので、まぁLinuxと同じIDを振っても良いように思うが、libcが違う(常識的なLinuxglibcAndroidはbionic)のと、提供しているAPIにかなり開きが有るので別名を付けた。特に、fork/execが使えないのと、ファイルI/Oに制約が有るのが大きい。。

nmoshは、複数のバイトコードキャッシュを一つのファイルに纏めてライブラリとして使用できるバイトコードアーカイブ機能を持っている。これは本来Win32/NetBSD向けの単体アプリケーションをnmoshで書くために用意したもので、これらの環境ではそもそもクロス開発でないのでこのような機能は必要なかった。
今回のために、VMバイトコードとFASLのバージョンに互換性のあるnmoshを準備できれば、バイトコードアーカイブをクロスビルドできるようにした。このため、(target-os)手続きが新設され、ソースツリーには従来のC配列に変換されたランタイムライブラリに加えて、FASL形式の生バイナリも置くようになった。
この対応はまだ不完全で、ライブラリのvisit(展開)時に実行されるコードはそのまま実行されてしまう。これは絶妙なポイントで、マクロで使用される手続きは環境依存の無いように書く必要が有るということになる。

  • fd 1/2への出力をAndroidのlogcatにリダイレクトする

stdoutとstderrはAndroid上ではサポートされないので、これらへの出力はFileクラス内部でリダイレクトするようにしてしまった。
本来はクラスを分けて対応するべきな気もする。

nmoshには、C手続きをexposeするための"プラグイン"機構が有る。この方式で書いたプラグインを、nmosh本体に静的にリンクできるようにした。
コレには今のところあまり大きなメリットは無いが、将来リンク時最適化をサポートしたときに、実行ファイルの縮小とかその辺では効果が有る。はず。
CMakeではライブラリのリンク等が抽象化されているのでこの手のテクニックは実現しづらい。nmoshの場合は、一旦静的リンクライブラリをadd_libraryし、最後にリンカの--whole-archiveオプションでリンクするという方法で無理矢理実現している。
WindowsMac OSと違って、ELFの世界では静的リンクライブラリには依存関係の概念が無いので、必要なライブラリは手動でディスカバリしてリンクする必要が有る。壮絶。