D言語インタフェースとCOMインタフェースの関係を深く分析する

5702 ワード

先日、dxpcomプロジェクトで出会ったxpcomインタフェースの互換性の問題を解決するために、DMDコンパイラのソースコードを見て、Dのインタフェースについていくつか理解して、今まとめて、忘れています.
Dには,C++のように抽象クラスを用いずにインタフェースを識別するためのキーワードinterfaceがある.
interface ITest
{
int test();
}
class ITest
{
int test()=0;
}

DのインタフェースはC++のインタフェースとは異なり,Dのインタフェースは依然としてClassInfoを含み,虚表の0項に格納されている.
DMDのソースコードから分かるように,D中のクラス,インタフェースはすべてダミーテーブルの0項にClassInfoポインタが保存されている.
このように、DのインタフェースがC++インタフェースと互換性がないと、DはWindowsのCOMオブジェクトを呼び出すことができず、少なくとも「優雅」な呼び出しはできない(structを使用してバイナリ互換性を代替することもできる).
この問題を解決するために、DMDはC++と互換性のあるCOMインタフェースを示す必要がある.すなわち、ダミーテーブルが「きれい」なインタフェースが必要である.また、COMインタフェースから継承されたインタフェースは依然としてCOMインタフェースであり、COMモデルの実装では「IUnknown」ルートインタフェースが適切に定義されている(COMシステムのすべてのインタフェースはIUnknownを継承している).
したがって、DMDは、単純な実装の原則から、1つのインタフェースがDインタフェースなのかCOMインタフェースなのかを区別し、インタフェースがInterfaceキーワードで宣言されているにもかかわらず、このインタフェースがIUnknownと呼ばれているかどうかを判断することが重要である.さらに興味深いことに、DMDは、インタフェースの名前が「IUnknown」であるかどうかを判断するだけで、インタフェースの方法がどのように定義されているかにかかわらない.
上記の内容は、Windows COMプログラミングを行う場合、ほとんど気づかれません.WindowsのすべてのインタフェースはIUnknownから継承されているので、正常に使用すればよいからです.
一方,Mozilla xpcomプログラミングを行う場合,xpcomのルートインタフェースはISupportsと呼ばれ,DMDはC++互換性のあるCOMインタフェースとしてコンパイルする必要があるとは考えず,虚表の0項目を保持し,結果として利用者に虚表ポインタがずれた印象を与える.
Dに基づくこのCOMインタフェースを識別する方式は、dxpcomプロジェクトにおいてqieziが別名を用いて変換し、dxpcomプロジェクトにおけるすべてのインタフェース名を優雅に統一するとともに、DMDに正しいCOMインタフェースを生成させることができる.
extern(Windows)
interface IUnknown {
  static const char[] IID_STR = NS_ISUPPORTS_IID_STR;
  static const nsIID IID = NS_ISUPPORTS_IID;

  /* void QueryInterface (in nsIIDRef uuid, [iid_is (uuid), retval] out nsQIResult result); */
  nsresult QueryInterface(nsIID * uuid, void * *result);

  /* [noscript, notxpcom] nsrefcnt AddRef (); */
  nsrefcnt AddRef();

  /* [noscript, notxpcom] nsrefcnt Release (); */
  nsrefcnt Release();

}

alias IUnknown nsISupports;

この現象はまた,Dにおける別名(alias)が記号の処理において1つの記号の置換にすぎず,C/C++における#defineの役割と同様であることを良く説明している.
次の2つのコードは本稿の内容をよく解釈することができます(qieziの提供に感謝します)
コード1は、インタフェースIinterfaceが依然として標準Dインタフェースであるため、ダミーテーブルの0項目はClassInfoポインタでは表示できない呼び出しであり、実行結果ではダミーテーブルとして表現されてオフセットしているため、実行期間によって断言できない.
extern(Windows):   
int test1(IInterface p)   
{   
    return 1;   
}   
  
int test2(IInterface p)   
{   
    return 2;   
}   
  
int test3(IInterface p)   
{   
    return 3;   
}   
  
struct InterfaceVtbl   
{   
extern(Windows):   
    int function(IInterface) test1;   
    int function(IInterface) test2;   
    int function(IInterface) test3;   
}   
  
struct Interface   
{   
    InterfaceVtbl* vtbl;   
  
    InterfaceVtbl vtbl_;   
  
    static Interface opCall()   
    {   
        Interface res;   
        res.vtbl_.test1 = &test1;   
        res.vtbl_.test2 = &test2;   
        res.vtbl_.test3 = &test3;   
        res.vtbl = &res.vtbl_;   
        return res;   
    }   
}   
  
interface IInterface   
{   
    int test1();   
    int test2();   
    int test3();   
}   
  
extern (D):   
  
void main()   
{   
    Interface i = Interface();   
    assert(i.vtbl.test1(cast(IInterface)&i) == 1);   
    assert(i.vtbl.test2(cast(IInterface)&i) == 2);   
    assert(i.vtbl.test3(cast(IInterface)&i) == 3);   
  
    IInterface ii = cast(IInterface)&i;   
    assert(ii.test1() == 1);   
    assert(ii.test2() == 2);   
    assert(ii.test3() == 3);   
}

コード2は、コード1の構造と完全に一致しているが、実行時に断言する検査を通過することができる.唯一の違いはIinterfaceの名前がIUnknownに変わっただけ!!
extern(Windows):   
int test1(IUnknown p)   
{   
    return 1;   
}   
  
int test2(IUnknown p)   
{   
    return 2;   
}   
  
int test3(IUnknown p)   
{   
    return 3;   
}   
  
struct InterfaceVtbl   
{   
extern(Windows):   
    int function(IUnknown) test1;   
    int function(IUnknown) test2;   
    int function(IUnknown) test3;   
}   
  
struct Interface   
{   
    InterfaceVtbl* vtbl;   
  
    InterfaceVtbl vtbl_;   
  
    static Interface opCall()   
    {   
        Interface res;   
        res.vtbl_.test1 = &test1;   
        res.vtbl_.test2 = &test2;   
        res.vtbl_.test3 = &test3;   
        res.vtbl = &res.vtbl_;   
        return res;   
    }   
}   
  
interface IUnknown   
{   
    int test1();   
    int test2();   
    int test3();   
}   
  
extern (D):   
  
void main()   
{   
    Interface i = Interface();   
    assert(i.vtbl.test1(cast(IUnknown)&i) == 1);   
    assert(i.vtbl.test2(cast(IUnknown)&i) == 2);   
    assert(i.vtbl.test3(cast(IUnknown)&i) == 3);   
  
    IUnknown ii = cast(IUnknown)&i;   
    assert(ii.test1() == 1);   
    assert(ii.test2() == 2);   
    assert(ii.test3() == 3);   
}

なお,extern(D),extern(Windows),extern(Pascal)などの特徴は,インタフェースのタイプに関係なく関数の呼び出し規則を記述するために用いられるだけである.
一言:Dの中のクラスと標準DインタフェースはすべてClassInfoが虚表の0項の上で、COMインタフェースの虚表はきれいです;一方、1つのインタフェースをCOMインタフェースとして宣言する方法は、このインタフェースをIUnknownと命名するか、IUnknownから継承するかである.