Xamarin.Androidで躍動感のある生態系を構築するにはどうしたらいいのか

https://qiita.com/advent-calendar/2018/xamarin の24日目エントリーです。

近況

たぶんアドベントカレンダーに近況報告なんて書いているのはわたしだけではないかと思いますが()、Xamarinを卒業してから真性無職エンジョイ勢です。10月くらいまでは無職だけど技術書典5の裏方やったりXamarin本の最新刊を1人で書いたりしていて割と忙しかったんだぜ…あ、紙の書籍版はComic ZIN秋葉原店にあると思います(技術書典会場で在庫をそのまま預けられるのでとてもありがたい)。boothは倉庫代が値上がりしてしまったので(それまでが安すぎた)だいぶ前に泣く泣く引き払っていて、今は電子版しかありません。

11月の半ばからはしばらく日本を離れて欧州旅行…なのかな…していました。本当は北欧とかに足を伸ばしたかったのですが、寒そうだったので未踏だった仏・伊のあたりを無難にフラフラしていました。建前としては、11/20-21にロンドンで開催されていたAudio Developers Conference (ADC) 2018に参加するための訪欧でした。ADC自体はJUCEを開発しているROLI社のイベントというのが実態に近いのですが、GoogleAppleMicrosoftも来てオーディオ関連のセッションを行う程度にはポピュラーやイベントです。日本からもクリプトンがスポンサーをやっていたりしましたね。

しかし音楽ツールやフレームワークの開発だけ調べていても心もとないので、しばらくは音楽制作とかを勉強してみたいと思っています。ADCに来ていた人たちも大半が自分で何かしら創作していたようだったので、地に足のついたことをしたいなあというお気持ちです。

Xamarinを卒業してからもしばらくはmonoには協力するつもりでいるのですが、そんなわけで他にやることが割とあったのでコードをcontributeしたりは特にしていません。最近だとdotnet/wpfが出たので、いよいよ昔やっていたSystem.Xamlの実装を置き換えられそうかなあとか期待していますが、先にpull requestが作られていたのでこれも他の人に任せておこう…というお気持ちです。まあ先の同人誌で世界で他に誰も書いていないであろうMono Compiler APIまで含めたJITの解説とか書いて出しているし、十分お釣りが来るレベルでしょ…(何)

本題について

そういうわけで今日はXamarin.Androidチーム在籍時代に作って放置していたツールをいくつか紹介しつつ、Xamarin.Androidで「躍動感のある生態系」を構築するにはどうしたらいいか、という大味な話を書きます。仕事になるなら最後までもっていってもよかったんですが、そこまで優先度上げられませんでした。すまんやで、という気持ちも多少は無くはないのですが、まあ自分が悪いわけでもないしな…ということで現状有姿です。こういうのを恥も外聞もなく出せるタイプのキャラクターでよかった…(!?)

xamarin-android-apitools

最初の数段落はどうでもいい話なのですが、前フリとして書いておきます。

api-merge

Xamarin.Androidでは、Android APIバインディングの実体であるところのMono.Android.dllについて、複数のAndroid API Levelのandroid.jarのAPI情報を吸い出してから、無理やり結合して、古いAndroidバージョンのAPIに対応するMono.Android.dllとの互換性を維持するMono.Android.dllを生成しています。

Android APIの実体であるandroid.jarは、意図的な古いメンバーの削除などでは快適変更が加えられたりはしていますが、それを除けば基本的にはJavaレベルでのABI(APIのバイナリ互換性)が保たれています。

これを単純にMono.Android.dllとしてバインドすればABIが維持されるかというと、そういうことにはなりません。これはJavaと.NETの違いによるもの(たとえば派生クラスのメソッドの引数型がcovariant/contravariantになる)と、Xamarin.Androidバインディング生成ツール(generator)の仕様によるもの(たとえばsetterしかなくてSetXxx()メソッドだったものが、getterも加わってXxxプロパティになる)があります。いずれにしろ、これをそのままにしておくと、Xamarin.Androidでは新しいAPIバージョンを追加するたびにAPIに破壊的変更が生じるということになり、Xamarin.AndroidのプロジェクトではTarget API Levelを変更する度にアプリケーションがビルドしなくなったりすることになります。これはJava/Kotlinを使っていると生じない問題です。

