OCamlベースのAndroidアプリに向けて

O'Caml on Android というパッチを作っている. 柔軟かつ信頼性の高いOCamlAndroidのアプリを書くのが目標だ. ここ2ヶ月ほどocamljs (OCamlからJavaScriptへのコンパイラ) を使っていて、OCamlの信頼感と柔軟性にとても満足したので,ではAndroidでもOCamlだ、とばかりに開発を再開している.
iPhoneiPadObjective-Cと違い,AndroidではJavaの型の扱いやJVMとの相互作用が必要でありなかなか厄介だ. しかしiPhoneではいくつかの開発 事例もあるのでAndroidでもそれなりに有効ではないかと信じている.がんばりたい.

OCamlを使う利点

もはや自分の中では言い古した感があるのだけど,念のため列挙してみる.

信頼性

OCamlには null値がない.このため NullPointerExceptionが起こらない.さらに,代数的データ型が使えるため パターンマッチによる場合分けの 網羅性検査 がある. Javaにも型システムがあるけれど, 全ての参照型でnull値が許される点と,nullチェックの抜け漏れが容易に起こり得る点でよろしくない.

簡潔さ

型推論が(ほぼ)完全であるため,関数のパラメータや戻り値の型の宣言が必要なく,コードが簡潔で読みやすくなる. 一方,Javaには型推論がない(少なくとも現時点でAndroidで使えるJava5はそうだ)ので,たとえばジェネリクスを交えて HashMap<String,<Set<Class>>> のような型をいちいち書くのは冗長である.

パフォーマンスの向上

OCamlプログラムはネイティブコードにコンパイルされるため,DalvikVMにJIT(Android2.2以降)が備わってもOCamlのほうが高速…かもしれない.

OCamlを経由することの欠点

欠点がないわけではない.

言語境界のオーバーヘッド

JavaOCaml間でのやりとりで,引数や戻り値を互いの言語で扱える形に変換するオーバーヘッドがある.フレームワークとのやり取りが多いならば問題になるかもしれない

マルチスレッドの扱い

OCamlGCは並行でない. もしAndroidがマルチコア化される時代が来れば,パフォーマンス上の問題になる.またOCamlのコードを同時に実行できるスレッドは1つだけであり,途中でOCamlスレッドのコンテキストスイッチが発生しないケースがあるかもしれない(過去の日記参照;ほとんどないと思うけどAndroidで調べてない).

解釈実行のオーバーヘッド

バイトコード方式を採用した場合はそのオーバーヘッドがある.ネイティブコードにコンパイルしたほうがよい.

OCamlJavaの連携: 4通りの方法

AndroidアプリはJavaベースなので, Javaから OCaml を実行するには次の2つの軸から4通りが考えられる:

  • JVM or JNI ?
    • Java側で実行(解釈)する (A)
    • Android-NDKを使ってネイティブ側で実行する (B)
  • ocamlc or ocamlopt ?

私がつくっているO'Caml on Android (名前を付けたい…)は、Android NDKを使うので (B-1) か (B-2) だ. 一方,Cadmium を使った OCaml on AndroidJVM上でOCamlバイトコードを解釈実行するので(A-1)だ.

JNIを使った OCaml-Java連携: 3つのレベル

JNIを経由したOCamlJavaの連携には,洗練度により,次の3つのレベルがある:

  • OCamlで書いた関数を C言語でラップし, JNI経由で呼び出す.
  • camljava を使う
    • OCamlのオブジェクトのメソッドを, Java側から呼び出せる.その逆も可能.しかし言語の境界で型安全でない.
  • O'jacare を使う
    • 上記の欠点を克服し, Java のクラスが OCaml の class type で表現されており型安全. JavaのオブジェクトをOCamlのオブジェクトとして操作できる. Java から OCamlを呼び出すには, OCaml側でJava のクラス/インタフェースを継承/実装によって行う.ただし スタブを生成するための IDL を自分で準備しなければならい(自動生成すれば良いけど).

下の項目は上の項目に依存しているので,順に解説する.

原始的な方法

