オープンソース化されたXamarin.Androidの概要

何がオープンソース化されたのか

Xamarin Evolve 2016で、以下のリポジトリがMITライセンスで公開されました。

XamarinComponentsは、どちらかといえば(以前からある)pluginsの内容を吸収した、他リポジトリにリンクする、ポータルのようなかたちになっています。

今回オープンソース化されたのは、Xamarin platform SDKという部分で、これは一言で言えば「スタンドアローンで実行可能なリリースビルドのアプリケーションをビルドできる環境」になります。ビルドして実行するコードは提供するよ、それ以上の開発環境は提供しないよ、ということですね。Xamarin Studioはオープンソースになっていません(言うまでもないですがMonoDevelopは昔からオープンソースです)。アプリケーションをデバッグ実行したい場合は、相変わらずプロプラエタリのXamarin StudioかVisual Studioを使う、ということになります(ちとがっかり)。

これに伴って、Xamarin.iOSもXamarin.AndroidもXamarin.Formsも、大幅なリポジトリの再構成を余儀なくされており、またプロプラエタリコードの切り離し作業もおこなわれているので、率直に言えば開発者もまだ何が起こっているのかよくわからない状態です。ビルドも公式にはMacしかサポートされておらず、他のメンバーがせっせと壊したLinuxビルドを試行錯誤して直す作業をわたしがやったりしていました(というか今でもしている感じですね)。

ともあれ、今公開されてあるソースは、昔からあるやつのコードとは多少違うものだ、ということです。

そんなわけで、ソースが公開されたので、今回はこれまで詳しく書いていなかったXamarin.Androidの細かい部品について、ちまちまと書いていこうと思います。あんまり読みやすくまとめようという感じではなく、思いついたことを書き並べている感じです。

xamarin-androidの使い方

(この1行は後で消しますが)現状ではビルドされるファイルが不足していて、動かせる環境にないのですが、それでは目的が達成できていないので、今後数日のうちに修正されていくと思います。

Xamarin.Android SDKを使うには、コンソールからプロジェクトのcsprojをビルドするしかありません。

xamarin-androidリポジトリをチェックアウトすると、tools/scripts/xabuild というスクリプトがあり、これが必要な環境変数などをセットアップしつつxbuildを呼び出すように作られています。xbuildの代わりにこれを使うと良いです。(もちろん、無償化公表前のXamarin.Androidとは異なり、コンソールからビルドするのにProfessionalライセンスを必要とするようなことは、もはやありません。)

基本的には、MSBuildタスクを含むXamarin.Android.Build.Tasks.dllが、xbuildがMSBuild extension DLLを解決するパス(MSBuildExtensionPaths)に存在していれば、あとはXamarin.Androidのプロジェクトをビルドするために必要なXamarin.Android.{CSharp|FSharp}.targetsファイルがそのDLLの中に含まれるEmbeddedResourceとして解決されて、Xamarin.Android固有のタスク(BuildApkなど)を処理できるようになるはずです。

最新Xamarin.Androidの構成要素

monoランタイム

monoランタイムはxamarin-androidのgit submoduleとしてチェックアウトすることになります。

Xamarin.Androidは内部的にNDKでビルドしたmonoランタイムを使用するため、アーキテクチャ依存のネイティブライブラリ libmonosgen-2.0.so を含むことになるわけですが、公表時点でのxamarin-androidは、(あろうことか)デフォルトではarmeabiでのみビルドされます。まあこれはAndroid開発者のことを全く理解していないAndroidチームのやる気の無さの表れというよりは、OSS化の方針が決まってからEvolveまでほとんど時間がなかったからというのが大きいです。

xamarin-android内部では、この embedded mono runtime とAndroidプラットフォームを結びつける libmonodroid.so が使われています。xamarin-android自身は、monoランタイムとJNIのinteropを実現しているわけではなく、Androidアプリケーションのブートストラップの過程でmonoランタイムを初期化して利用可能にするためのコードなどが含まれています(Java.Interopモジュールの登場以前はこの辺は一体化していたのですが、Java.Interopの導入以降は、そういう構成になったようです)。

バインディング作成ツール

OSS版Xamarin.Androidには、Androidプロジェクトを新規作成するためのツールは含まれていません(!)が、IDEが無くても、バインディング ライブラリは、自前で作成することができます(!!)。