そういうわけで、Mono.Android.dllをビルドするときは、前述の「APIを無理やり結合」するステップが組み込まれており、通常のバインディング ライブラリのビルドとは異なる複雑な手順になっています。Mono.Android.dllのビルドが単純なバインディング ライブラリ プロジェクトになっていない主な理由もこれです。この結合ステップはapi-mergeと呼ばれるツールで実現しています。api-mergeは、全てのMono.Android.dllのビルドの過程で呼び出されるもので、API Level 28にもなると、マージ処理にもそれなりの時間がかかります…が、それは今回の主題ではないのでおいといて。

このapi-mergeによってAPI互換性が保たれ、Target API Levelを変更してもコードはそのままビルドできる、というわけです。これはxamarin-androidのunit testingにも組み込まれており、xamarin-androidmake run-api-compatibility-tests を実行してチェックできます(新しいAPIがstable APIとして登録されている場合のみです)。

api-merge everywhere...?

このapi-mergeの仕組みですが、Mono.Android.dllの他にも、複数のバージョンのライブラリの間で互換性を維持したほうがいいんじゃないかなあと思われるようなライブラリはあったわけですね。たとえばsupport-v4。どんどんバージョンが上がるので、nugetでパッケージのバージョンを上げたらビルドできなくなる、ということがあっても不思議ではないです。

そういうわけで、これらのライブラリについてもapi-mergeを利用できるように、api-mergeをバインディング ライブラリ プロジェクトの一部として組み込もうという動きが一時期あったのですが、「そこまでやっても得られるメリット無くない?」という感じの流れになって、計画は中断となりました。本家がAPIの破壊的変更を絶対に行わないポリシーで更新しているわけでもないし、基本的にnugetパッケージのバージョンを上げなければ良いだけですし、そもそもXamarinのAndroid Componentsが本家の更新に全然オンタイムで追いつけていないので「1つ前のバージョンでDeprecatedになってた」みたいなAPIがあってもすっ飛ばしていたら意味ないですよね…

xamarin-android-apitools...?

いずれにせよ、こういう背景から、複数バージョンのライブラリ間でAPI互換性をチェックできる仕組みがほしい、ということでわたしが手にかけていたのがこのxamarin-android-apitoolsです(長い前フリだった…!) ただ、↑のような流れもあって、無理にAPIの比較をすることもないかな…それよりむしろJavaAPIにあってバインディング側に無いものを探したり出来たほうがよいかな…みたいな気持ちで途中まで作って放置してあります。

実際には、バインディングの生成過程のどこかでAPIが欠落している可能性もあるので、API定義のXMLもチェックできるようにしたいと思って、それらもロードできるようにしてあります。

そんなわけでこのxamarin-android-apitoolsでは、複数のAPIデータソースからAPI定義をロードしてツリー表示するだけのツールとなりました(!?)。ロードできるデータは次のとおりです。

実のところ、現状ではAPI生成のどの段階で型やメンバーが欠落したかを調べるためのツールとしてのみ便利です(API比較機能が完成していればさらに便利なのですが)。表示もXwtで作ったショボいやつなので、あんまり便利ではありません。

xamarin-android-binding-automator

Androidエコシステムはsupport/jetpackを含む膨大なサードパーティ ライブラリによって成り立っており、これをいかに適切なかたちでXamarin.Android用にバインドできるかというのは製品の大きな課題です。

わたしにはかつて密かな野望(?)がありました。binding generatorをゼロから作り直して、Mono.Android.dllをゼロから生成し直して、サポートライブラリ(現在ではjetpackというべきでしょう)のバインディングを全部刷新して、GoogleAndroidリリースに即日とは言わないまでもすぐに対応できるようにしたい、というものです。

