チュートリアル: ComWrappers API を使用する

このチュートリアルでは、ComWrappers 型を適切にサブクラス化して、最適化された、AOT 対応の COM 相互運用ソリューションを提供する方法について説明します。 このチュートリアルを始める前に、COM、そのアーキテクチャ、既存の COM 相互運用ソリューションについてよく理解しておく必要があります。

このチュートリアルでは、次のインターフェイス定義を実装します。 これらのインターフェイスとその実装を見ていきます。

  • COM/.NET の境界を越えた型のマーシャリングとマーシャリング解除。
  • .NET でネイティブ COM オブジェクトを使用するための 2 つの異なる方法。
  • .NET 5 以降でカスタム COM 相互運用を有効にするために推奨されるパターン。

このチュートリアルで使用されるすべてのソース コードは、dotnet/samples リポジトリにあります。

Note

.NET 8 SDK 以降のバージョンでは、ComWrappers API 実装を自動的に生成するソース ジェネレーターが提供されています。 詳細については、「ComWrappers のソース生成」を参照してください。

C# での定義

interface IDemoGetType
{
    string? GetString();
}

interface IDemoStoreType
{
    void StoreString(int len, string? str);
}

Win32 C++ での定義

MIDL_INTERFACE("92BAA992-DB5A-4ADD-977B-B22838EE91FD")
IDemoGetType : public IUnknown
{
    HRESULT STDMETHODCALLTYPE GetString(_Outptr_ wchar_t** str) = 0;
};

MIDL_INTERFACE("30619FEA-E995-41EA-8C8B-9A610D32ADCB")
IDemoStoreType : public IUnknown
{
    HRESULT STDMETHODCALLTYPE StoreString(int len, _In_z_ const wchar_t* str) = 0;
};

ComWrappers の設計の概要

ComWrappers API は、.NET 5 以降のランタイムで COM 相互運用を実現するために最小限必要な相互作用を提供するように設計されています。 これは、組み込みの COM 相互運用システムには存在する細かな処理の多くは存在せず、基本的な構成要素から構築する必要があることを意味します。 この API の主な役割は次の 2 つです。

  • 効率的なオブジェクトの識別 (たとえば、IUnknown* インスタンスとマネージド オブジェクトの間のマッピング)。
  • ガベージ コレクター (GC) の相互作用。

このような効率性は、ComWrappers API を使用してラッパーの作成と取得を行う必要があるようにすることで実現されます。

ComWrappers API の役割はごくわずかであるため、相互運用の処理のほとんどをコンシューマーで行う必要があるのは理にかなっています。確かにそのとおりです。 ただし、追加作業の大部分は機械的なものであり、ソース生成ソリューションによって実行できます。 たとえば、C#/WinRT ツール チェーンは、ComWrappers を基にして構築された、WinRT 相互運用のサポートを提供するためのソース生成ソリューションです。

ComWrappers サブクラスを実装する

ComWrappers サブクラスを提供するということは、マネージド オブジェクトを COM に射影し、COM オブジェクトを .NET に射影するためのラッパーを作成して記録するために十分な情報を、.NET ランタイムに提供することを意味します。 サブクラスの概要を調べる前に、いくつかの用語を定義しておく必要があります。

マネージド オブジェクト ラッパー – マネージド .NET オブジェクトには、.NET 以外の環境から使用できるようにするためのラッパーが必要です。 これらのラッパーは、従来から COM 呼び出し可能ラッパー (CCW) と呼ばれています。

ネイティブ オブジェクト ラッパー – .NET 以外の言語で実装された COM オブジェクトには、.NET から使用できるようにするためのラッパーが必要です。 これらのラッパーは、従来からランタイム呼び出し可能ラッパー (RCW) と呼ばれています。

ステップ 1 – 実装するメソッドを定義し、その目的を理解する

ComWrappers 型を拡張するには、次の 3 つのメソッドを実装する必要があります。 これらの各メソッドは、ラッパーの型の作成または削除にユーザーが関与することを表します。 ComputeVtables() メソッドと CreateObject() メソッドでは、それぞれ、マネージド オブジェクト ラッパーとネイティブ オブジェクト ラッパーを作成します。 ReleaseObjects() メソッドは、提供されたラッパーのコレクションを基になるネイティブ オブジェクトから "解放" するよう要求するために、ランタイムによって使用されます。 ほとんどの場合、ReleaseObjects() メソッドの本体では、単に NotImplementedException をスローします。これは、それが呼び出されるのは、参照トラッカー フレームワークが含まれる高度なシナリオにおいてだけであるためです。

