C++とのインターフェイス
D は Cとのインターフェイス, は完璧ですが、C++ とのインターフェイスはかなり制限されています。 C++ とリンクするには3つの方法があります:
- C++側の、C向けインターフェイスを作る機能を用いる。 そして、D側からは Cとのインターフェイス でアクセス。
- C++側の、COMインターフェイスを作る機能を用いる。 そして、D側からは COMインターフェイス を通じてアクセス。
- 以下で述べるような、 制限付きでC++の関数およびクラスに直接アクセスするための機能を用いる。
基本的な考え方
C++ との 100% の互換性を達成するということは、 完全なC++コンパイラのフロントエンドをDに実装するというのと大差ありません。 過去の経験からすると、 その実装は最低でも10人年規模のプロジェクトになってしまい、 Dコンパイラを実装するのが事実上不可能になってしまいます。 C++ とのリンクを実現しようとしている他の言語も同じ問題を抱えていて、 いくつかの対案が考えられています:
- COM インターフェイスをサポートする (ただしWindows専用)
- C++ のコードを頑張って手作業でCでラップする
- SWIG のような、 C ラッパを生成する自動化ツールを使う
- C++のコードを全部他の言語に移植する
- あきらめる
D では現実的なアプローチとして、 全てではないにしろかなり多くの問題を解決できるような、 以下にあげるささやかな機能を提供しています:
- C++ と名前マングリング規則を合わせる
- C++ と関数呼び出し規約を合わせる
- C++ と、単一継承の場合に限り仮想関数テーブルの形式を合わせる
C++ のグローバル関数を D から呼ぶ
C++のソースファイルに書かれたC++の関数があったとします:
#include <iostream>
using namespace std;
int foo(int i, int j, int k) {
cout << "i = " << i << endl;
cout << "j = " << j << endl;
cout << "k = " << k << endl;
return 7;
}
これを呼び出したいDのコードでは、foo を C++ のリンケージと関数呼び出し規約に従うものとして宣言します:
extern (C++) int foo(int i, int j, int k);
こうすると、Dのコードから呼び出すことが可能になります:
extern (C++) int foo(int i, int j, int k);
void main() {
foo(1,2,3);
}
最初のファイルをC++コンパイラで、2つめをDコンパイラでコンパイルして、 両者をリンクして実行すると、 以下の結果が得られます:
i = 1
j = 2
k = 3
何が起きているのかをもっと具体的に説明しますと:
- Dコンパイラは、C++ の関数名がどのようにmangleされているのか、 またC++の関数の正しい呼び出し/復帰手続きについて、把握しています。
- C++ にはモジュールという概念がないため、 C++ リンケージを持つ関数はプログラム全体でグローバルなものとして扱われます。
- Dでは、__cdecl, __far, __stdcall, __declspec, などC++標準外の拡張呼び出し規約はサポートされません
- 型修飾子 volatile は D にはありません。
- Dの文字列は0終端になっていません。 詳しくは "データ型の互換性" をご覧下さい。 ただし、文字列リテラルは0終端になっています。
名前空間の中にある C++ の関数は、直接 D から呼び出すことができません。
D のグローバル関数を C++ から呼ぶ
D の関数を C++ からアクセス可能にするには、 C++ リンケージで宣言します:
import std.stdio;
extern (C++) int foo(int i, int j, int k) {
writefln("i = %s", i);
writefln("j = %s", j);
writefln("k = %s", k);
return 1;
}
extern (C++) void bar();
void main() {
bar();
}
C++ 側はこんな風になります:
int foo(int i, int j, int k);
void bar() {
foo(6, 7, 8);
}
コンパイル、リンクして実行すると以下の出力が得られます:
i = 6
j = 7
k = 8
クラス
D のクラスは常に Object から派生していて、 また、C++ のクラスのレイアウトと互換性がありません。 しかし、D の interface は、 C++ の単一継承のクラス階層と非常によく似ています。 そこで、extern (C++) 属性付きで宣言された D の interface は、 仮想関数テーブル (vtbl[]) の形式を 完全に C++ と合わせるようにしました。 通常の D の interface は、vtbl[] の先頭に D の RTTI 情報へのポインタが入るという少し違った形式となっています (C++ の場合、 先頭には一つめの仮想関数へのポインタが格納されます)。
C++ の仮想関数を D から呼ぶ
以下のようにクラスを定義しているC++のソースがあったとします:
#include <iostream>
using namespace std;
class D {
public:
virtual int bar(int i, int j, int k)
{
cout << "i = " << i << endl;
cout << "j = " << j << endl;
cout << "k = " << k << endl;
return 8;
}
};
D *getD() {
D *d = new D();
return d;
}
D のコードでこれを扱うには:
extern (C++) {
interface D {
int bar(int i, int j, int k);
}
D getD();
}
void main() {
D d = getD();
d.bar(9,10,11);
}
D の仮想関数をC++から呼ぶ
D のコード:
extern (C++) int callE(E);
extern (C++) interface E {
int bar(int i, int j, int k);
}
class F : E {
extern (C++) int bar(int i, int j, int k)
{
writefln("i = ", i);
writefln("j = ", j);
writefln("k = ", k);
return 8;
}
}
void main() {
F f = new F();
callE(f);
}
に C++ コードからアクセスするには:
class E {
public:
virtual int bar(int i, int j, int k);
};
int callE(E *e) {
return e->bar(11,12,13);
}
注:
- 非 virtual 関数と static メンバ関数にはアクセスできません。
- クラスのフィールドには、getter/setter を仮想関数で用意することでしかアクセスできません。
関数オーバーロード
C++ と D では関数オーバーロードの規則が異なっています。 D のソースコードでは、たとえ extern (C++) な関数であっても、 D のオーバーロード規則に従います。
メモリ割り当て
C++ では、 ::operator new() や ::operator delete() によって明示的にメモリを管理します。 一方 D ではガベージコレクタでメモリを割り当てるので、 明示的な delete は不要です。 D の new と delete は C++ の ::operator new や ::operator delete と互換性がありません。 C++ の ::operator new で割り当てたメモリを D の delete で解放しようとしたり、あるいはその逆をやろうとすると、 悲惨なことになってしまうでしょう。
mallocで確保されたバッファを要求するようなC++の関数と連携するために、 D では c.stdlib.malloc() や c.stdlib.free() の呼び出しによって 明示的にメモリ管理を行うこともできます。
Dのガベージコレクタで確保したメモリへのポインタを渡すには、 C++の関数がそのメモリを使い終わる前にガベージコレクタが領域を回収してしまう、 といった事故が起きないことを確かめなくてはなりません。 これは幾つかの方法で実現できます:
- std.c.stdlib.malloc() で確保した領域にデータをコピーし、 そちらを代わりに渡す。
- その領域へのポインタをスタック上(引数か、自動変数)に残しておく。 ガベージコレクタはスタック上のオブジェクトは生きていると判定します。
- Lその領域へのポインタを静的データ領域に残しておく。 ガベージコレクタは静的データ領域のオブジェクトは生きていると判定します。
- そのポインタを、std.gc.addRoot() か std.gc.addRange() によってガベージコレクタへ登録する。
オブジェクトがまだ使われていることを GC に知らせるには、 割り当てられたメモリ領域の内部へのポインタがあれば十分です。 領域の先頭へのポインタを保持しておく必要はありません。
Dによって作られた以外のスレッドのスタックや、 他のDLLに属するデータセグメントについては、 ガベージコレクタによって探索されません。
データ型の互換性
D の型 | C の型 |
---|---|
void | void |
byte | signed char |
ubyte | unsigned char |
char | char (char は D では unsigned) |
wchar | wchar_t (sizeof(wchar_t) が 2 のとき) |
dchar | wchar_t (sizeof(wchar_t) が 4 のとき) |
short | short |
ushort | unsigned short |
int | int |
uint | unsigned |
long | long long |
ulong | unsigned long long |
float | float |
double | double |
real | long double |
ifloat | なし |
idouble | なし |
ireal | なし |
cfloat | なし |
cdouble | なし |
creal | なし |
struct | struct |
union | union |
enum | enum |
class | なし |
type* | type * |
なし | type & |
type[dim] | type[dim] |
type[dim]* | type(*)[dim] |
type[] | なし |
type[type] | なし |
type function(parameters) | type(*)(parameters) |
type delegate(parameters) | なし |
これらの対応関係は、ほとんどの 32 bit C++ コンパイラで成立します。 ただし、 C++標準ではそれぞれの型のサイズを明確に定めているわけではないので、注意が必要です。
構造体と共用体
Dの構造体と共用体は、Cのそれとほぼ同じです。
Cのコードでは、実装特有の#pragmaやコンパイラのコマンドスイッチによって、 構造体の整列を制御します。これに対応するものとして、D には 明示的なアラインメント属性が用意されています。 C側でのアラインメントを調べて、 D側の構造体宣言に明示的にその値を設定して下さい。
Dはビットフィールドをサポートしません。必要ならば、 シフトとビットマスク演算によってエミュレートできます。 htod 使うと、 ビットフィールドはシフトとマスクを使ったインライン関数に変換されます。
オブジェクトの生成と破棄
メモリ割り当てと解放と同じく、 D のコードで構築されたオブジェクトは、D のコードで破棄される必要があります。 同様に、 C++ で構築されたオブジェクトは C++ で破棄する必要があります。
特殊なメンバ関数
実行時型識別 (RTTI)
D の実行時型識別は、 C++とは完全に違う方式で実現されています。 この2つには互換性がありません。
C++ クラスオブジェクトの値渡し
D から POD (Plain Old Data) な C++ の構造体にアクセスすることは可能で、 また、参照経由で C++ のクラスの仮想関数にアクセスすることも可能です。 しかし、C++ のクラスに値としてアクセスすることはできません。
C++ テンプレート
C++ のテンプレートと D のテンプレートの共通点はほとんどありません。 また、C++ のテンプレートを Dとのリンク互換性がある形で表現するうまい方法も見あたりません。
このため、C++ の STL や Boost が D から直接アクセスできるようになることは将来的にもおそらくないでしょう。
例外処理
D と C++ の例外処理は完全に異なる物になっています。 このため、D と C++ の境界を越えるように例外を投げるコードは、 正しく動作しないと思われます。
将来の方向性
次期C++標準規格 C++0x がこの機能に影響するかどうかは、 さだかではありません。
将来的には、C++ ABI のより多くの側面が Dから直接アクセス可能になる予定です。