これは技術的な制約よりは政治的な制約と技術的負債(MicrosoftVisual StudioチームがAndroidのリリースサイクルを根本的に考慮しない、古臭いAPI Level 10のバインディングをいつまでも維持しないといけない、APIに破壊的変更は1ミリも加えられない、バインディングのあるべき姿をまともに検討する前にAPIをフリーズした、等)があって今後も一生実現しないのではないかと思いますが、今後類似の開発フレームワークがXamarin.Androidの失敗を繰り返さないために、総括しつつ、何をすればあるべき姿に近づけられたのかを模索しておくことは意義があるでしょう。

何でgeneratorやMono.Android.dllを書き換えたいの?

generatorを書き換えたいという野望は、実のところわたしに限らず多くのメンバーが口にして、一部のメンバーは開発にまで着手して、結局誰も実現できていないものです。ユーザーとしてのわれわれのフラストレーションをいくつか列挙して、それぞれについて検討してみましょう。

(1) jar/aarを渡したらそれだけでバインドできる範囲だけでDLLを生成してほしい。バインドできないメンバーは削る

実のところこれが大方のデフォルトの挙動であり、何もmetadata fixupを記述せずにビルドするとエラーになるものの大半は「これを無視するようにしたら結局ビルドが通らなくなる」類のものです(たとえばnon-abstractクラスでabstractメソッドのオーバーライドを自動的にバインドできないので無視するわけにはいかない)。

実のところ、現状でも「インターフェースのメンバーに問題があるだけでインターフェース全体が生成されない」と「指定された型がメソッドの引数や戻り値に使われていて生成できない」の連鎖的な組み合わせで、膨大な型やメンバーがバインドされないことはよくあります。デフォルトでバインドされない結果生じる問題を解決するのも、かなりの困難を伴います。

(2) 名前の衝突などは衝突しないように生成してほしい

FOOフィールドとgetFoo()メソッドがあるときに、どちらもFooプロパティになるので、このままでは生成できないのですが、一方が消えます。わたしはこれは両方生成した上でmetadata fixupを追加するよう促したほうがいいんじゃないかと思いますが、今さらこのgeneratorの挙動は変えられないだろうなあ…この挙動に依存してビルドが通っているバインディング、多分それなりにあると思うので。

これをFoo, Foo1みたいに生成するようにすると、ビルドエラーはなくなりますが、それはバインディングのユーザーが本当に求めるべきものとはとても思えないので(ちなみにこれをやっちゃったのがxsd.exeというかSystem.Xml.SerializationSystem.Web.Services.Descriptionですね)、「ビルドエラーが出ないようにしてほしい」という漠然とした要求に対して、盲目的にこういうアプローチを採用しなかったのは正解だったと思います。

(3) 我々は愚直なバインディングを求めている。StreamやXmlReaderはいらない

これはXamarin社内に古くからあった問題ですが、「われわれはJavaよりもうまくやれる資産がある」という耳ざわりだけは良いスタンスが強く、generatorはjava.io.InputStreamなどをわざわざSystem.IO.Streamにマッピングするような設計になっています。もっと設計的にビミョいのはXmlReaderで(というか当時の設計方針に沿ってわたしが実現したのですが…)、SAX APIXmlPullParserからXmlReaderを返すようになっています。

この辺は実のところ「じゃあどこまでJava APIでやるとビミョくないの?」という反対側からの視点があり、たとえばjava.util.ListをそのままJava.Util.Listとして返すのは適切なのか、現状のようにSystem.Collections.IListにしたほうが適切ではないか、みたいな話はあります。System.Stringで表されているjava.lang.StringをJava.Lang.Stringのインスタンスで返すようにしたら、アプリケーション開発は多分かなり面倒なことになるでしょう(implicit conversionが可能かどうかによる、かもしれませんし、Xamarin.Androidアセンブリ固有の変換処理に依存するとなると共有ライブラリのビルドが面倒になったかもしれません)。

しかしStreamやXmlReaderは明らかに「やり過ぎ」であり、これらはそのままJava APIで返しておいて、必要な場合のみこれらを相互変換できるラッパーを被せられるようにすればよかったのです(実際、内部的にはうっすらとしたラッパーが使われています)。Streamは特にinputとoutputの境界が曖昧になって美しくありません。XmlReaderにはさらに問題があり、これさえなければMono.Android.dllがSystem.Xml.dllに依存することは無かったのです。余計なアセンブリが挟まって貴重なスペースを無駄にしています。