// See referenced sample for implementation.
class DemoComWrappers : ComWrappers
{
    protected override unsafe ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags, out int count) =>
        throw new NotImplementedException();

    protected override object? CreateObject(IntPtr externalComObject, CreateObjectFlags flags) =>
        throw new NotImplementedException();

    protected override void ReleaseObjects(IEnumerable objects) =>
        throw new NotImplementedException();
}

ComputeVtables() メソッドを実装するには、サポートするマネージド型を決定します。 このチュートリアルでは、以前に定義した 2 つのインターフェイス (IDemoGetTypeIDemoStoreType) と、2 つのインターフェイスを実装するマネージド型 (DemoImpl) をサポートします。

class DemoImpl : IDemoGetType, IDemoStoreType
{
    string? _string;
    public string? GetString() => _string;
    public void StoreString(int _, string? str) => _string = str;
}

CreateObject() メソッドについては、サポートする対象を決定する必要もあります。 ただし、この例では、COM クラスではなく、関心のある COM インターフェイスのみがわかっています。 COM 側から使用されているインターフェイスは、.NET 側から射影しているインターフェイス (つまり、IDemoGetTypeIDemoStoreType) と同じです。

このチュートリアルでは、ReleaseObjects() は実装しません。

ステップ 2 – ComputeVtables() を実装する

まず、マネージド オブジェクト ラッパーから始めます。これらのラッパーは比較的簡単です。 インターフェイスを COM 環境に射影するため、インターフェイスごとに仮想メソッド テーブル (vtable) を作成します。 このチュートリアルでは、vtable を一連のポインターとして定義します。各ポインターは、インターフェイスでの関数の実装を表します。ここでは順序が "非常に" 重要です。 COM では、すべてのインターフェイスは IUnknown から継承されます。 IUnknown 型では、QueryInterface()AddRef()Release() という 3 つのメソッドが、この順序で定義されています。 IUnknown のメソッドの後に、特定のインターフェイスのメソッドがあります。 たとえば、IDemoGetTypeIDemoStoreType について考えます。 概念的には、型の vtable は次のようになります。

IDemoGetType    | IDemoStoreType
==================================
QueryInterface  | QueryInterface
AddRef          | AddRef
Release         | Release
GetString       | StoreString

DemoImpl を見ると、GetString()StoreString() の実装は既にありますが、IUnknown の関数についてはどうでしょうか。 IUnknown インスタンスを実装する方法についてはこのチュートリアルでは扱いませんが、ComWrappers において手動で行うことができます。 ただし、このチュートリアルでは、その部分の処理はランタイムに任せます。 IUnknown の実装は、ComWrappers.GetIUnknownImpl() メソッドを使用して取得できます。

すべてのメソッドが実装されたように見えますが、残念ながら、COM vtable で使用できるのは IUnknown の関数だけです。 COM はランタイムの外部にあるため、DemoImpl の実装へのネイティブ関数ポインターを作成する必要があります。 これは、C# の関数ポインターと UnmanagedCallersOnlyAttribute を使用して行うことができます。 COM 関数のシグネチャをまねて static 関数を作成することにより、vtable に挿入する関数を作成できます。 次に示すのは、IDemoGetType.GetString() の COM シグネチャの例です。COM ABI では最初の引数がインスタンス自体であることを思い出してください。

[UnmanagedCallersOnly]
public static int GetString(IntPtr _this, IntPtr* str);

IDemoGetType.GetString() のラッパーの実装は、マーシャリング ロジックと、ラップ対象のマネージド オブジェクトへのディスパッチで構成されている必要があります。 ディスパッチのすべての状態は、提供される _this 引数内に含まれます。 _this 引数は、実際には ComInterfaceDispatch* 型になります。 この型は、1 つのフィールド Vtable を持つ下位レベルの構造を表します。これについては後で説明します。 この型とそのレイアウトの詳細は、ランタイムの実装の詳細であり、依存しないようにする必要があります。 ComInterfaceDispatch* インスタンスからマネージド インスタンスを取得するには、次のコードを使用します。

