COM 相互運用に対する .NET 型の適格性を確認する

.NET 型を COM に公開する

COM アプリケーションにアセンブリ内の型を公開する場合は、設計時に COM 相互運用の要件を検討する必要があります。 以下のガイドラインに従うと、マネージド型 (クラス、インターフェイス、構造体、列挙型) は COM の型とシームレスに統合します。

  • クラスは、インターフェイスを明示的に実装する必要があります。

    COM 相互運用は、クラスのすべてのメンバーとその基底クラスのメンバーを含むインターフェイスを自動的に生成するメカニズムを備えていますが、明示的なインターフェイスを提供する方がはるかによい方法です。 自動的に生成されるインターフェイスは、クラス インターフェイスと呼ばれます。 ガイドラインについては、「クラス インターフェイスの概要」を参照してください。

    インターフェイス定義言語 (IDL) またはそれと同等のものを使う代わりに、Visual Basic、C#、C++ を使ってコードにインターフェイス定義を組み込むことができます。 構文の詳細については、言語のドキュメントを参照してください。

  • マネージド型はパブリックにする必要があります。

    アセンブリ内のパブリック型のみが登録されて、タイプ ライブラリにエクスポートされます。 その結果、パブリック型のみが COM に表示されます。

    マネージド型は、COM に公開されない可能性がある他のマネージド コードに機能を公開します。 たとえば、パラメーター化されたコンストラクター、静的メソッド、および定数フィールドは、COM クライアントに公開されません。 さらに、ランタイムが、ある型に、またはある型からデータをマーシャリングすると、データがコピーまたは変換される可能性があります。

  • メソッド、プロパティ、フィールド、イベントは、パブリックである必要があります。

    また、パブリック型のメンバーを COM に表示させる場合は、メンバーもパブリックにする必要があります。 アセンブリ、パブリック型、またはパブリック型のパブリック メンバーの可視性は、ComVisibleAttribute を適用することにより制限できます。 既定では、すべてのパブリック型とメンバーが可視になります。

  • 型では、パラメーターなしのパブリック コンストラクターを COM からアクティブ化する必要があります。

    マネージド パブリック型のみが COM に表示されます。 ただし、パラメーターなしのパブリック コンストラクター (引数のないコンストラクター) がないと、COM クライアントでは型を作成できません。 その場合でも、型が他の方法でアクティブ化されると、COM クライアントは型を使うことができます。

  • 型を抽象にすることはできません。

    COM クライアントも .NET クライアントも、抽象型は作成できません。

COM にエクスポートされるとき、マネージド型の継承階層はフラット化されます。 マネージド環境とアンマネージド環境では、バージョン管理も異なります。 COM に公開された型は、他のマネージド型とバージョン管理特性が異なります。

.NET から COM 型を使用する

.NET から COM 型を使用する予定で Tlbimp.exe (タイプ ライブラリ インポーター) などのツールを使用しない場合は、次のガイドラインに従う必要があります。

  • インターフェイスに、ComImportAttribute が適用されている必要があります。
  • インターフェイスには、COM インターフェイスのインターフェイス ID で GuidAttribute が適用されている必要があります。
  • インターフェイスには、このインターフェイスの基底インターフェイスの種類 (IUnknownIDispatch、または IInspectable) を指定する InterfaceTypeAttributeが適用されている必要があります。
    • 既定のオプションでは、基本データ型の IDispatch を使用し、宣言されたメソッドをインターフェイスの想定される仮想関数テーブルに追加します。
    • 基本データ型の IInspectable の指定は、.NET Framework でのみサポートされています。

これらのガイドラインは、一般的なシナリオの最小要件を提供します。 さらに多くのカスタマイズ オプションが存在しており、「相互運用固有の属性の適用」で説明されています。

.NET で COM インターフェイスを定義する

.NET コードが、ComImportAttribute 属性を持つインターフェイスを介して COM オブジェクトのメソッドを呼び出そうとするときに、仮想関数テーブル (別名 vtable または vftable) を構築して、呼び出すネイティブ コードを決定するために、インターフェイスの .NET 定義を形成する必要があります。 このプロセスは複雑です。 次の例は、いくつかのシンプルなケースを示しています。

いくつかのメソッドを備えた COM インターフェイスについて考えてみましょう。

struct IComInterface : public IUnknown
{
    STDMETHOD(Method)() = 0;
    STDMETHOD(Method2)() = 0;
};

このインターフェイスについては、次の表に、その仮想関数テーブルのレイアウトについて説明します。

IComInterface 仮想関数テーブル スロット メソッド名
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2

各メソッドは、宣言された順序で仮想関数テーブルに追加されます。 特定の順序は C++ コンパイラによって定義されますが、オーバーロードのないシンプルなケースでは、宣言の順序によってテーブル内の順序が定義されます。

このインターフェイスに対応する .NET インターフェイスを次のように宣言します。

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid(/* The IID for IComInterface */)]
interface IComInterface
{
    void Method();
    void Method2();
}

InterfaceTypeAttribute は、基底インターフェイスを指定します。 いくつかのオプションが用意されています。