少し弁解というかフォローを入れるとしたら、Mono.Android.dllもgeneratorも元々は全Java APIをカバーするようなことは意図しておらず、特に.NETのSystem.* APIが代替として機能する範囲はむしろ「優れているほう(われわれのAPI)を使う」「いらないものはバインドしない」という設計思想になっていたのです。

しかし、この考え方はgeneratorやバインディングプロジェクトを一般化させる上で、ただの邪魔者にしかなりませんでした。「いらないもの」としてバインドされなかったJava APIに依存するAndroidライブラリの機能は、generatorにとっては「存在しないAPIを使おうとするライブラリ」でしかなく、単にバインドされないという結果に終わるのです。特Java.Util.CollectionなどコアなAPIバインディング不在は、さまざまなバインディングのビルドに大きな悪影響を及ぼしました。

後方互換性という技術的負債

generatorやその生成物であるところのMono.Android.dllにいろいろ問題があることは分かりました。問題が分かっているなら改善すれば良いのではないでしょうか?

問題はそんなに単純ではありません。Mono.Android.dllはXamarin.Androidフレームワーク アセンブリであり、このAPIに全てのXamarin.Androidの生態系とXamarin.Formsの生態系が影響を受けます。

Xamarin.Androidは十分にバインディング生成機構が成熟していない時点で、十分な検討が行われない中でMono.Android.dllのAPIが断行されてしまい、結果的に中途半端に問題をかかえたAPIが残りました。しかし固定されたことに変わりはなく、これを破壊することはできないのです。

これは従来の.NET Frameworkが死に体になった状態と似ています。.NET Frameworkと異なるのは、Xamarin.Androidには.NET Coreに相当する「新しい部分」が無いという点です。

しかし古いものはいつか使用に耐えなくなります。それであれば、現在の生態系を切り捨ててでも、新しいバインディングの生態系を構築すべきではないでしょうか。これを目指したのが(ようやく本題に入った)このxamarin-android-binding-automatorを中心とする構想でした。

AndroidSupportComponentsの問題

Xamarin.Androidの現在の生態系を支えている重要な要素のひとつがAndroidSupportComponentsです。現在のXamarin.Androidがかかえている「Android本家の開発スタイルにXamarin.Androidが全く追従できていない」という大きな問題の根源がここにあります。Xamarinは「正式版がまだ出ていないから」などと悠長なことを言わずに、もう少し真剣にJetpackサポートなどを「新パッケージ = 即日対応」くらいのレベルで追及すべきなのですが、互換性問題などから完全に後手後手に回っている状態であると評価せざるを得ないでしょう。

(サポートライブラリのバインディングは、かつてはXamarin.Androidチーム側で提供していたのですが、Xamarinコンポーネントストアの発足に伴ってコンポーネント開発部隊が新設され、やがてサポートライブラリもそこに移管されてしまったので、総合的なXamarin.Androidエコシステムの設計を行える組織体系ではなくなってしまったということは、一因としてあります。)

AndroidSupportComponentsは現状、MacWindowsでしかまともに動作しないCakeによってビルドされる仕組みになっていて、2016年頃にわたしが手を加えようとした時には既に手遅れでした(それでCakeにプルリクを送って修正しようとしていた時期もあるのですが、そもそもCakeをビルドするdotnetがまともにLinuxに対応しない問題があり、dotnetチームはまともにやる気が無く問題が修正されない…という状況で、わたしはほぼ匙を投げました)。

xamarin-android-binding-automator.exe

AndroidSupportComponentsはいずれにしろ後手後手なので、こうなったら自前でバインディングのビルド生成機構を作ってしまえ…というわけで作っていたのがこのリポジトリです。現状ひとつだけ存在するツールxamarin-android-binding-automator.exeは、指定されたMavenのパッケージIDから、依存関係を丸洗いして、それぞれのjar/aarをダウンロードして、それぞれについて依存関係(*.csproj中のProjectReference)を追加しつつバインディングを生成するようになっています。MavenではパッケージIDからPOMと呼ばれるパッケージ記述ファイルをリポジトリから取得できるので、それを活用しています(解析はやっつけですが)。