バインディング ライブラリは、Xamarin.Androidにおける機能の一部にすぎませんが、Mono.Android.dllはこのバインディング ライブラリをサポートする機能に基づいて作られているものであり、Xamarin.Androidの根幹を成す機能であると言えます。

バインディングDLLはどのように作成されているのでしょうか? 基本的な流れは、次のようになっています。

  • jar2xml: Javaライブラリ(jar)を解析して、API定義XMLを生成する
  • generator: API定義に対する修正を適用しながら、C#ソースを生成する
  • csc/mcs: C#ソースをビルドする

最初の部分では、膨大なAPI定義のXMLを生成します。C#コードをAPI定義とするXamarin.iOSとは全然違いますね。なぜこんな設計になっているかというと、これはもともと必要ないはずのツールだったのです。Android AOSPにはXMLベースのAPI定義が含まれており、Android 3.0がクローズドソースで公開されるようになるまでは、Xamarin.AndroidはAOSPのXML定義を解析するツール(generator.exe)のみを使用していました。それがクローズドソースのandroid.jarが登場するようになり、このままでは対応できない状態がいつまで続くか分からなかったので、jarファイルを開いて解析できるツールが必要になった、というわけです。

また、この頃からAndroid supportライブラリなどが登場し、外部ライブラリのAPIを簡単に使えるようにするための汎用的な仕組みが求められてきていました。そのため、それまでandroid.jarだけを相手にしていれば良かっただけのバインディング生成ツールに、さまざまな修正を加えてそれなりに一般化することになりました。この時点で、たとえば、generatorはjarだけでなくDLLも解析しなければならなくなっています(APIの階層構造を把握するためには、全てのjarが必要になるわけですが、依存ライブラリのjarは無いかもしれないですし、そもそもバインドされていないかもしれませんし、DLL上では全く別の名前になっているかもしれません)。

既にそれまでの時点でgeneratorはかなり複雑化していたところに屋上屋を架すことになって、今は割と「誰も触りたくない」部品になっています。残念ながら今のところこれを書き直す計画はありません。

class-parse: 新しいAPI抽出ツール