ComInterfaceType 基底インターフェイスの型 属性付きインターフェイスでのメンバーの動作
InterfaceIsIUnknown IUnknown 仮想関数テーブルには、最初に IUnknown のメンバーがあり、次に宣言順にこのインターフェイスのメンバーが含まれます。
InterfaceIsIDispatch IDispatch メンバーは仮想関数テーブルに追加されません。 これらは、IDispatch を介してのみアクセスできます。
InterfaceIsDual IDispatch 仮想関数テーブルには、最初に IDispatch のメンバーがあり、次に宣言順にこのインターフェイスのメンバーが含まれます。
InterfaceIsIInspectable IInspectable 仮想関数テーブルには、最初に IInspectable のメンバーがあり、次に宣言順にこのインターフェイスのメンバーが含まれます。 .NET Framework でのみサポートされます。

COM インターフェイスの継承と .NET

ComImportAttribute を使用する COM 相互運用システムはインターフェイスの継承とやりとりしないため、いくつかの軽減手順を実行しない限り、予期しない動作が発生することがあります。

System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute 属性を使用する COM ソース ジェネレーターは、インターフェイスの継承とやりとりするため、期待どおりに動作します。

C++ での COM インターフェイスの継承

C++ では、他の COM インターフェイスから派生する COM インターフェイスを開発者が次のように宣言できます。

struct IComInterface : public IUnknown
{
    STDMETHOD(Method)() = 0;
    STDMETHOD(Method2)() = 0;
};

struct IComInterface2 : public IComInterface
{
    STDMETHOD(Method3)() = 0;
};

この宣言スタイルは、既存のインターフェイスを変更せずに COM オブジェクトにメソッドを追加するメカニズムとして定期的に使用されます。これは破壊的変更になります。 この継承メカニズムにより、次の仮想関数テーブル レイアウトが作成されます。

IComInterface 仮想関数テーブル スロット メソッド名
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
IComInterface2 仮想関数テーブル スロット メソッド名
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

その結果、IComInterface2* から IComInterface で定義されたメソッドを簡単に呼び出します。 具体的には、基底インターフェイスでメソッドを呼び出しても、基底インターフェイスへのポインターを取得するための QueryInterface の呼び出しは必要ありません。 さらに、C++ では、IComInterface2* から IComInterface* への暗黙的な変換が可能です。これは適切に定義されており、QueryInterface を再び呼び出すことを回避できます。 その結果、C または C++ では、希望しない場合は基本データ型を取得するために QueryInterface を呼び出す必要がなく、これによりパフォーマンスが向上する可能性があります。

Note

WinRT インターフェイスは、この継承モデルに従いません。 これらは、.NET の [ComImport]ベースの COM 相互運用モデルと同じモデルに従って定義されています。

ComImportAttribute とのインターフェイス継承

.NET では、インターフェイスの継承のように見える C# コードは、実際にはインターフェイスの継承ではありません。 次のコードがあるとします。

interface I
{
    void Method1();
}
interface J : I
{
    void Method2();
}

このコードでは、"JI を実装しています" とは言っていません。このコードは実際には、"J を実装するすべての型は I も実装する必要があります" と述べています。この違いは、ComImportAttribute ベースの相互運用機能を非人間工学的なインターフェイス継承にする、基本的な設計上の決定につながります。 インターフェイスは常に単独で考慮されます。インターフェイスの基底インターフェイス リストは、特定の .NET インターフェイスの仮想関数テーブルを決定する計算には影響しません。

その結果、前の C++ COM インターフェイスの例と自然に同等なものは、異なる仮想関数テーブルのレイアウトになります。

C# コード:

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    void Method3();
}

仮想関数テーブルのレイアウト:

IComInterface 仮想関数テーブル スロット メソッド名
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
IComInterface2 仮想関数テーブル スロット メソッド名
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface2::Method3

これらの仮想関数テーブルは C++ の例とは異なるため、実行時に重大な問題が発生します。 ComImportAttribute を使用した .NET でのこれらのインターフェイスの正しい定義は次のとおりです。

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    new void Method();
    new void Method2();
    void Method3();
}

メタデータ レベルでは、IComInterface2IComInterface を実装しませんが、IComInterface2 の実装者が IComInterfaceも実装する必要があることを指定するだけです。 したがって、基底インターフェイス型の各メソッドは再宣言する必要があります。

GeneratedComInterfaceAttribute を使用したインターフェイスの継承 (.NET 8 以降)

GeneratedComInterfaceAttribute によってトリガーされる COM ソース ジェネレーターは、C# インターフェイス継承を COM インターフェイス継承として実装するため、仮想関数テーブルは想定どおりにレイアウトされます。 前の例を見ると、.NET で System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute を使用したこれらのインターフェイスの正しい定義は次のようになります。

[GeneratedComInterface]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[GeneratedComInterface]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    void Method3();
}

基底インターフェイスのメソッドは再宣言する必要はなく、また、再宣言するべきではありません。 次の表は、結果として得られる仮想関数テーブルについて説明しています。

IComInterface 仮想関数テーブル スロット メソッド名
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
IComInterface2 仮想関数テーブル スロット メソッド名
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

ご覧のように、これらのテーブルは C++ の例と一致するため、これらのインターフェイスは正しく機能します。

関連項目