IDemoGetType inst = ComInterfaceDispatch.GetInstance<IDemoGetType>((ComInterfaceDispatch*)_this);

これで、vtable に挿入できる C# メソッドが完成したので、vtable を構築できます。 RuntimeHelpers.AllocateTypeAssociatedMemory()なアセンブリで動作するようにメモリを割り当てるため、RuntimeHelpers.AllocateTypeAssociatedMemory() を使用することにご注意ください。

GetIUnknownImpl(
    out IntPtr fpQueryInterface,
    out IntPtr fpAddRef,
    out IntPtr fpRelease);

// Local variables with increment act as a guard against incorrect construction of
// the native vtable. It also enables a quick validation of final size.
int tableCount = 4;
int idx = 0;
var vtable = (IntPtr*)RuntimeHelpers.AllocateTypeAssociatedMemory(
    typeof(DemoComWrappers),
    IntPtr.Size * tableCount);
vtable[idx++] = fpQueryInterface;
vtable[idx++] = fpAddRef;
vtable[idx++] = fpRelease;
vtable[idx++] = (IntPtr)(delegate* unmanaged<IntPtr, IntPtr*, int>)&ABI.IDemoGetTypeManagedWrapper.GetString;
Debug.Assert(tableCount == idx);
s_IDemoGetTypeVTable = (IntPtr)vtable;

vtable の割り当ては、ComputeVtables() の実装の最初の部分です。 サポートする予定の型の包括的な COM 定義も作成する必要があります。DemoImpl およびそのうちのどのパーツを COM から使用できるようにする必要があるかを考えてください。 作成された vtable を使用して、COM でのマネージド オブジェクトの完全なビューを表す一連の ComInterfaceEntry インスタンスを作成できます。

s_DemoImplDefinitionLen = 2;
int idx = 0;
var entries = (ComInterfaceEntry*)RuntimeHelpers.AllocateTypeAssociatedMemory(
    typeof(DemoComWrappers),
    sizeof(ComInterfaceEntry) * s_DemoImplDefinitionLen);
entries[idx].IID = IDemoGetType.IID_IDemoGetType;
entries[idx++].Vtable = s_IDemoGetTypeVTable;
entries[idx].IID = IDemoStoreType.IID_IDemoStoreType;
entries[idx++].Vtable = s_IDemoStoreVTable;
Debug.Assert(s_DemoImplDefinitionLen == idx);
s_DemoImplDefinition = entries;

マネージド オブジェクト ラッパーの vtable とエントリの割り当ては、型のすべてのインスタンスについてデータを使用できるので、前もって行うことができ、行う必要があります。 ここでの作業は、static コンストラクターまたはモジュール イニシャライザーで実行できますが、ComputeVtables() メソッドが可能な限りシンプルで高速になるように、前もって行っておく必要があります。

protected override unsafe ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags,
out int count)
{
    if (obj is DemoImpl)
    {
        count = s_DemoImplDefinitionLen;
        return s_DemoImplDefinition;
    }

    // Unknown type
    count = 0;
    return null;
}

ComputeVtables() メソッドを実装すると、ComWrappers サブクラスで DemoImpl のインスタンス用のマネージド オブジェクト ラッパーを生成できるようになります。 GetOrCreateComInterfaceForObject() の呼び出しから返されるマネージド オブジェクト ラッパーは IUnknown* 型であることにご注意ください。 ラッパーに渡されるネイティブ API で別のインターフェイスが必要な場合は、そのインターフェイス用の Marshal.QueryInterface() を実行する必要があります。

var cw = new DemoComWrappers();
var demo = new DemoImpl();
IntPtr ccw = cw.GetOrCreateComInterfaceForObject(demo, CreateComInterfaceFlags.None);

ステップ 3 – CreateObject() を実装する

