ものがたり(旧)

atsushieno.hatenablog.com に続く

Android AOSPのXMLをjavaクラスから生成する

めでたくMono for Android 1.9がリリースされたので、わたしがXamarinで最初にやっていた仕事のはなしを書こうと思う。いや実は書いたのもう先月だったんですけどね。リリース出たら出そうと思ってたらもう9月も終わりそうな。

Mono for Androidでは、Androidのソース (AOSP) に含まれるAPI定義XMLからクラスライブラリを生成している。しかし、HoneycombはGoogleソースコードを公開していないクローズドソースのフレームワークであり、Mono for Androidがこれに対応するのは不可能だ。

というわけで、XamarinではAOSPのXMLに相当するものをandroid.jarから生成して対応することにした。既に基本的なjavaクラスライブラリ解析部分とXML生成部分は他のハッカーがやっつけていたのだけど、それを製品に適用できるところまでもっていく人手が必要だったというわけだ。このコードは一応Xamarinのgithub上でも公開されている。ただ、AOSPに特化したもので、汎用的ではない。

そんなわけで、今回はこのXML生成ツールの(というか、それに絡む面倒な問題の)はなし。XMLからコードを生成する部分はMono for Android独自の話題なので書かない。

変換処理の基本

初期のコードは単純だった。変換のためのライブラリ解析部分はごく単純にJavaのリフレクションAPIjava.lang.reflect)を利用して行われており(Javaで書かれている)、XMLの生成はDOMとJAXPで行われている。これだけなら時間さえあれば誰でも出来るだろう。

しかし、ほどなくして、単純な実装のアウトプットと、実際にAOSPで公開されているXMLを比較すると、だいぶ違うことに気が付いた。われわれのコードジェネレータはAOSPのものを前提として作られているので、あまり独自形式で出力するわけにもいかない。かくして、AOSPとの違いを探索する作業が始まった。

定数値の取得

今回の変換処理では、static finalなフィールド(たとえばMath.PIなど)の値がもし取得可能であれば取得することになるのだけど、これが必ずしもリフレクションで取得できるとは限らない。

  • boolean型のフィールドについてField#getBoolean(null)を呼んでも、その戻り値がfalseであれば、それが規定値なのか、単に値が存在しないのかすら分からない(これはField#get(null)でも同様)。
  • また、DoubleのINFINITYなど特殊な定数の値は数値では表現できないため、実はAOSPでも “1 / 0” だの “-1 / 0” だのといった文字列表現になっている(つまり単純な自動出力ではない)。
  • さらに、protectedなフィールドの値を取得しようとするとIllegalAccessExceptionになってしまう。

これらの問題のいくつかは、リフレクションをやめてasmバイトコード解析することで解決した。

proguardの洗礼

android.jarは、単純にjavacでコンパイルされて生成されたものではなく、その後にproguardを使用してコードが調整されている。たとえばその過程でメンバーに付加されていたはずのアノテーションが消滅しており、@Deprecatedを拾うことができなくなっていた。ちなみにクラスに付加されていた@Deprecatedは残っている。

ClassLoaderの動作による混乱

アノテーションの問題は、javaのリフレクションにかかる制約もあいまって、だいぶややこしい事態になる。Javaランタイムに含まれるクラスの情報は、外部のURLClassLoader.loadClass()などから呼び出しても、ランタイム自体から返されてしまうということにある。つまり、android.jarに含まれるjava.*クラスの情報を取得しようとしても、実行しているJREの情報しか取得できない。(これは.NETのリフレクションで外部にあるmscorlib.dllをロードした場合にも制約がついて回ることと似ている。)

その結果、アノテーションは、java.*クラスについては取得できる(ただしandroid.jarから正しく取得しているわけではなく、JREの情報を取得しているに過ぎない)が、android.*など他のクラスについては取得できない、という、実態に合わない結果が返ってくる。

ちなみにJava 6のJREandroid.jarに含まれるJavaのクラスは、AOSPのXMLから判別できる限りでは、javax.net.ServerSocketFactory.getDefault()がsynchronizedであるかそうでないかの違いがあるだけで、後は全て一致しているので、JREからクラスをロードしていることにはあまり不安要素は無い。

バイトコードの制約

Javaバイトコードに関係する制約として、まず分かりやすい問題として、JavaのリフレクションAPIでは、メソッドのパラメータ名を取得することができない。これは仕方ないので、全てAPIリファレンスをscrapingして得ている。

