CXXIがクロスプラットフォームでC++/C# interopを実現する(予定)
monoチームからCXXIという新しいコンポーネントが登場しました。これはC++とC#のinteroperabilityを実現するフレームワークだそうです。
》 CXXI: Briding the C++ and C# worlds. http://tirania.org/blog/archive/2011/Dec-19.html
以前からcppinteropという名前で作っていたハカーたちがいて、7月のMonospace conferenceでも発表されていたのですが、それがブラッシュアップされて登場したのがコレです。
》 mono / cxxi https://github.com/mono/cxxi
以下、6割方↑記事の内容を引っ張ってきています。
CXXIの機能・特徴
CXXIが実際にサポートする機能は、ざっと次のようなものだそうです(便宜上C#と.NETはまとめてC#と書きます):
- C++のオブジェクトをC#から操作できる
- C#でC++のオブジェクトを生成できる
- C++クラスのメソッドをC#から呼び出す
- gccに-fkeep-inline-functionsなどを指定すればインラインメソッドも呼び出し可能
- C++クラスをC#で派生できる
- C++のメソッドをC#でオーバーライドできる
- C++のクラスやC++/C#の混合クラスを、ネイティブクラスであるかのようにC#またはC++に公開できる
通常のP/InvokeやMarshalByRefObjectよりは、C++を実用的に使えるものと言えるでしょう。他のC++ interopはどうなっていたんでしょうか?
- Microsoftの.NETの場合は(まあmono on Windowsにもありますが)COM interopがありました。これはC++のクラスをCOM化しないとできません。めんどくさいですね。(そもそもCOMはVBとC++のコード共有のために存在している側面もあるので、RCW/CCWの呼び出し規約が直感的でないのは仕方ないとも言えます。)
- C++の内容を呼び出すCのコードを作成することもできます。(.NETとは無関係に)Cヘッダを公開しているけど中身はC++で書かれているようなライブラリでは、この手法を採用しているものが多いと思います(具体的なコード例が先のリンク先にあります)。これは数が増えるとめんどくさいですし、メソッドをオーバーライドすることもできません(swigもそうですね)。
- Microsoftは、C++/CLIという別のアプローチを採用しました。これはmixed mode assemblyというネイティブとマネージドの混合アセンブリを生成するもので、生成されたコードには可搬性がありません。MicrosoftはWindowsを活かしWindowsと心中する企業なので、それで問題ないのですが、monoにはクロスプラットフォームのやり方が必要ですし相応しいでしょう。
CXXIランタイム
ではCXXIはどのように実装されているのかというと、バイナリレベルでC++のオブジェクトの内容を読み取って、それをもとにC#ベースのクラス構造やメソッド呼び出しを実現します。その具体的な仕組みを知るために、まずC++でコンパイルされたネイティブコードがどのようなバイナリ構造をもっているかを知る必要があるでしょう:
- オブジェクトレイアウト: CXXIではプラットフォーム依存のメモリ上のバイナリ構造をもとに、オブジェクト情報を保持しています。これはコンパイラやプラットフォームによってバラバラなので、それぞれに合わせて動作することになります。
- vtableレイアウト: これはクラス階層構造における仮想メソッドのオーバーライドを実現するために用いられるバイナリ構造です。(一般的に)vtableは関数ポインタを保持していて、メソッドがオーバーライドされたオブジェクトの場合は基底クラスと異なる関数を指し示すことになります。ちなみに非仮想メソッドはvtableには含まれず、Cの関数としてコンパイルされることになります。
- mangled name: C++コンパイラはCコンパイラであり、コンパイルされたオブジェクトコードには、ソースコード上のメソッド名にそれぞれの命名規則を適用して生成された名前をもつ関数が含まれています。この命名規則はコンパイラ依存で、逆に言えばそれに従ってさえいれば、バイナリレベルでのinteroperabilityが実現できるというわけです(名前さえ分かればP/Invokeと同じように名前から関数のアドレスを知ることが出来ます)。
これらがどのようにコード上で展開されるかは上記リンク先に例があります。
CXXIでは、引数やオブジェクト自体のメモリレイアウトを操作するために、プラットフォーム依存のオブジェクトレイアウトを知る必要があり、また非仮想メソッドを呼び出すためにmangled nameを把握しておく必要があります。
以上でも随所で説明しましたが、CXXIは、その仕組み上、プラットフォーム/アーキテクチャとコンパイラに依存します。
CXXIでは、対象ライブラリのC++ヘッダを入力として、CXXIランタイム(Mono.Cxxi.dll)を経由してC++オブジェクトを操作するC#クラスのソースを生成します。
CXXIランタイムの実際の動作は次のような感じになります:
- C#でクラスのインスタンスを生成すると、C++でインスタンスが生成されます。
- 生成されたC#コードのクラスは、C#で派生クラスを定義するのに利用できます。C++の仮想関数は仮想メソッドとなり、オーバーライドすればそれが呼び出せます。
- C++の多重継承に対応しています。具体的には、生成されたC#のコードには、多重継承したクラスに対応する型へのキャスト演算子オーバーロードが追加されるので、それらを利用します。
- オーバーライドされたメソッドでは、baseを呼び出して、C++コード上にある基底クラスの実装を呼び出すことができます。
- 多重継承したクラスにある仮想メソッドをオーバーライドすることができます。
- (自分で生成せずに)既存のC++インスタンスをラップするようなC#のオブジェクト(peer)を生成することもできます。
CXXIジェネレータの仕組み
次はCXXIでC++ヘッダからC#ソースを生成するジェネレータについて。CXXIでは、C++ヘッダからgcc-xml(gcc4の中間コードgimpleのXML表現にコンバートするツール)で生成したXMLを入力として、C#のソースを生成します。生成されるのはソースなので、これに手を加えて最終的なライブラリを生成できます。
ここでポイントとなるのは、生成されたC#コードには、具体的なメモリレイアウトに当てはめられたオブジェクトの情報が含まれているわけではない、つまりまだ環境/アーキテクチャ中立のコードになっている、ということです。具体的なメモリレイアウトの解決は、実行時に行われます。
(ジェネレータではgcc-xmlのみをサポートしていますが、生成されるC#ソースにはgcc依存の部分もMSVC依存の部分も存在せず、またC#ソースの生成に必要なのはヘッダファイルのみなので、gccで解析できるヘッダファイルでさえあれば、困ることは無いはずです。Windowsのヘッダファイルの多くは、cygwinにも含まれるgcc-mingwのw32apiパッケージにも含まれています。)
現状と今後
CXXIは大きな実用の可能性を見せるものですが、未完成の部分もいくつかあります。
- 現在のcxxiは、System.Reflection.Emitを利用してブリッジを動的に生成しています。これで動的にC++コンパイラのABIに合わせて対応できるようになっています。静的コンパイルは出来ていません(iPhoneやPS3環境はまだ)。
- 現在のcxxiはgcc / Itanium ABIにのみ対応しています。MSVC ABI対応のコードは存在しますが、未完成です。(わたしの理解が正しければ、armeabiにも対応していません。)
- 現在のcxxiでは、deleteできるオブジェクトは、C#から生成したオブジェクトのみです。そうでないオブジェクトは、自分でdeleteする必要があります。
- -
というわけで、CXXIのざっとした説明でした。わたしの理解では、cxxiが目指しているのはid:atsushieno:20110628:p1 で書いたBridJやJNAeratorのようなツールなんだろうと思っています。Javaの世界では、この種のC++バインディングツールが百花繚乱というか、それぞれ利点と欠点があって、それぞれが短いライフサイクルで生きているなあという印象がありましたが、C#のバインディングツールとして、CXXIもこれまでのツールとは一線を画した存在として、一石を投じることになりそうです。