ネイティブ オブジェクト ラッパーの構築は、マネージオド ブジェクト ラッパーの構築より実装オプションが多く、はるかに複雑です。 最初に検討する問題は、ComWrappers サブクラスによる COM 型のサポートをどの程度許すかということです。 すべての COM 型をサポートすることもできますが、大量のコードを記述するか、Reflection.Emit を巧妙に使用する必要があります。 このチュートリアルでは、IDemoGetTypeIDemoStoreType の両方を実装する COM インスタンスのみをサポートします。 有限のセットがあることがわかっており、提供される COM インスタンスでは両方のインターフェイスが実装されている必要があると制限してあるので、静的に定義された単一のラッパーを用意できます。ただし、COM では動的なケースが非常に一般的であり、両方のオプションについて詳しく説明します。

静的なネイティブ オブジェクト ラッパー

まず、静的な実装を見てみましょう。 静的なネイティブ オブジェクト ラッパーの場合、.NET インターフェイスを実装するマネージド型の定義が含まれ、マネージド型での呼び出しを COM インスタンスに転送できます。 静的ラッパーの大まかな概要は次のとおりです。

// See referenced sample for implementation.
class DemoNativeStaticWrapper
    : IDemoGetType
    , IDemoStoreType
{
    public string? GetString() =>
        throw new NotImplementedException();

    public void StoreString(int len, string? str) =>
        throw new NotImplementedException();
}

このクラスのインスタンスを構築してラッパーとして提供するには、いくつかのポリシーを定義する必要があります。 この型がラッパーとして使用される場合、両方のインターフェイスを実装しているため、基になる COM インスタンスでも両方のインターフェイスが実装されている必要があると思われます。 このポリシーを採用すると、COM インスタンスで Marshal.QueryInterface() を呼び出すことによって、これを確認する必要があります。

int hr = Marshal.QueryInterface(ptr, ref IDemoGetType.IID_IDemoGetType, out IntPtr IDemoGetTypeInst);
if (hr != 0)
{
    return null;
}

hr = Marshal.QueryInterface(ptr, ref IDemoStoreType.IID_IDemoStoreType, out IntPtr IDemoStoreTypeInst);
if (hr != 0)
{
    Marshal.Release(IDemoGetTypeInst);
    return null;
}

return new DemoNativeStaticWrapper()
{
    IDemoGetTypeInst = IDemoGetTypeInst,
    IDemoStoreTypeInst = IDemoStoreTypeInst
};

動的なネイティブ オブジェクト ラッパー

動的ラッパーの場合、型は静的ではなく実行時に照会する手段があるため、いっそう柔軟です。 このサポートを提供するために、IDynamicInterfaceCastable を利用します。詳しくは、こちらをご覧ください。 DemoNativeDynamicWrapper にはこのインターフェイスだけが実装されていることをご確認ください。 そのインターフェイスによって提供される機能により、実行時にサポートされている型を特定することができます。 このチュートリアルのソースでは、作成時に静的なチェックを実行しますが、これは単にコード共有用です。DemoNativeDynamicWrapper.IsInterfaceImplemented() の呼び出しが行されるまでチェックを延期できるためです。

// See referenced sample for implementation.
internal class DemoNativeDynamicWrapper
    : IDynamicInterfaceCastable
{
    public RuntimeTypeHandle GetInterfaceImplementation(RuntimeTypeHandle interfaceType) =>
        throw new NotImplementedException();

    public bool IsInterfaceImplemented(RuntimeTypeHandle interfaceType, bool throwIfNotImplemented) =>
        throw new NotImplementedException();
}

DemoNativeDynamicWrapper によって動的にサポートされるインターフェイスの 1 つを見てみましょう。 次のコードでは、"既定のインターフェイス メソッド" の機能を使用して、IDemoStoreType の実装が提供されます。

[DynamicInterfaceCastableImplementation]
unsafe interface IDemoStoreTypeNativeWrapper : IDemoStoreType
{
    public static void StoreString(IntPtr inst, int len, string? str);

    void IDemoStoreType.StoreString(int len, string? str)
    {
        var inst = ((DemoNativeDynamicWrapper)this).IDemoStoreTypeInst;
        StoreString(inst, len, str);
    }
}