もともとバインディング プロジェクトの構成要素は大きくなく、jar/aarとmetadata fixup、追加C#コードがあればほぼ足りるのですが、現状metadata fixupや追加コードは考慮されていません。バインディングプロジェクトではさらにソースコードのjarからパラメーター名を、javadocのjarからドキュメンテーションを取得できるので、それらは追加されます。

究極的にはMono.Android.dll(android.jar)も生態系の一部となるようにカスタマイズできるようにしたいのですが、Mono.Android.dllのビルドにはapi-mergeなど特殊な課題もあり、まずはsupport libraryから…という感じです(でした)。Mono.Android.dllは、かつてはバインディング ランタイムとしての役割も持っていたのですが、現在ではこれはJava.Interop.dllに移管されており(後方互換のためにMono.Android.dllにもランタイム相当の部分が残されています)、Mono.Android.dllを切り捨てつつ、あるいは完全に残したままで、新しいバインディングアセンブリ(たとえばXamarin.Android.dll)を構築することは、不可能ではありません(libmonodroidの側に"Mono.Android.dll"をハードコーディングしている部分があれば、それは切替可能なかたちに書き換えないといけませんが)。

切替可能な生態系の構築

Androidサポートライブラリ(旧)のバインディングを中心とする、現状閉塞感の強いエコシステムを書き換えるには、より迅速にバインディングを提供する仕組みがあるべきです。バインドされるライブラリには、他のライブラリへの依存関係があり、これらはMavenのパッケージの依存関係として記述・処理されます。Xamarin.Androidバインディングには、現状、Mavenパッケージの相互依存関係を記述する要素はありません。

また、Mavenパッケージの依存関係は、こちらの期待を外れて非互換なかたちで差し替わる可能性があります。Javaにおける型解決には「どのjarに含まれていたか」のような情報は存在しないので彼らは自由に組み合わせを変えることがありますが(Java9のモジュールまわりでこの辺の事情は変わる可能性はありますが)、.NETにおける型情報にはアセンブリ名も含まれるので、それほど自由ではありません。非互換変更が生じたら「生態系ごと切り替える」つもりで臨むしか無いでしょう。

そのためには、ABI互換性維持といったぬるいことを言っていないで、新しい生態系で全てを瞬時に新しく構築できることが重要であろうと思います。そして生態系の切り替えは世界線の切り替えにも近い…というとピンとくる方もいるかと思いますが、そうです、gitのブランチ切り替えがこれに親和的なのではないかと思います。誰でも自由に生態系を改善しつつ、必要なものはソースからビルドできる。これが理想の生態系システムではないかと思います。

総括

…と、風呂敷だけは広げたのですが、この構想自体はXamarin.Androidチームにいるとほぼ実現不可能なんですよね。政治的な問題もあるし(チームの切り分けとか)。Microsoftがわたしの作業環境であるLinuxでもきちんと.NET/Xamarin開発環境をサポートしていくような会社であれば、これを継続していたかもしれませんが、現実は残念な感じだったので、この辺の構想をまとめたあたりで「まあもういいでしょ」という感じになりました。(まあそもそもを言えばバインディングまわりが自分の担当というわけでもなかったのですが。)

jetpackに関しては、バインディングAPIだけでも大変なのに、roomやdata-bindingなどGradleのビルドシステムを統合しないと実現できないようなコード自動生成にどう対応するかという問題もあり、この辺の課題に対してXamarin.Androidでは答えを出せていません。この辺も「今すぐにでも」取り組むべき課題なのですが、XamarinチームはXamarin.Formsのbindingとかあるからいいでしょ…くらいにしか思っていないようです。

もっとも、Xamarinのコンポーネントチームでも、最近ビルドシステムを刷新してMavenからの自動取り込みも行うようになった?ようなので、ここまで抜本的ではないにしても、何かしらの小刻みな改善は施していくかもしれません。jetpack対応は以下のツリーで行われています。Xamarin.AndroidやXamarin.Formsのエコシステムに期待する人は、彼らの動向に注目しておくとよいでしょう。

https://github.com/xamarin/AndroidSupportComponents/tree/AndroidX