バイトコードで悩ましいのは、一部の(全てではない)ジェネリクス情報が消えてしまうこと(いわゆるerased generics)だ。ソースコード上にあった情報が、バイトコード上では消えてしまって取得できない。AOSPのXMLでは、ジェネリック型の型引数情報が消滅していることが多い(これも全てではなく、条件の整理が必要になるだろう)。

ジェネリクス情報が消えてしまうと、ジェネリックインターフェースを実装しているクラスで実装する必要のあるメソッドのシグネチャーも変わってしまう。たとえば、IComparableであれば、compareTo(T)を実装しなければならないが、getDeclaredMethods()ではcompareTo(Object)が(も)返ってくる(compareTo(T)と2つ返ってくるのは、ひとつはコード上の明示的な実装で、もうひとつはインターフェースメソッドとして自動的に返されるもの、ということだろう。次のセクションで触れる)。

Class.getDeclaringMethods()の戻り値のフィルタリング

AOSPのXMLは、オーバーライドされたメソッドの扱いについて非常に不可解で、派生クラスで宣言されたメソッドが含まれたり含まれなかったりしている。その条件は不可解で、わたしはこれを無理やり実装し終えた今でも、AOSPのXMLの内容を合理的に説明できない。(たとえば、AdapterView#setAdapter()の宣言が欠けているにもかかわらず、java.security.Provider#put()が含まれている理由を説明できない。説明できないので、後者は手作業であえてフィルタリングせずにXMLに出力している。)

解決済みだがハマった問題のひとつとして、Javaでは、基底クラスでnon-abstractであるメソッドと同じシグネチャーをもつメソッドを、派生クラスでabstractとして定義できてしまう。これによって、多くのクラスでtoString()メソッドがabstractとして再宣言され、XMLにも含まれているのを発見した。これらは先のオーバーライドされたメソッドの中でも、排除されないメンバーとして扱うことになる。

また、AOSPでは、java.lang.StringBuilderとjava.lang.StringBufferの共通の基底クラスであるjava.lang.AbstractStringBuilderが消滅している。この問題は、Javaの言語仕様上、publicなクラスがnon-publicな基底クラスを持ててしまうという(.NET開発者にとっては信じがたい)設計になっていることに原因があって、このAbstractStringBuilderはまさにnon-publicな基底クラスというわけだ。そしてこのクラスでgetDeclaringMethods()を呼び出したときの戻り値リストから、AOSPのXMLに合わせる作業も厄介なものになる(実のところ完全には行っていない)。

また、Class.getDeclaredMethods()が、インターフェースのメソッドのうち、宣言していないものを自動的に返してくるのだけど、これが基底クラスと派生クラスでジェネリック型引数が変わる(たとえば具体化する)と、改めて別のインターフェースメソッドが返ってくるので、混乱が生じる。これはジェネリック型消去の有無でさらに難しくなる。

余談だが、Javaではprotectedメソッドをpublicでオーバーライドすることが出来てしまう。Androidアプリケーションのサンプルの多くはActivityクラスのOnCreate()をpublicメソッドとしてオーバーライドしているが、.NETではprotectedでオーバーライドすることになる。

AOSP XMLのバグ

最後に、AOSPのXMLには、細かいところでさまざまな不足部分があって、クラスライブラリを正確に反映していない。GoogleにどれだけAOSPにコミットする気があるかを見るためのひとつの試金石として、androidのissuesに登録しておいた。
http://code.google.com/p/android/issues/detail?id=19569

ちなみにAOSPのXMLに相当するものは、最新のソースコードについてはAOSPのビルド過程でも生成される(out/target/common/obj/PACKAGING/android_jar_intermediaries/public_api.xml)しアノテーションが付加された状態のdebug jarもout/target/common/obj/JAVA_LIBRARIES/framework_intermediariesとかout/target/common/obj/JAVA_LIBRARIES/core_intermediariesとかに生成されるのだけど、AOSPのビルドには相当時間がかかるので、とても追求する気にはなれない。古いバージョンでも同じようにファイルが生成されるかどうかは分からないし、古いバージョンをビルドするにはJava 6とJava 5を切り替えたり…とても考えたくない。

まとめ

androidのライブラリをJavaリフレクションで眺めるとたいへん面倒だということがわかった。これは特に、ランタイムライブラリが分析対象と異なること、proguardが一枚かんでいること、Javaのerased genericsの問題、が大きい。でも、その辺の壁を乗り越えれば、フレームワーク全体を見通した汎用的なツールが作れるようになるかもしれない。