この例では、注意すべき重要な点が 2 つあります。

  1. DynamicInterfaceCastableImplementationAttribute 属性。 この属性は、IDynamicInterfaceCastable メソッドから返されるすべての型で必要です。 IL のトリミングが容易になるというもう 1 つの利点があり、AOT のシナリオの信頼性が高くなることを意味します。
  2. DemoNativeDynamicWrapper へのキャスト。 これは、IDynamicInterfaceCastable の動的な性質の一部です。 IDynamicInterfaceCastable.GetInterfaceImplementation() から返される型は、IDynamicInterfaceCastable を実装する型を "カバー" するために使用されます。 ここでの gist は this ポインターです。DemoNativeDynamicWrapper から IDemoStoreTypeNativeWrapper へのケースを許可しているため、これは偽装ではありません。

COM インスタンスへの呼び出しを転送する

使用されているネイティブ オブジェクト ラッパーに関係なく、COM インスタンスで関数を呼び出すことができる必要があります。 IDemoStoreTypeNativeWrapper.StoreString() の実装は、unmanaged C# 関数ポインターを使用する例として使用できます。

public static void StoreString(IntPtr inst, int len, string? str)
{
    IntPtr strLocal = Marshal.StringToCoTaskMemUni(str);
    int hr = ((delegate* unmanaged<IntPtr, int, IntPtr, int>)(*(*(void***)inst + 3 /* IDemoStoreType.StoreString slot */)))(inst, len, strLocal);
    if (hr != 0)
    {
        Marshal.FreeCoTaskMem(strLocal);
        Marshal.ThrowExceptionForHR(hr);
    }
}

vtable の実装にアクセスするための COM インスタンスの逆参照を調べてみましょう。 COM ABI では、オブジェクトの最初のポインターはその型の vtable へのポインターであり、そこから目的のスロットにアクセスできると定義されています。 COM オブジェクトのアドレスが 0x10000 であるとします。 最初のポインター サイズの値は、vtable のアドレスである必要があります (この例では 0x20000)。 vtable に移動したら、4 番目のスロット (0 から始まるインデックス指定でインデックス 3) を探して、StoreString() の実装にアクセスします。

COM instance
0x10000  0x20000

VTable for IDemoStoreType
0x20000  <Address of QueryInterface>
0x20008  <Address of AddRef>
0x20010  <Address of Release>
0x20018  <Address of StoreString>

関数ポインターが得られたら、オブジェクトのインスタンスを 1 番目のパラメーターとして渡すことにより、そのオブジェクトでそのメンバー関数にディスパッチできます。 このパターンは、マネージド オブジェクト ラッパーの実装の関数定義に基づく見慣れたものであるはずです。

CreateObject() メソッドが実装されたら、ComWrappers サブクラスで IDemoGetTypeIDemoStoreType の両方を実装する COM インスタンス用のネイティブ オブジェクト ラッパーを生成できます。

IntPtr iunk = ...; // Get a COM instance from native code.
object rcw = cw.GetOrCreateObjectForComInstance(iunk, CreateObjectFlags.UniqueInstance);

ステップ 4 – ネイティブ オブジェクト ラッパーの有効期間の詳細を処理する

ComputeVtables()CreateObject() の実装でラッパーの有効期間の詳細について少し説明しましたが、考慮事項はそれ以外にもあります。 これは短い手順かもしれませんが、ComWrappers の設計の複雑さが大幅に増す可能性もあります。

AddRef() メソッドと Release() メソッドの呼び出しによって制御されるマネージド オブジェクト ラッパーとは異なり、GC によるネイティブ オブジェクト ラッパーの有効期間の処理は非確定的です。 ここでの問題は、COM インスタンスを表す IntPtrRelease() はネイティブ オブジェクト ラッパーによっていつ呼び出されるのか、ということです。 2 つの一般的な方法があります。

  1. ネイティブ オブジェクト ラッパーのファイナライザーで、責任を持って COM インスタンスの Release() メソッドを呼び出します。 このメソッドを呼び出しても安全なのは、このときだけです。 この時点では、.NET ランタイムにおいてネイティブ オブジェクト ラッパーへの他の参照がないことは、GC によって正しく決定されています。 COM アパートメントを適切にサポートしている場合は、これは複雑になることがあります。詳細については、「その他の注意点」セクションを参照してください。

  2. ネイティブ オブジェクト ラッパーで IDisposable を実装し、Dispose()Release() を呼び出します。

注意