実は、この最初の部分については、次回リリースから選択肢が増えます。この選択肢は、バインディング プロジェクト ファイル(.csproj。F#はバインディングライブラリの作成ではサポートされていません)にMSBuildプロパティAndroidClassParserで制御できます。そこには、次の値のいずれかを指定します。

  • jar2xml
  • class-parse

jar2xmlJavaのリフレクションとバイトコード操作ライブラリASMを使用して作成されていましたが、class-parseC#で実装されています。jar2xmlが抱えていた絶妙な問題として、Javaリフレクションによって取得されるAPIは、そのコードを実行しているJVMに束縛されてしまう、というものがあります(.NETのSystem.Reflection APIと同じような問題ですね)。Mono.Android.dllには、本来android.jarに無いはずのJava7のAPIへのバインディングが含まれている、というバグがあって、これを解決するのはjar2xmlでは根本的に無理があった(出来なくはないが設計が全く異なるものになる)、というわけです。IKVMのコードを使いまわしたり、javapの出力を利用してはどうか、といったアイディアがいくつか出てきましたが、最終的にC#で書かれたシンプルなバイトコードパーサーを作って終わりということになりました。

もっとも、class-parseが銀の弾丸となったわけでは全くありませんでした。jar2xmlに知らないうちに頼りきっていたわれわれが直面した問題は、バイトコード解析だけではきちんとしたクラス階層の処理ができない、ということです。たとえば、Javaではpublic型をnon-public型から派生できるわけですが、java.lang.reflectはそれをうまく隠してくれていたわけです。class-parseは愚直なパーサーで、その辺りを全く解決しませんでした。

class-parseの開発時は、jar2xmlの出力フォーマットとの互換性も全く気にしていなかったため(generator.exeも拡張してclass-parseフォーマットもサポートすればいい、程度に考えていました)、生成されたXMLに対してさらにMetadata.xmlなどによってAPI定義を修正しなければならないという問題もありました。

また、AOSP由来のXMLでは、派生クラスでオーバーライドされた実装メソッドなどの情報は含まれていませんが、class-parseは愚直なバイトコードパーサーなので、その辺の事情を全く考慮しません。つまりclass-parseの出力は情報の構成が変わってしまっているわけで、そのままgeneratorに渡したわれわれが目にしたのは、大量のコンパイルできないC#コードでした。

結局のところ、今まで通りgeneratorに仕事してもらうためには、class-parseの出力を変換して正常化しなければなりませんでした。以上のような問題をまとめて解決するために、新たにapi-xml-adjusterというツールが作成されました。これは、class-parseの情報をもとにクラスの階層構造を解決して適切な修正を加えつつ、API XMLの構造を変換する役割を分担します。

generator

generatorは以上のような過程を経て生成されたAPI定義XML (バインディング ライブラリ プロジェクトをビルドすると、obj/* 以下に、api.xmlというファイルが生成されるはずです)をもとに、指定されたMetadata API fixup(Metadata.xmlなど)を適用して、クラス階層構造を構築し、インターフェース メソッドなどを抽象クラスで適宜追加し、プロパティやイベントにメンバーを変換・追加して、最終的なDLLのソースを生成します。コードの生成はTextWriter.WriteLineで行っています。いちいちCodeDomなんて使っていません(CodeDomはResource.designer.cs/.fsの生成時に使っています)。

ここ数ヶ月の間に追加された地味な機能のひとつに、Android Annotationsのサポートがあって、たとえばRequirePermissionなどのアノテーションJavaソースに追加してビルドしてさらにきちんとaarにパッケージされたものについては、それを読み込んだ上で対応するattributeを追加するようにしたはずなのですが、肝心のandroid.jarに対応するAndroid SDKのplatform-tools/api/annotations.zip の内容が、いつまで経ってもLollipopの内容のままで止まっていて、たとえばandroid.media.midiAPIに対応するIntDefなどが追加されないままなので、Google仕事しろと思いながら日の目を見ない機能のひとつとなっています。

あとさらに細かすぎて伝わらない追加機能として、同じくAndroid SDKのplatform-tools/apiにある api-versions.xml を読み込んで、ApiSinceというattributeを生成するようにしたのですが、IDE側でこれに対応するメッセージが表示されているかどうかは未確認です(出るようにする、とは言っていました)。使っているAPIがminSdkVersionで指定したバージョンにに無く、コード上でバージョンチェックも通していないと、実行時エラーになってしまうので、簡単に回避できるようにはしたいですよね。

MSBuildタスク

Xamarin.AndroidMSBuildタスク…というより正確には一連のタスク群ですが…は、xbuildあるいはMSBuildでXamarin.Androidのプロジェクトをビルドするために必要なもので、Xamarin.Android.Build.Tasks.dllに含まれています。

このMSBuildタスク群の中には、Androidアプリケーションのプロジェクトで使われるAaptやBuildApkといったタスクの他、Binding projectのためのJarToXmlやBindingsGeneratorといったタスクも含まれています。

ただ、これまでの製品版と大きく異なる部分があって、fast deploymentなど、デバッグビルドのために存在していたタスクが、根こそぎプロプラエタリコードに持って行かれています(つまり、xamarin-androidには含まれていません)。やれやれですね。最新のプレビュー版にはInstant Runのcold swap相当のコードも実装してあるので、そこも公開されていると少し面白かったのですが。

ライブラリ

Mono.Android.dll

Mono.Android.dllは、複数のAndroidプラットフォームに対応する、やや特殊なバインディング ライブラリです。基本的に、サポートするAPI LevelごとにMono.Android.dllをビルドするのですが、それぞれのAPI Levelのandroid.jarの単純なバインディングを生成しているのではなく、それぞれのAPI Level以下のAPI定義をマージした上で、それに対するバインディングを生成しています。これは、Java言語のVMCLIでは、存在しないメソッドを解決するやり方が微妙に異なり、異なるプラットフォーム間でのandroid.jar(に対応するDalvikバイトコード)では生じない問題が、Mono.Android.dllでは生じることになるため、メソッドの定義を適宜前方互換にしてやるひつようがあるためである、と説明されています。

(その副作用で、いつまで経っても邪魔なOrg.Apache.Http*が消えない、といった問題を抱えています。)

以上のような理由などもあって、Mono.Android.dllのビルドは次のように、いくつかの点で特殊な構成になっています:

  • API定義XMLは、生成済みのものがリポジトリに含まれています。
    • API定義XMLは、それぞれのAPI Levelについて、class-parseとapi-xml-adjusterを使用して生成しますが、ビルド時には必要ありません(この辺りをハックしたい人や、新しいAPI Levelの登場時にそのサポートを追加したい人がやります)
  • Mono.Android.dllのビルドでは、まずapi-mergeが呼び出され、生成済みのAPI定義XMLがマージされ、その次にgeneratorがソースを生成し、最後にmcsがC#ソースをコンパイルします。
  • Mono.Android.dll用のメタデータXMLではなくCSVファイルになっています(!) これはXamarinのチーム内ではもともと使われていたフォーマットが、前述のような歴史的な理由で公開ツールとなった時に、仕様の不安定なCSVフォーマットは公開せずにXMLにするという決定が下されたためです。これはgenerator内部でXML形式に変換されて処理されます。

Mono.Android.dllのソースはgithub上の xamarin-android リポジトリにありますが、これは(名前から推測できる通り)Xamarin.Androidの(OSS版の)中心的なリポジトリです。Java.Interopやmonoをsubmoduleにしており、libmonodroidと呼ばれる、Androidアプリケーションのライフサイクル上で動作するmonoのホスティング環境を、Androidの各CPUアーキテクチャごとにビルドします。これには非常に時間がかかります。monoをひとつビルドするだけでも時間がかかるのに、それを(ランタイムだけとはいえ)アーキテクチャごとに行っているわけです。

5/4追記: ↑は現在は正確ではないかもしれません(libmonodroidがarmeabiのビルドしか行っていないため、多分こちらもarmeabiのみ)

xamarin-android リポジトリのソースは、大別して3つに分けられます。

  • ライブラリ (src/Mono.Android, src/Mono.Android.Export)
  • msbuildタスク
  • ビルドツール (ライブラリのソースを生成するツールなど)

Java.Interop.dll

Xamarin.Androidは、前回のstableリリースから、内部的な構成を大きく変えて、Mono.Android.dllの内部で、新しくJava.Interop.dllというライブラリを参照しています。ただし、公開APIの観点では、特に変更はありません。この新しいDLLは、Android APIバインディングを含まず、JNIを通じたJava APIの呼び出しを実現するために必要な機能だけを含むライブラリです。

Java.Interop.dllは、ほぼJNI呼び出し部分を最適化するために新規開発されたものであり(README.mdには「これはセカンドシステム症候群だ」と明言されています)、アプリケーション開発者が気にするべき部分は基本的にありません(もともと、このリポジトリは、メイン開発者が実験的な遊び場として使っていたという側面が大きいです)。

github上の Java.Interop リポジトリには、Java.Interop.dllと、これに基づくバインディングを作成するためのビルドツールの機能を実装するDLLがいくつか含まれています。現状、このリポジトリには、実際の製品で使われていない実験的なコードも数多く含まれているので、ここで全てを説明することはしません。

Java.Interopに含まれるソースは、大別して2つに分けられます。

  • ライブラリ。src/Java.Introp がその中心となる、Java.Introp.dllをビルドするためのプロジェクトです。
  • 公開ビルドツール用ライブラリ。Java APIバインディングを生成するためのツール(generator.exeなど)の機能を実装しているライブラリが、ここに含まれます。

この他に、ビルド時にしか使用しないツールなども含まれているのですが、重要性は低いので省略します(JNIEnvクラスの大量のメンバーを自動生成するツールなどがあります)。

Java.Interopは、開発者が何を思ったのかMSBuildの仕組みだけで全てビルドできるように複雑なビルドスクリプトをセットアップしたのですが、Windows上ではビルドしないわ、Linuxビルドも壊れているわ、Android SDKダウンロードまでスクリプト化したもののエラー トラッキングなどがまともに出来ないわ、カスタムSDKセットアップの柔軟性は微塵もないわで、端的に言ってこのビルドスクリプトは失敗作です。が現状それしかないので我慢するしか無い。

それはさておき、Java.Interop.dllがあると何が出来るかというと、たとえば、このコア部分だけを再利用したまま、全く別のAPIマッピング理論に基づくバインディングDLLを構築して使用することができます。当然ながら、そうすると過去の遺産は使えなくなるわけですが、support-v*ライブラリなどは自分でビルドすることもできるため、多くの人にとっては、あまり問題にならないでしょう。特にXamarinが提供してきたGoogleのライブラリに対するバインディングは、ソースが公開されることが期待されていますXamarinComponentsリポジトリでソースが公開されています。

(今のところ、XamarinでMono.Android.dllのAPIを抜本的に差し替える予定はありません。)

もちろん、Xamarin.AndroidAndroidアプリケーションを作るためのものであり、Androidアプリケーションは、Androidアプリケーションのライフサイクルに従ったものでなければなりません。既存のMono.Android.dllを使用しなくても、その根本に変わりはありません。