6/24に行われたジェネリクス勉強会で、珍しく(?)ランタイムよりのトピックでジェネリクスについて語るセッションを担当した。generics on Xamarin productsという題目である。
ジェネリクスに関連するトピックなら何でもありという勉強会で、結果的に非常に濃密な内容のセッションだらけで、とても良い内容だったと思う。当日の様子はtogetterにまとめられている。
わたしの発表したスライドについては、以下で公開しているのだけど、
スライドだけではどうにも理解が難しい内容がいくつかあると思う。今回の発表内容について詳しくまとめていたので、事後になってしまってやや残念感があるのだけど、公開しておきたい。*1
C#のジェネリクス
「Xamarinの」ジェネリクス、というトピックは、何が特殊なのだろうか?
XamarinはMonoを基盤とする開発・実行環境であり、C#標準に忠実に従っている。
まず、ジェネリクスにはいくつかのバリエーションがあることを理解する必要がある。C#のジェネリクスについて、C++のテンプレートプログラミングと同じようなものだと見なしていたり、Javaのジェネリクスと同じような挙動を期待していると、いろいろ食い違いが生じてしまう。
C#のソースはCIL(MSIL、DLLやEXE)という仮想マシンコードにコンパイルされ、CLIの実行環境によって実行される。このCILの中には型の定義や参照が入っているが、この中にはジェネリックな型情報が含まれている。
それがどうした、と思うかもしれないが、これはJavaの仮想マシンコードには存在しない特徴である。Javaコンパイラが生成するバイトコードはジェネリックな型情報を「含まない」(ここではざっくりと「含まない」と書くが、実のところメタデータとしてはジェネリック型情報がある程度含まれている)。そのため、実行時にはジェネリック型は「存在しない」。
また、標準C++では、そもそもコンパイル時に型情報が全て解決されて、実行時には型情報を使ったメタプログラミングを行うことはできない(C++方面にはRTTI・実行時型情報*2というものがあるが、メタプログラミングを行えるとは言いがたい。同様のトピックはDelphiにもある)。
(SwiftやObjective-C、Rustを話に含めると、static dispatch、virtual dispatch、dynamic dispatchといった概念も取り上げた上でジェネリクスを論じないといけなくなるので、ここでは割愛したい。)
これらに対して、C#のジェネリクスは、実行時にも存在している。このため、たとえばC#ではIList\<int>とIList\<double>は実行時にも異なる型になるし、C#におけるFoo
とFoo\<T\>
は別の型になる((実際、System.ServiceModel
名前空間にはChannelFactory
とChannelFactory\<T\>
がある))。ECMA C#はECMA CLIを前提としており、ECMA CLIではこのジェネリックな型を実行時に適切にインスタンス化して実行する必要がある。
このようなジェネリック型システムをreified genericsという*3。
C#コンパイラと.NET実行環境の選択肢
プラットフォームとランタイムの組み合わせ
2017年現在、.NETプラットフォームは、大部分がMicrosoftによって提供されているものだが、実装は単一ではない。Windowsデスクトップやサーバー環境で動作しているのは多くが.NET Frameworkであろう。Microsoft系プラットフォームではWinRTやUWPが使われているかもしれない。Windows以外のデスクトップ環境では.NET Framework互換環境としてMonoが使われる。クロスプラットフォームでGUIの無いPC用アプリケーションや一部のサーバーアプリでは.NET Coreが使われているかもしれない。モバイルではXamarinが使われている(ひとことでモバイルと書いたが、iOSとAndroid、watchOSやtvOS、Android Wearなどは少しずつ異なる)。そしてゲームではUnityも大きい。
これらは全て異なる実行環境であり、その動作の仕組みはプラットフォームによって異なっている。とはいえ、大まかには、ランタイムが.NETに由来するものか、Monoに由来するものかで分けることができるだろう。
platform | .NET Framework | UWP | .NET Core | Mono | Xamarin | Unity |
---|---|---|---|---|---|---|
runtime | CLR | CLR | (Core)CLR | Mono | Mono | Mono |
API availability | full | subset | subset | full | subset | subset |
Windows PC | o | o | o | (o) | N/A | o |
macOS | N/A | N/A | o | o | o (Xamarin.Mac) | o |
Linux | N/A | N/A | o | o | N/A | o |
iOS (,watchOS,tvOS) | N/A | N/A | N/A | N/A | o | o |
Android (, Wear, TV) | N/A | N/A | N/A | N/A | o | o |
Windows mobile | N/A | o | N/A | N/A | N/A | o |
Tizen | N/A | N/A | o | N/A | N/A | o |
あるいは、もっと簡潔にこうまとめても良いかもしれない
Framework | Runtime | Platform |
---|---|---|
.NET Framework | (CLR) | Windows only |
.NET for WinRT | (CLR) | Windows 8.0+, WinRT |
UWP | (CLR*) | Windows 10+ |
.NET Core | (CoreCLR) | Windows, MacOS, Linux, Tizen |
Mono | (Mono) | MacOS, Linux, Windows |
Xamarin | (Mono) | iOS(,watchOS,tvOS), Android (,Wear,TV) |
Unity | (Mono) | almost anywhere |
いずれも細かい表ではあるが、それでもこのまとめ方はある程度「雑」で「恣意的」であることに気をつけておいてもらいたい。たとば.NET Frameworkに含まれるWindowsプラットフォーム固有のAPIに依存するもの(WPFやWFやSystem.Managementなど)はMonoでは実装されないが、それはここでfull
と記すことに何ら問題はないという前提である。
今回のトピックはXamarinプラットフォームであり、XamarinのランタイムはMonoなので、以降、実装に踏み込むときは、Monoを探索していくことになる。
ちなみに、Xamarinと銘打たれた製品であっても、Xamarin.Formsのようなクロスプラットフォームライブラリの実行環境は、それぞれのプラットフォームによるものであることを意識しておいたほうがよい。たとえば、Windows MobileやTizenで動作するXamarin.Formsは、実際にはUWPや.NET Coreの上で動作している。これらはXamarinのUIフレームワークではあっても、Xamarinプラットフォームで動作しているわけではなく、今回のトピックからは外れることになる。
C#コンパイラ: cscとmcs
.NETプラットフォームにおけるC#コンパイラは、ランタイムほど多様ではない。簡潔な表になるがまとめておこう。
.NET <= 4.5 | .NET > 4.5 | .NET Core | Mono < 5.0, XS | Mono >= 5.0,VSMac | Unity |
---|---|---|---|---|---|
classic csc | Roslyn csc | Roslyn csc | mcs | Roslyn csc | mcs |
この先を読み進める前に、ひとつ注意しておきたいことがある。この説明は、あくまでSDKに含まれるコンパイラの話であり、IDEを経由したビルドではIDEの規定するコンパイラがということだ。特にXamarin向けのコードをビルドしている場合でも、Windows上でVisual Studioを使用している場合は、コンパイラはcsc(classicあるいはRoslyn)であり、そのことはビルドされたコードの実行に際して問題にならない。Xamarin製品でも使用されているMonoランタイムは、CIL準拠のコードを正しく実行できる。
Unityはやや特殊なので説明が必要だろう。Unityは2000年代からMonoをゲーム用に改造して極小化したプラットフォームであり、開発にはMonoDevelopを用いていた。そのランタイムは当時MonoプロジェクトをリードしていたNovell社から商用ライセンスを受けていた(そうしないとLGPLになり、商用ゲームの配布に困難を来すものであった)。しかしNovell社がAttachmate社となってMonoチームを切り離した後、XamarinはAttachmateとライセンス提携によって自由にmonoランタイムを利用できるようになったが、Unity TechnologiesではXamarinが改造を加えた新しいランタイムを使用できなくなった。もともとUnityはMonoランタイムに大きく修正を加えており、追従は簡単ではなかったこともあってか、Xamarinと改めてライセンスを契約することはなく、Unityは古いMonoを使い続けることになった。
この状態が、MicrosoftがXamarinを買収してMonoランタイムをMITライセンスに変更するまで続き、それまでは、C++とAOT(事前コンパイル)を前提としたIL2CPPのような仕組みによって、C# 6.0までのサポートが提供されていた(monoのコンパイラmcsはMITライセンスであったため、MonoDevelop上から最新のものを利用することができた)。Unity 5.4で最新のmonoランタイムに合わせたことになる。
もっとも、Xamarinの最新版はUnityの最新版よりさらに先を行っている。Mono 5.0から、C#コンパイラはRoslynに完全に置き換えられた。このMono 5.0は、Visual Studio for Macの正式版リリースとタイミングが合うように公開されたもので、つまりこの時期にリリースされたXamarinの各製品 (iOS/Android/Formsなど)は、Roslynを前提としたビルドシステムでビルドして実行できるようになっている。
Roslyn(csc)とmcsは何が違うかというと、まず(1)C# 7.0以降をサポートしているのはRoslynのみである。tupleやtype switchをmcsで使用することはできない。また(2)cscが生成するデバッグシンボルはpdbまたはportable pdb(拡張子はいずれも.pdb)であるのに対して、mcsのデバッグシンボルはmdbであり、これらは異なるフォーマットである(これはECMA CLIで標準化されていなかったのである)。Windows以外の環境でcscで /debug
を指定して実行するとエラーになる。pdbはWindowsプラットフォームを前提としたフォーマットだからだ。他にもいくつか違いがあるが、基本的にはmcsにはもう機能追加やバグフィックスがなされない、ということだ。
Unityはまだmono 5.0を前提とした製品になっていないが、いずれ追従するだろう(ただ、C# 7.0の機能に必要なライブラリがサポートされるかどうかはわからない)。
本節のまとめ
説明がだいぶ長くなってしまったので、この節の要点を2つだけまとめておこう。
Monoランタイムにおけるジェネリクスの実装
前節では、C#コンパイラの違いはXamarinプラットフォームでアプリケーションを実行する際に有意な違いはほぼ無い、ということを説明した。一方で、Monoランタイムによるアプリケーションの実行方式は、有意な影響をもたらす。その理由は後で詳しく説明するが、まずはその前提として、Monoが特にジェネリクスに関連してどのようにコードを実行するのかを説明しておこう。
EEと実行方式
ECMA CLIでは、CILプログラムのコード実行エンジン(Execution Engine、EEとも呼ばれる)の方式を規定していない。仮想マシンコードを実行する方式には、主に、仮想命令コードを逐次読み取ってランタイム自身が解釈して実行するインタープリター方式と、仮想命令コードを実際のCPUの命令列に翻訳してそれをCPUに直接実行させるコードジェネレーター方式があり、後者にはさらに実行時にこれを行うJIT(ジャストインタイム)コンパイル方式と実行前に行うAOT(事前)コンパイル方式があるが、どれもECMA標準上は問題ない。
Monoランタイムは、開発当初2年ほどはmintと呼ばれるインタープリターでCILコードを実行する方式であった。これは実装が最も簡単であったためである。コードジェネレーターは、CPUアーキテクチャごとに実装を書き分ける必要があり、これには少なからぬ労力が必要になった。しかしいずれにしろ、ほどなくMonoの主な実行エンジンはminiと呼ばれるJITエンジンとなった(現在でもJITが主流であると言ってよい)。
MonoでAOTが求められるようになったのは、RAM容量の小さい組み込み環境でMonoを使いたいというニーズが上がってきた頃からである。コードを事前コンパイルしておけば、それをROMに焼いてしまうことでMonoを利用できるようになる、というわけだ。AOTは、この時点では後で説明するような理由により「不完全」なものであったが、不完全であることはほぼ問題ではなかった。
どの方式にも長所と短所がある。インタープリタはランタイムで簡単に実装でき、クロスプラットフォームで実行できるが、実行速度では他の方式に劣る。それ以外の方式ではCPUアーキテクチャごとにコード生成実装が必要になる。JIT方式であれば事前にコードをプラットフォーム別にコンパイルしておく必要は無いが、変換処理は高速であることが求められるし、また実行時には生成されたコードのためにより多くのRAMメモリを必要とする。AOT方式であれば変換処理に時間がかかっても大きな問題はなく、コードはROMメモリに焼いておくことができるが、CPUアーキテクチャごとに生成しておく必要があり、また実行時にならないと必要がコードがわからない状況に対応できない(この問題は後で詳しく説明する)。
EE | 実装の移植性 | パフォーマンス | RAM消費量 |
---|---|---|---|
interpreter | easy | bad | △ |
JIT | hard | good | ☓ |
AOT | hardest | good | ◯ |
AOTとJITのどちらが高効率のコードを生成できるかというと、AOTのほうが良い傾向があるが、実のところコードに依存するので(最適化手法にもよる)、事前にコンパイル処理が完了しているAOTのほうが早いとは一概には言えない。
オブジェクトレイアウト
ランタイムが解決しなければならない課題のひとつは、CILメタデータに基づき、オブジェクト構造をメモリ上に展開して、実行すべきコードの所在を適切に解決することである。クラスにはメソッドがあり、それらは継承関係によってはvirtualであり、あるいはoverrideであり、その状態次第で実行されるメソッドが変わる。それらを、エントリポイントから順次解決していく必要がある(そして、呼び出されるメソッドはEEによって実行される)。
オブジェクト構造は、通常は、型情報が必要になった時点で、ランタイムがメモリ上に生成する*4。オブジェクトのレイアウトはクラスあるいはインターフェースの定義から固定できる(インターフェースの場合は、実際のクラスからそのインターフェースへのメンバーのマッピング処理が挟まることになる)。いったんメモリ上に生成した型情報は、その型が利用される限りは何度でも再利用でき、再度オブジェクトレイアウトの生成処理を行う必要はない。
というのが大前提である。
さて、ここにreified genericsが絡んでくるとややこしくなる。ある構造体Foo\<T\>
にint型のメンバーA、T型のメンバーB、object型のメンバーCがいるとしよう(StructLayoutはSequentialであるとする)。
[StructLayout (LayoutKind.Sequential)]
struct Foo<T>
{
public int A;
public T B;
public object C;
}
このオブジェクトのレイアウトに必要なメモリサイズは、Tの型が決まらないと分からない。Cのように参照型のメンバーについては、実際のオブジェクトへのポインタを格納するだけで足りるが、Tは構造体かもしれないし、このFoo型をどの型でインスタンス化するか*5によって、必要なサイズが変わる。
もうひとつ、今度は図解も交えて例を出そう。次のようなジェネリッククラスがあるとする。
class G<T> {
void M (int X, T Y, double Z {...}
}
このクラスのメソッドM
の呼び出しから生成されるネイティブコードのメモリレイアウトは、T
の型次第で変わる。
Tが参照型である場合は、ポインタサイズで固定できているが(上記の例ではMarshal.SizeOf(IntPtr) = 4
の環境を前提としている。64ビット環境ならSizeは8になろう)、それ以外の型引数については、T
の型次第でメモリレイアウトが変わっていることが見て取れるだろう。
こうなると、オブジェクト構造は「インスタンス化されたジェネリック型」に特化して生成すれば良いのではないか、とも思えるが、そうなるとList\<int\>
、List\<byte\>
、List\<double\>
…と、インスタンス化が必要な型は組合せ爆発的に必要になり、その割にはほとんどの内容は同一なので、メモリ効率が非常に悪い。
そういうわけで、monoランタイムでは、ジェネリック型については、型パラメータに依存する部分については特別に解決することにして、それ以外の部分は一本化してコード生成することにしている。これをgeneric code sharing(あるいはgeneric sharing、shared genericsなど)という。
これはerased genericsでは起こらない問題とも言える。erased genericsのランタイムにはジェネリック型情報が無いので、List
monoにおける実装の詳細についてはGeneric Sharingのドキュメンテーション ページを参照。このドキュメント自体はランタイム実装者のためのもので、RGCTX(runtime generic context)といった単語が説明なく用いられているので読みにくいが、特にこのページからリンクされているランタイム開発者のblog postsはgeneric sharingの何が難しいかをいろいろと解説している。
ちなみにCLRの実装もmonoと同様のジェネリック共有が実装されているはずである*6。特にNGenについては、事前に既知であるジェネリックインスタンス型についてはコード生成していると説明されている。
この辺りの問題はLLVMベースのコンパイラでも、不要に膨大なコードを生成しないために必要となるはずだが、その辺りは今回調べるための時間が無かったので、詳しい人が解説してくれることを期待したい。また、この種の問題はJava方面で進行中のVarhalla Projectについても言えるはずなので(この辺りに詳しく載っているかもしれない)、この方面でも知見が広まると面白いと思う。
iOSのジェネリクス制約
iOSの制約とAOTがもたらす問題
CLIのEEについて解説したとき、JITとAOTについては、大まかな違いしか解説しなかったが、事前にコードを生成しなければならない、というのは、実行時にならないと正確な型情報が分からずオブジェクトのレイアウトもできないジェネリクスにとっては、大きな問題である。
実のところAOTの実装は「不完全な」AOTであった、と書いていた理由はこの問題のためである。それでも、ジェネリックコード共有があれば、組み込み環境でも実行時に必要最小限のコードを生成すればよく(そのためにJITエンジンを搭載する必要は生じるが)、コードの実行が根本的に不可能になる、という性質の問題ではなかった。
しかし、時代は代わり、アプリケーションによる実行時コード生成が全く行えない環境が登場する。iOSである*7。iPhoneのアプリケーションはAppleによる審査制であり、不正なコードを事後的に実行されないように、動的コード生成が禁止(明示的なAppleの許可設定が無い限り実行不可能)になっている。
Xamarin.iOSの前身はMonoTouchと呼ばれる製品で、2009年にリリースされたが、monoのAOTはこの頃にUnityでの成果から学びつつ実装されていた。iPhoneアプリにおける動的コード生成は当初から禁止されていたので、MonoTouchのリリース時には既にジェネリクスの利用には制約がある旨の注意書きが出されていた。
ちなみに、Xamarinのモバイル製品のAPIは、当初はSilverlightのCoreCLR(現在の.NET CoreにおけるCoreCLRとは、概念的には類似するが別物である)に関連付けられた、最小限のクラスライブラリに、非ジェネリックコレクションやXML DOMやXmlSerializerなど、もう少し古いAPIも付け加えたものだったが、もしSilverlightのAPIしか無かったら、根本的にコレクションAPIの利用に困難を来していただろう。
Generic Value Type Sharing
このままでは実際のC#アプリケーションを作成する際に大きな制約になることは明らかだったので、Xamarinではこの問題を解決する仕組みを独自に開発した。それが、この節で説明するgeneric value sharingである。
問題の根本は、実行時にならないと解決しないようなジェネリック型の利用場面で、必要なスタックメモリのサイズが確定できないことにある。この問題を解決する方法は2つ考えられる。
(1)gsharedvtについては「十分に大きい」サイズの値型用メモリを確保しておくことによって、ある程度の大きさまでの値型なら、実際の型を問わず格納・実行できるようにすればよい。筆者の理解では、Xamarin.iOS 6.4時点で導入されたgsharedvtの実装はこのアプローチだ。このメモリレイアウトを先程までの図に倣って示す。Xは固定サイズである。
(2) gsharedvt な引数については、実行時ジェネリック情報をもとに、実際の値型に必要なメモリスペースをスタック領域に確保し、その後CPUアーキテクチャ別のトランポリンを使って、実際に必要なメモリレイアウトを構築してから呼び出す。こちらが現在のmonoランタイムで採用されているアプローチである。ヒープ領域にメモリ確保しないので、GC呼び出しを発生させることはない(これはILのlocalloc
相当の呼び出しで実現しているようだ)。monoランタイムは、メソッド呼び出しにgsharedvtである引数が含まれていることを検出すると、トランポリンコードを使って、スタック領域に呼び出し先のメソッドが期待している引数のメモリレイアウトを形成し、メソッドを呼び出し、その後に以前の状態にメモリを復元する作業を行う。
このgeneric value sharingは、2016年にXamarinプラットフォームがオープンソースで公開される前は、Xamarinのプロプラエタリなmono拡張として非公開状態であったが、2016年にOSS化されてmonoに取り込まれている。gsharedvtというキーワードでソースコードを探索すると、mono/mini
ディレクトリ以下にmini-(arch)-gsharedvt.c
やtramp-(arch)-gsharedvt.c
というソースが見つかるはずである。また、OSS化に伴って、ドキュメントも追加されている。
ちなみに、この機能はXamarin.iOS 6.4で導入された当初はiOS固有のオプションで、mtouchコマンドの引数に-gsharedvtを追加しないと有効にならなかったが、現在ではこのオプションには意味はなく、必ず有効になっている。プラットフォームもiOS限定ではない。ただし、もしかしたらgsharedvtが有効なCPUアーキテクチャは限られているかもしれない((monoのソースコード中にmono_arch_gsharedvt_sig_supported
というMONO_INTERNALでマークされた関数があるが、これを呼び出している部分の存在は確認できなかったので、もう全てのアーキテクチャで有効になっている可能性が高い。))。
Xamarin APIにおけるジェネリクス
ここからは、少しだけ実際のプラットフォーム ネイティブAPIとMonoランタイムのinteroperabilityについて言及しておこうと思う。
Objective-CとC#のInterop
Xamarin.iOSおよびXamarin.Mac - 毎回これらを併記するのは面倒なので、以降はOSSリポジトリ名に合わせてxamarin-maciosと書くことにする - のプラットフォーム ネイティブAPIはObjective-Cが基本となっている。xamarin-maciosの基本的な仕組みは、monoのembedded APIとObjCRuntimeを経由した、CLIとObjective-Cの相互運用である。Objective-Cはdynamic dispatchなど実行時にバインドすることを前提としている。Objective-CのAPIと相互運用する.NETのオブジェクトは、Foundation.NSObjectクラスを継承していなければならない。
Objective-Cには、本当のジェネリクス…もといreified genericsは存在しないので、C#のジェネリック型引数をObjective-CバインディングAPIで定義するためには、そのジェネリック型引数をNSObjectから継承させなければならない。バインディングAPIはRegisterAtributeを使って明示的に定義するか、既存のバインディングをオーバーライドするものであり、バインディングでないAPIでは意識しなくても良い。また、API定義のみの問題であり、メソッド本体の中などでは、NSObjectから継承していないものでも利用できる。その型参照はmonoランタイム上で解決されるためである。
また、インスタンス化されたジェネリック型(instantiated generic type)をバインディングAPI定義に含めることはできない。ObjCRuntimeにはreified genericsが無いため、インスタンス化されたジェネリック型を解決できないためである。これは型名解決の問題なので、その型から派生する型を定義すれば良い。
public class GenericUIView : Generic\<UIView\> { ... }
xamarin-maciosとジェネリクスに関する制約については、その問題に特化した公式ドキュメントのページがあるので、詳しくはこれを参照されたい。
Xamarin.Androidにおけるジェネリクス
Xamarin.AndroidのプラットフォームAPIはJavaであり、Xamarin.AndroidはJNIとmonoのembedded APIを経由して、CLIとJava APIを相互運用している。Javaもerased genericsの世界であり、バインディングAPIにはJava.Lang.Object型のインスタンスを渡す必要がある。もっとも、Javaはインターフェースプログラミングの世界であり、Javaのインターフェースに対応するXamarin.AndroidのインターフェースをJava.Lang.Object(java.lang.ObjectにバインドするXamarin.Androidのクラス)から派生させることはできないので、バインディングのCLI側インターフェースではIJavaObject
というXamarin.Androidのインターフェースを継承させる。Java.Lang.ObjectクラスはIJavaObjectを実装しており、Android APIに対応するXamarin.Android APIのクラスは、全てJava.Lang.Objectから派生している。
Android APIに対応する型は、基本的にジェネリック型ではない(ごくまれにジェネリック型のバインディングが存在するが、これは便宜上作られた手書きのクラスである)。これは、JNI経由でCLIオブジェクトを生成する場合にジェネリック型をインスタンス化する方法が分からない、という、xamarin-maciosと同様の制約である。
xamarin-maciosと同様、バインディングAPIはRegisterAttributeやオーバーライドによって識別されるものであり、RegisterAttributeはジェネリック型では使用できず、ジェネリックメソッドは定義できない…といった制限も、xamarin-maciosと概ね同様である。詳しくはXamarin.Androidのドキュメンテーションを参照されたい。
Future topics
デフォルトインターフェースメソッドとジェネリクス
今後C#に追加される予定の言語機能として、デフォルトインターフェースメソッドがある。インターフェースのメンバーを宣言して、そのデフォルト実装も追加できる、そして派生インターフェースでオーバーライドできる、というものだ。Javaのデフォルトインターフェースメソッドとほぼ同様の機能である。派生インターフェースでオーバーライドしたいという要件さえ無ければ、C#では拡張メソッドによって対処できていた問題だ。
Xamarin製品では、このC#のデフォルトインターフェースメソッドを活用できる場面がある:
- xamarin-maciosにおけるプロトコルのメンバー: Objective-CとSwiftのプロトコルには、実装が必須でないものがある。このようなメンバーについて、Xamarinのバインディングにおいて実装を強制することはできないので、これらはインターフェースメソッドとすることは出来ず、ExportAttributeなどを使用してサポートすることになる。これが空実装のデフォルトインターフェースメソッドが使えるようになれば、普通のインターフェースに転換できる。
- Xamarin.AndrodのJava APIにおけるデフォルトインターフェースメソッドのバインディング: Java8で追加されたデフォルトインターフェースメソッドが、Android APIでも使われるようになってきているが、現状これらはC#で対応する機能が存在しないためバインドされていない。デフォルトインターフェースメソッドがC#側でも実現すれば、これは単純にマッピングできるようになると期待できる。
デフォルトインターフェースメソッドはまだ仕様策定中の機能であり、さまざまな調整が必要となるはずである。特にECMA 335とは矛盾する仕様があるため、仕様の改定も必要になる。
ちなみに、プロトタイピングの過程でshared genericsに関連して、coreclrチームからmonoランタイムチームに提示された面白い話題があったので、余談として紹介しておきたい。
以下はデフォルトメソッド実装をもつインターフェースである。
interface IX<T> {
int Get();
void Set(int val);
int M()
{
int val = Get();
val++;
Set(val);
return val;
}
}
以下はこのインターフェースを複数のジェネリック型パラメータ(IList\<Z>とIEnumerable\<T>)で実装した構造体である。
struct P<Z> :
IX<IList<Z>>,IX<IEnumerable<Z>> {
int v;
public int Get()
{ return v; }
public void Set(int val)
{ v = val; }
int IX<IList<Z>>.M()
{ return ++v; }
}
そして以下のクラスに含まれるメソッドの中では、このデフォルトインターフェースメソッドを呼び出している。
class TestClass {
public static int Zap<T,U>(T, t)
where T:IX<U>
{
return t.M() + t.M();
}
}
さて、以下のメソッド呼び出しは、それぞれ何を返すだろうか?
Zap<P<object>,IEnumerable<object>> (new P<object> ()); // (1)
Zap<P<object>,IList<object>> (default(P<object> ())); // (2)
実のところ、クイズが目的ではない。それぞれの戻り値は2と3になるべきである。彼らの問題は、上記のZap
メソッドをILに変換しそれをgeneric sharingで変換したメソッド呼び出しの実体にある。
IL_0003: constrained. !!T
IL_0009: callvirt instance int32 class IX`1<!!U>::M()
デフォルトインターフェースメソッドの変換結果は、Pのデフォルトでないインスタンスについては、これをbox化したオブジェクトに対して呼び出すことになり、Pのデフォルト値については、box化せずに呼び出すことになるが、このgeneric sharingによるコードの呼び出し結果について、unboxする必要が「あるかもしれないし、ないかもしれない」という呼び出しを行うことはCLRの実装としてはできない。どうすべきか?
…実装上解決すべき問題は、このようなものがある。非常にややこしい例だ。
coreclrのプロトタイプ実装からは、他にもJITにウソ情報を流さないと正しく処理できない状況などもあることが見て取れる。これは多分最終的な実装でもこのままで問題ないのだろうが、実装を複雑にする要因になっていることはわかるだろう。
SwiftNetifier
詳しいことは全く報じられていないが、XamarinはBuild 2017でSwiftNetifierというプロジェクトの存在を明らかにしている。Swift APIをもとにxamarin-maciosのバインディングを生成するツールである。Swiftのジェネリクスはreified genericsであり、reified generics同士がうまく相互運用できるのかどうかは分からない。とはいえ、実行時にObjCRuntimeでインスタンス化できるのはNSObjectのみであろうから、現在と同様の制約はなおも存在するのではないか、というのが筆者の考察である。
Project Varhalla
Androidプラットフォームで実現するのはいつになるかわからないが、Javaの将来版ではジェネリクスがC#と同じreified genericsになることが予定されており、これを実現しようとしているのがProject Varhallaである。Varhallaに相当するものがAndroidにも登場するとしたら(そしてその時点でもAndroidがJavaに基づいているとしたら)、いずれはXamarin.Androidでもreified generics同士の相互運用を実現することが必要になってくるだろう。
その未来のほうがすっきりした相互運用を実現出来る可能性も、無いわけではないが、基本的には過去のライブラリの後方互換性を意識した製品をリリースすることも期待されているので、現時点でそこまで考えることは無いかもしれない。
Swiftにせよ、Varhallaにせよ、reified genericsの相互運用に固有の問題があるとしたら、今後明らかになっていくであろうトピックであり、ジェネリクスは古くて新しいトピックであり続けるのではないか、と暗雲を投げかけて(?)筆を置くこととしたい。
*1:本当は勉強会の予習資料として公開するつもりだったのだけど、実装の調査が不十分なままで出来ていなかった。
*2:https://en.wikibooks.org/wiki/C%2B%2B_Programming/RTTI
*3:reifyは辞書的には「具体化する」という意味らしいが、それが直感的かどうか筆者には判断しがたいので、以降も言及するときはreified genericsと書くことにする
*4:その前段階として、マネージドコードとしての型参照やアセンブリの解決をAppDomainなどをもとに解決するステップもあるのだけど、これは今回のトピックとほぼ関係ないので割愛する
*5:オブジェクトのインスタンス化ではなくジェネリック型のインスタンス化であることに注意
*6:「Profesional .NET 2.0 Generics」のセクション “Under the Hood” を参照