IDisposable のパターンは、CreateObject() の呼び出しの間に CreateObjectFlags.UniqueInstance フラグが渡される場合にのみ、サポートする必要があります。 この要件に従っていない場合、破棄されたネイティブ オブジェクト ラッパーが破棄された後で再利用される可能性があります。

ComWrappers サブクラスの使用

これで、テストできる ComWrappers サブクラスが作成されました。 IDemoGetTypeIDemoStoreType が実装されている COM インスタンスを返すネイティブ ライブラリの作成を避けるには、マネージド オブジェクト ラッパーを使用し、それを COM インスタンスとして扱います。いずれにしても、それに COM を渡すためには、これが可能である必要があります。

最初に、マネージド オブジェクト ラッパーを作成しましょう。 DemoImpl のインスタンスを作成し、その現在の文字列の状態を表示します。

var demo = new DemoImpl();

string? value = demo.GetString();
Console.WriteLine($"Initial string: {value ?? "<null>"}");

これで、DemoComWrappers のインスタンスと、COM 環境に渡すことができるマネージド オブジェクト ラッパーを作成できます。

var cw = new DemoComWrappers();

IntPtr ccw = cw.GetOrCreateComInterfaceForObject(demo, CreateComInterfaceFlags.None);

マネージド オブジェクト ラッパーを COM 環境に渡す代わりに、この COM インスタンスを受け取ったものとして、代わりにネイティブ オブジェクト ラッパーを作成します。

var rcw = cw.GetOrCreateObjectForComInstance(ccw, CreateObjectFlags.UniqueInstance);

ネイティブ オブジェクト ラッパーを使用すると、それを目的のインターフェイスの 1 つにキャストし、通常のマネージド オブジェクトとして使用できます。 DemoImpl のインスタンスを調べて、マネージド インスタンスをラップするマネージド オブジェクト ラッパーをラップしているネイティブ オブジェクト ラッパーに対する操作の影響を確認できます。

var getter = (IDemoGetType)rcw;
var store = (IDemoStoreType)rcw;

string msg = "hello world!";
store.StoreString(msg.Length, msg);
Console.WriteLine($"Setting string through wrapper: {msg}");

value = demo.GetString();
Console.WriteLine($"Get string through managed object: {value}");

msg = msg.ToUpper();
demo.StoreString(msg.Length, msg.ToUpper());
Console.WriteLine($"Setting string through managed object: {msg}");

value = getter.GetString();
Console.WriteLine($"Get string through wrapper: {value}");

ComWrapper サブクラスは CreateObjectFlags.UniqueInstance をサポートするように設計したので、GC が発生するのを待たずに、すぐにネイティブ オブジェクト ラッパーをクリーンアップできます。

(rcw as IDisposable)?.Dispose();

ComWrappers を使用した COM のアクティブ化

通常、COM オブジェクトの作成は COM のアクティブ化を介して実行されます。これはこのドキュメントでは扱わない複雑なシナリオです。 従うべき概念パターンを提供するために、COM のアクティブ化に使用される CoCreateInstance() API を紹介し、ComWrappers でどのように使用できるかについて説明します。

アプリケーションに次のような C# コードがあるとします。 次の例では、CoCreateInstance() を使用して COM クラスをアクティブ化し、組み込みの COM 相互運用システムを使用して COM インスタンスを適切なインターフェイスにマーシャリングしています。 typeof(I).GUID の使用はアサートに限定されており、コードが AOT に適しているかどうかに影響する可能性のあるリフレクションを使用している場合であることに注意してください。

public static I ActivateClass<I>(Guid clsid, Guid iid)
{
    Debug.Assert(iid == typeof(I).GUID);
    int hr = CoCreateInstance(ref clsid, IntPtr.Zero, /*CLSCTX_INPROC_SERVER*/ 1, ref iid, out object obj);
    if (hr < 0)
    {
        Marshal.ThrowExceptionForHR(hr);
    }
    return (I)obj;
}

[DllImport("Ole32")]
private static extern int CoCreateInstance(
    ref Guid rclsid,
    IntPtr pUnkOuter,
    int dwClsContext,
    ref Guid riid,
    [MarshalAs(UnmanagedType.Interface)] out object ppObj);