JNI経由でOCamlを呼ぶには,まず JNIで呼び出す関数をエクスポートした共有ライブラリ(AndroidLinuxベースなので .so)を作成し,Java側で System.loadLibrary() でロードする.(OCaml-Nagoyaのwikiにも書いてみた)
そこで まず私は OCaml on Android という, .so を生成できるコンパイラを準備した. ビルドには Android NDKが必要である.
あまり細かいことはできないが,例えば OCamlのトップレベルを Android アプリとして動かせるようになる(なった).

camljava を使う

camljavaOCaml のモジュール Jni を提供しており,ここから JNIの関数をだいたい叩ける.しかし camljava を Android で使うにはいくつかの障壁がある(O'jacareも同じ問題がある)

  • camljava は main関数が OCaml側にあるのが前提で,そのままでは使えない
    • Jniモジュールのロード時にJVMを新しく生成しようとしてしまうのでコメントアウト
    • メソッドが終了すると消える参照をキャッシュしてしまう. 調べたかぎりでは Java側クラスの参照とJNIEnv*
  • メモリ管理系の関数が欠落
    • DeleteLocalRef がない. 多くのクラスやオブジェクトを参照すると local reference tableがいっぱいになる
  • Androidのバグ.私がレポートしたものは

現在パッチを書いている.手元で大部分の修正はできているが,メモリ管理周りはまだ自信がない.

O'Jacareを使う

O'Jacare は, camljava をベースに作られている.Javaのクラスを OCamlのクラスとして扱えるのが利点だが, Javaはメソッド名のオーバーロードを許すがOCamlでは許さないといった差異があるため, スタブの生成には IDLファイルを準備する必要がある.
Java のクラスについて宣言した .idl ファイルを ojacare コマンドに与えると, OCaml 側のスタブと Java側のスタブが生成される.OCamlのクラスとして, Javaのクラスを扱える. OCaml側でこのクラスを inherit すれば,Javaのクラス/インタフェースを拡張/実装でき,コールバックできる
しかし,ざっくり Android のクラスを ojacare にかけてみたところ,次の問題をみつけた.(私の備忘録として…)

  • (マニュアルに記載あり)Java側の参照を開放しない.
  • 型システムの制限
    • ジェネリクスに非対応 (多相性がOCaml内に閉じていれば問題ないが…).
    • インナークラス等に非対応(多分.やり方が分からない…)
    • 戻り値型のcovarianceに非対応 (java.lang.Appendableなど,現状とくに問題ではない)
  • メソッド名の問題 (軽症).
    • _ (アンダースコア)で始まるメソッドを定義できない, など(すぐに修正できるけど…)
    • IDLファイルの予約語と衝突するJavaのメソッドは使えない
  • O'jacareビルド時の camlp4の非互換性
    • 当初これにかなり苦しめられた…
    • OCaml on AndroidOCaml 3.12 (arm-eabi+ソフトウェア浮動小数点が扱える) 以降が必要だが,このバージョンのcamlp4は O'jacareが期待する OCaml 3.08当時のcamlp4と非互換.
    • camlp5 を使えば良い…と思いきや,camlp5 も 5.15 と 6 で非互換な部分が多かったりする.
    • O'Jacareをcamlp5に対応させるパッチを書いた.
  • camljavaと同様の問題.
    • クラスへの参照をキャッシュしてしまうがメソッド終了時に参照が失われる…
    • 多くのクラスをIDLに含めると,起動時に local reference tableの上限(たった512!)を越えて findClass等をするので落ちる
  • O'jacare の問題.
    • (修正済み) src/check/type.ml で Ishort -> Cbyte となっているのは Ishort -> Cshort
  • int が camlint にマップされてしまう. R.layout.main などは 扱えない

型システムの制限はやや厳しい.また,メモリ管理周りも改善しないと使えない.

現状

  • O'jacare を使って Androidのオブジェクトを操作できるようになった
    • Android SDKのクラス群から IDLを自動生成した
    • とりあえず Activity を OCaml側で extend して いくつかのメソッドをオーバーライドできるようになった
    • 整理してパッチを公開したい

補遺:デバッグについて

OCamlが例外で落ちる時は,C言語の標準出力にメッセージを吐くが,これはlogcat には出てこない.stderr を dup2 して,ソケットに出力して別スレッドからlogcatに再出力するのが良い.参考