インサイドXamarin(6)
Xamarin.iOSで使用するライブラリ
Xamarin.iOS解説の後編。iOSの.NET APIである「monotouch.dll」や、Xamarin.iOS向けの追加ライブラリなどについて説明。
前回に続き、今回もXamarin.iOSを取り上げる。
Xamarin.iOS.dll: iOSの.NET API
.NETらしいコーディングを実現する工夫
Xamarin.iOS.dllファイル(前回説明したとおり、他に、Xamarin.WatchOS.dll、Xamarin.TVOS.dllも存在するが、これらは特に個別に言及しない限り、「Xamarin.iOS.dllと同様」とする)は、Objective-CベースのiOS SDKのAPIをC#/F#/CLI(.NET)から利用できるようにした、P/Invokeを基本とするライブラリだ。単に「iOSの機能をC#やF#で呼べるようにした」というだけではなく(もちろんそれだけでも難しい課題だが)、.NETらしいコーディングを実現できるように、さまざまな工夫が仕込まれている。公式ドキュメントに丁寧に書かれているが、部分的に例を挙げよう。
- Objective-Cのenumは、CLIのenumに変換される。
int
型の値でもenumとして使われるのが相当であるものについてはenumが定義される
- iOS SDK APIの引数型や戻り値型で使用される
NSString
型は、System.String
型に置き換えられる。ただし、NSString
型を表すクラスは存在し、そのインスタンスを生成し、メンバーを明示的に呼び出すこともできる
- iOS SDKにおける配列は
NSArray
型で、そこには要素型は存在せず不便だが(Objective-Cにはジェネリック(=Generics: ジェネリクス)も無い)、要素型が自明であるNSArray
型は、CLIの配列型(System.Array
)になる
- UIKitにおけるイベントは、iOS SDKのAPIでは
UIControl
クラスのaddTarget
メソッドを呼び出してイベントの種類とアクションを指定するだけの仕組みだが、Xamarin.iOSでは各イベントについてCLIのイベントが定義されている
また、iOS SDKが32bitと64bitを統一的にサポートし、開発者にもその使用を要求するようになったため、nint
、nfloat
といった型が新しく追加されており、Xamarin.iOS.dllでもこれらが使用されている。
ちなみに、Xamarin.iOS 9以前では、「monotouch.dll」というアセンブリでクラシックAPIがサポートされていたが、Xamarin.iOS 10からは削除されている。monotouch.dllのAPIには、MonoTouch
という名前が名前空間に用いられていたが、Xamarin.iOS.dllでは、これが無くなっている。
Objective-Cに由来する機能を実現する機構
一方で、Objective-Cに由来する機能をC#で使用するために必要になった複雑な機構もある。いくつか例を挙げよう。
- Objective-Cと相互運用するC#/CLIのクラスは、あらかじめ
RegisterAttribute
、ExportAttribute
などの属性で、Objective-Cのクラスと関連付けておく必要がある。これを実現するために、Xamarin.iOSでは、CLIのクラス側に属性を指定してマッピング情報を登録する。Objective-Cクラスに対応するCLIのクラスは、NSObject
クラスを継承する必要がある。実際の「monotouchランタイム」(=組み込みmonoにXamarin.iOS固有のコードを取り込んだランタイムを、そのように呼ぶ)への登録は、シミュレータービルドでは実行時にシミュレーター上で(動的に)、デバイスビルドではビルド時に開発機で(静的に)行われる
- Objective-Cのプロトコルは、メソッドの集合に名前を付けた「規約」であって、基本的にはCLIにおけるインターフェースに似た存在だが、プロトコルのメソッドには必須でないものが存在するため、これをそのままCLIのインターフェースにすることはできない。そのため、Xamarin.iOSでは、プロトコルをインターフェースにマッピングすることはせず、
RegisterAttribute
で.NETのクラスにプロトコル名を指定することで、その型が指定プロトコルを実装するものとして扱われることを明示し、そのメソッドやプロパティのアクセサーにExportAttribute
を付加して、そのメンバーがプロトコルのメンバーを実装していることを示すようにしている。こうすることで、optionalのプロトコルメンバーが省略できるというわけだ
- (もし必要が生じて)Objective-CにおけるセレクターをXamarin.iOSから直接呼び出すには、究極的にはP/Invokeを使用してObjective-Cランタイムの
objc_msgSend
関数を呼び出すことになるが、そのための関数宣言は引数型・戻り値型に応じて選択あるいは宣言する必要があり、そのためのルールはそれなりに複雑なものだ(詳しくは、セレクターの使い方のドキュメントを参照されたい。かつてはクラシックAPIのMonoTouch.ObjCRuntime.Messaging
というクラスに大量の関連メソッドが定義されていたが、現在のUnified APIではObjCRuntime.Messaging
クラスはinternal
となってしまっている)
プロトコルを実装する方法
Objective-Cのプロトコルとデリゲートは、WindowsフォームやWPF、Silverlightなら「イベント」、GTK#なら「シグナル」、Qtなら「スロット」の概念に類似する。Objective-Cでは、さまざまなコントロールについて、さまざまなインターフェースを宣言して、不必要なメソッドの実装を要求するような失敗を避けている。
開発者がプロトコルを実装する方法について、Xamarin.iOSはいくつかの選択肢を提供している。公式ドキュメントではUIWebView
クラスを援用しているので、ここでもそれを踏襲しよう。
- イベント、またはイベントハンドラー型のプロパティ: これは最も簡単なもので、これらのメンバーにイベントハンドラーを登録しておけば、それらのメソッドがプロトコルの実装となる(プロパティは、戻り値が期待される場合や、複数のイベントハンドラーが期待されない場合に、イベントの代わりに定義される)
Delegate
プロパティ(例えばUIKit.UIWebView
クラスのDelegate
プロパティ): これは、プロトコルのメンバーを全てvirtual
メソッドとして宣言した型(例えばUIKit.UIWebViewDelegate
クラス)で定義される
WeakDelegate
プロパティ(例えばUIKit.UIWebView
クラスのWeakDelegate
プロパティ): これはNSObject
型で定義される。このプロトコルのデリゲートは、前述のようにExportAttribute
で修飾されているメンバーが呼び出されることを前提としている
Objective-Cのバインディング・プロジェクト
Xamarin.iOSでは、開発者自らがObjective-Cのバインディング・ライブラリを作成することで、過去のライブラリ資産を流用することもできる。バインディング・ライブラリの作成は高度なトピックなので、ここでは簡単に触れるだけにとどめておきたい。
Xamarin.iOSのバインディング・ライブラリは、まずC#ソースの形式で記述されたAPI定義を開発者が用意し、それをbtouch
というツールに渡して、実際のバインディング・ライブラリの実装を生成することになる。開発者は通常はIDE上でバインディング・ライブラリを作成することだけを考えればよい。btouch
の呼び出しはプロジェクトのビルド処理で自動的に行われる。ちなみにbtouch
はモバイル用APIとしてのXamarin.iOSに特化したものであり、WatchOSにはbwatch
、TvOSにはbtv
というツールが、それぞれ用意されている。
ただし、API定義をゼロから全て手作業で記述するのは、骨の折れる仕事である。そのため、Xamarinでは、「Objective-Sharpie」とも呼ばれる、バインディングの自動生成ツールを用意している。一般的な開発者は、これを使用するとよいだろう。このツールは、ClangのAPIを活用して、Objective-Cヘッダーの内容を基に、Objective-CバインディングのC#コードApiDefinition.cs
ファイルの大半を自動生成してくれる。そのアウトプットを手作業で修正して、ようやく実用できるライブラリを作成することになる。このObjective-Sharpieを使用したバインディング作成のウォークスルー・ガイドも公開されているので、バインディングを作成する際にはこれを参考にされたい。
Objective-Sharpieのアウトプットだけでバインディングが完結することはほぼない。多くの場合は、生成されたC#コードに手を加えることになるだろう。Xamarin.iOSに限った話ではないが、コード自動生成ツールが問題を自動的に解決することなどほとんどない。銀の弾丸は存在しない。Objective-Sharpieは、開発者が生成されたコードを検証すべき部分に、意図的にビルドを失敗させるためのVerifyAttribute
を生成する。
Xamarin.iOSとXamarin.Androidは、バインディングのAPIを作成するアプローチが異なり、iOSにおいては、いったん生成したC#のドラフトとなるコードを手作業で完成形に持っていくアプローチになる。これは、Objective-Cのバインディングが(どちらかといえば)型/メンバー宣言中心の内容になっていることが理由として挙げられる(Androidのバインディングも、JNIの呼び出しが中心だが、引数や戻り値のマーシャリングコードが大量に生成され、いったん生成したコードを手作業で修正するのはかなり困難だ)。
そのような背景から、Objective-Cバインディングで、デザインパターンにおけるジェネレーション・ギャップのようなバインディングを作成するようなアプローチは、無理に採らない方がよいだろう。やり方にもよるだろうが、ギャップを埋めるための作業がそれなりに大変になる。
その他のライブラリ
Xamarin.iOSの主要な役割は、iOS SDKのAPIバインディングを提供することである。しかし、iOSアプリケーションをより簡単にビルドしたり、あるいは他のプラットフォームとコードを共有できるアプリケーションをビルドしたりできるように、いくつかの追加ライブラリと機能が提供されている。(もちろん、ここで紹介するiOSに特化したライブラリの他に、Xamarin.Formsのような選択肢があることは言うまでもない)。
1MonoTouch.Dialog
MonoTouch.Dialogは、C#のコードによって簡単なUIを簡単に作成できるようにするためのライブラリだ。UITableViewController
クラスを使用し、Section
とElement
というUI構成要素を用いて作成されたC#のUIコードから、属性の値などを基にネイティブのUIを構築する。
公式サイトにあるサンプルコードとそのスクリーンショット(次の画面)を見れば、いかに簡潔にネイティブUIが記述されているか分かるだろう。MonoTouch.Dialogの詳細は、リンク先のドキュメントを参照されたい。
2OpenTK
OpenTKは、クロスプラットフォームのOpenGL(Open Graphics Library)およびOpenAL(Open Audio Library)のC#ラッパーだ。この場合のクロスプラットフォームとは、OpenGLが利用できる環境、すなわち従来のデスクトップの(WinRTではない)Windows、Mono(Windows、Mac、Linuxなど)、iOS、Androidということになる(OpenALバインディングのサポートは、あくまでOpenALが動作する環境でのみ)。
このライブラリは、MonoGameという、クロスプラットフォームでXNAのAPIを提供するオープンソースプロジェクトでも使用されている(MonoGameは、OpenTKが利用できないWinRTやWindows Phoneでは、SharpDXを使用している)。
3Touch.Unit
Touch.Unit(Xamarin.iOS Unit Testとしてプロジェクト・テンプレートにも存在する)を使うと、NUnitLiteテストをiOSで作成して実行できる。テストの実行はiOSターゲット(シミュレーターおよびデバイス)上で行われ、結果はターゲット上で確認できる。
ユーザーインターフェースのテストについては、Xamarin Test Cloudでも使用されているXamarin.UITestというライブラリも有効活用されたい。
Xamarin.iOSの注意事項
Xamarin.iOSでは、iOSが動的コード生成を拒絶しているために、他のプラットフォームにはない独自の制約がある。まず、System.Reflection.Emit
名前空間のクラスがほぼ全て使用できない。LINQで式ツリーを構築するコードも、内部でDynamicIL
を使用しているため、同様に使用できない。DLR(動的言語ランタイム)も内部的にコード生成を利用している。リフレクション全般ではないので、動的に型をチェックすることはできるし、AOTによってコンパイルされているクラスであればロードして実行することもできる。
AOTの制約についてはXamarin.iOSのドキュメントで詳しく説明されているが、ジェネリックまわりの制約が大きい。これは主に2種類ある。前述のJITに起因する制約と、Objective-Cとの間でジェネリックをマッピングに適用する時点で限界があるものだ。
ただし、ジェネリックに関しては、原則通りに実装しているとiOS上での.NETの一般的なコードの動作が厳しすぎたこともあって、Xamarin.iOSバージョン7前後で、多少無理をしてでもコードが期待どおりに動作できるよう、いくつかの制約が撤廃された。Xamarin.iOS 6.3のリリースノートのページにおいて、バージョン6.3.7の変更点について、「ジェネリック・コード共有の改善: もう値型のサイズ制限はありません。型の特定されたジェネリックメソッドに加え、一般的なフォールバック・ジェネリック・コードも任意の値型で使用できます。NullableのBoxとUnboxもコンパイルします。」という説明がある。これを踏まえて、過去のものとなった制限事項については、下で別のグループとしてまとめておく。以下は現存する主な制限事項である。
NSObject
からジェネリック型で派生することはできない- ジェネリック型でP/Invokeメソッドは使用できない
Dictionary<k,v>
オブジェクトのキーに値型を使用する場合は、Default
ではないIEqualityComparer<t>
オブジェクト(=比較インターフェースの実装)を明示的に渡す必要がある。このDefault
のオブジェクトはリフレクションで生成されてしまうためだ
以下は2014年1月末時点(Xamarin.iOS 7.0.6)ではもはや存在しない、ジェネリックまわりの(旧)制限事項だ。
virtual
ジェネリックメソッドの呼び出しは、AOT時に静的に解決できないことがある(C++では使用できないジェネリックの使い方だ)。参照型の場合はそれでもおおむね問題ないが、それでもパフォーマンスは悪い- 大サイズ(現時点では12Bytes以上)の値型は使用できない
- ジェネリック値型の使用に際しては、AOTが静的に解決できるよう、その使用が明示的に検知できるようになっていることが必要だ。リフレクションによって間接的にしか参照されないような場合には、ネイティブコード生成が行われず、実行時に失敗することになる
Nullable<t>
にリフレクションを使用して値を設定することはできない
最後に、自明ではないがランタイムが暗黙的にコードを生成している部分が他にもあることに注意されたい。リモーティングのTransparent Proxyの実現にはJITが使用されているし、P/Invokeで.NETのオブジェクトをP/Invokeするネイティブライブラリの関数に渡してコールバックさせる場合にも、内部でコード生成が行われている。P/Invokeのコールバックには、明示的にMonoPInvokeCallbackAttribute
を付けることで、対応するコードがAOTで生成されるようになる。
【コラム】ジェネリックがAOTに影響する理由
なぜジェネリックがAOTに影響するのか。それは、C++のテンプレートなどと.NET CILのジェネリックの設計アプローチの違いを考えてみると理解しやすい。C++では、テンプレートはコンパイル時に静的に解決されるが、CILではジェネリックの解決は実行時に行われる。実行時に生成されるコードは、可能な限りコードを共有しているが(これを「ジェネリックコードの共有」という。こうしないと型引数が違うたびに発生するJITで、毎回大量のネイティブコードが生成されることになるのだ)、その一部はそのジェネリックインスタンスの型引数に特化している。JITが次のようなコードをネイティブコードにコンパイルするとき、
public void Foo<T>(T arg) { ... }
このメソッドに対応するネイティブコードで確保される引数用のスタック領域は、T
の型に依存して変わる。T
がSystem.Int32
型なら4Bytesだし、System.TimeSpan
型なら8Bytesだ。つまり、型引数が決まらないうちは、事前にコードを生成することはできないというわけだ。参照型であれば、System.IntPtr
型のサイズで固定できるので、これは問題にならない。これが値型にのみ強い制約があった理由だ。
Xamarin.iOSでは、引数型が値型である場合でも、ジェネリックコードの共有を可能にするために、ある程度の大きさまでスタック領域を固定したコードをAOTで生成することで、この問題をある程度解決している。これがサイズ制約が存在した理由だ。
(最新版でサイズ制約をどのように解決したかについては、残念ながら筆者にはドキュメントや記事が確認できなかった。)
■
最後に、やや細かい話だが、System.Data APIの使い方についても注意すべきことがある。
まず、System.Data.dllファイルはiOS環境でも存在しているが、Xamarin.iOSのAPIは(モバイル・プロファイルの回でも言及した通り)SilverlightのAPIに類似する最小構成であって、System.Configuration
名前空間のようなAPIは含まれていない。これに依存するDbProviderFactories
クラスは機能しない。
また、唯一含まれている外部DLLのMono.Data.Sqlite.dllファイルは、API上は制約がないものの、前提としているSQLiteのバージョンが3.5であり、iOSに含まれているSQLiteのバージョンとの間で、潜在的なミスマッチが生じている可能性は否定できない(とはいえ、SQLite 3.0を含んでいた過去のiOSでは明らかに機能不足だったのに比べて、iOSに含まれている方が新しいバージョンであるため、単に非互換の可能性についての懸念で済んでいるのは幸いであるともいえる)。GetSchema()
、GetSchemaTable()
などの機能や、DataTable
クラスを使用するAPIは、注意しておいた方がいいだろう。2016年現在では、SQLiteはサードパーティのPCLでも利用できるので、そちらを採用するのもよいだろう。
※以下では、本稿の前後を合わせて5回分(第4回~第8回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
4. Monoのモバイル化の流れ ― Xamarin.iOS/Xamarin.Androidの誕生
デスクトップ環境での動作を主眼に開発された「.NET」のオープンソース実装である「Mono」は、どのようにモバイル開発に向かって流れていくことになったのか。
6. 【現在、表示中】≫ Xamarin.iOSで使用するライブラリ
Xamarin.iOS解説の後編。iOSの.NET APIである「monotouch.dll」や、Xamarin.iOS向けの追加ライブラリなどについて説明。
8. Xamarin.Androidで使用するライブラリ
Androidの.NET APIに相当する「Mono.Android.dll」の特徴と注意事項、さらにAndroidサポートパッケージやGoogle Play Servicesについて説明する。