ComWrappers を使用するように上記を変更するには、CoCreateInstance() P/Invoke から MarshalAs(UnmanagedType.Interface) を削除し、手動でマーシャリングを実行する必要があります。

static ComWrappers s_ComWrappers = ...;

public static I ActivateClass<I>(Guid clsid, Guid iid)
{
    Debug.Assert(iid == typeof(I).GUID);
    int hr = CoCreateInstance(ref clsid, IntPtr.Zero, /*CLSCTX_INPROC_SERVER*/ 1, ref iid, out IntPtr obj);
    if (hr < 0)
    {
        Marshal.ThrowExceptionForHR(hr);
    }
    return (I)s_ComWrappers.GetOrCreateObjectForComInstance(obj, CreateObjectFlags.None);
}

[DllImport("Ole32")]
private static extern int CoCreateInstance(
    ref Guid rclsid,
    IntPtr pUnkOuter,
    int dwClsContext,
    ref Guid riid,
    out IntPtr ppObj);

ネイティブ オブジェクト ラッパーのクラス コンストラクターにアクティブ化ロジックを含めることで、ActivateClass<I> のようなファクトリ スタイルの関数を抽象化することもできます。 コンストラクターに ComWrappers.GetOrRegisterObjectForComInstance() API を使用して、新しく構築されたマネージド オブジェクトとアクティブ化された COM インスタンスを関連付けることができます。

その他の注意点

ネイティブ AOT – Ahead-Of-Time (AOT) コンパイルを使用すると、JIT コンパイルが回避されるので、起動コストが向上します。 JIT コンパイルを不要にすることは、一部のプラットフォームでは、多くの場合、必要なことでもあります。 AOT のサポートは ComWrappers API の目標でしたが、ラッパーの実装では、AOT で障害が発生するケース (リフレクションの使用など) が誤って導入されないように、注意する必要があります。 Type.GUID プロパティは、リフレクションがわかりにくい方法で使用される場合の例です。 Type.GUID プロパティでは、リフレクションを使用して、型の属性が検査された後、場合によってはその値を生成するために型の名前とそれを含むアセンブリが検査されます。

ソースの生成 – COM 相互運用と ComWrappers の実装に必要なほとんどのコードは、何らかのツールによって自動生成される可能性があります。 タイプ ライブラリ (TLB)、IDL、プライマリ相互運用機能アセンブリ (PIA) など、適切な COM 定義を指定すると、両方の種類のラッパーのソースを生成できます。

グローバル登録ComWrappers API は COM 相互運用の新しいフェーズとして設計されたので、既存のシステムと部分的に統合する何らかの方法が必要でした。 ComWrappers API には、さまざまなサポートのためにグローバル インスタンスを登録できる、グローバルな影響を与える静的メソッドがあります。 これらのメソッドは、組み込みの COM 相互運用システムと同様に、すべてのケースで包括的な COM 相互運用のサポートを提供することが予想される ComWrappers インスタンス用に設計されています。

参照トラッカーのサポート – このサポートは、WinRT のシナリオで主に使用され、高度なシナリオを表します。 ほとんどの ComWrapper 実装では、CreateComInterfaceFlags.TrackerSupport または CreateObjectFlags.TrackerObject フラグによって NotSupportedException がスローされるはずです。 このサポートを有効にする場合は (おそらく Windows で、または Windows 以外のプラットフォームでも)、C#/WinRT ツール チェーンを参照することを強くお勧めします。

前に説明した有効期間、型システム、関数の機能に加えて、ComWrappers の COM 準拠の実装には、追加の考慮事項が必要です。 Windows プラットフォームで使用されるすべての実装には、次の考慮事項があります。

  • アパートメント – スレッド処理のための COM の組織構造は "アパートメント" と呼ばれ、安定した動作のために従う必要がある厳格な規則があります。 このチュートリアルでは、アパートメント対応のネイティブ オブジェクト ラッパーは実装しませんが、運用可能なすべての実装はアパートメント対応である必要があります。 これを実現するには、Windows 8 で導入された RoGetAgileReference API を使用することをお勧めします。 Windows 8 より前のバージョンの場合は、グローバル インターフェイス テーブルを検討してください。

  • セキュリティ – COM には、クラスのアクティブ化とプロキシされたアクセス許可のための充実したセキュリティ モデルが用意されています。