COM を使用した DirectX のプログラミング
Microsoft Component Object Model (COM) は、DirectX API サーフェスの大部分を含む、いくつかのテクノロジで使用されるオブジェクト指向プログラミング モデルです。 そのため、(DirectX 開発者として) DirectX をプログラムするときに COM を使用することは避けられません。
Note
「 C++/WinRT で COM コンポーネントを使用 する」トピックでは、 C++/WinRT を使用して DirectX API (およびすべての COM API) を使用する方法を示します。 これは、最も便利で推奨されるテクノロジです。
または、生の COM を使用することもできます。これは、このトピックの内容です。 COM API の使用に関連する原則とプログラミング手法の基本的な理解が必要です。 COM は難しく複雑であるという評判がありますが、ほとんどの DirectX アプリケーションで必要な COM プログラミングは簡単です。 これは、DirectX によって提供される COM オブジェクトを使用するためです。 通常は複雑さが生じる独自の COM オブジェクトを作成する必要はありません。
COM コンポーネントの概要
COM オブジェクトは、基本的に、アプリケーションが 1 つ以上のタスクを実行するために使用できる機能のカプセル化されたコンポーネントです。 展開の場合、1 つ以上の COM コンポーネントが COM サーバーと呼ばれるバイナリにパッケージ化されます。DLL ではなく、より頻繁に。
従来の DLL では、無料の関数がエクスポートされます。 COM サーバーでも同じことができます。 ただし、COM サーバー内の COM コンポーネントは、COM インターフェイスと、それらのインターフェイスに属するメンバー メソッドを公開します。 アプリケーションでは、COM コンポーネントのインスタンスを作成し、そこからインターフェイスを取得し、COM コンポーネントに実装されている機能の恩恵を受けるために、これらのインターフェイスでメソッドを呼び出します。
実際には、これは通常の C++ オブジェクトでメソッドを呼び出すのと似ています。 しかし、いくつかの違いがあります。
- COM オブジェクトでは、C++ オブジェクトよりも厳密なカプセル化が適用されます。 オブジェクトを作成し、パブリック メソッドを呼び出すことはできません。 代わりに、COM コンポーネントのパブリック メソッドは 1 つ以上の COM インターフェイスにグループ化されます。 メソッドを呼び出すには、 オブジェクトを作成し、 メソッドを実装するインターフェイスをオブジェクトから取得します。 インターフェイスは、通常、オブジェクトの特定の機能へのアクセスを提供する関連する一連のメソッドを実装します。 たとえば、 ID3D12Device インターフェイスは仮想グラフィックス アダプターを表し、たとえば、他の多くのアダプター関連タスクなどのリソースを作成できるメソッドが含まれています。
- COM オブジェクトは、C++ オブジェクトと同じ方法で作成されません。 COM オブジェクトを作成するにはいくつかの方法がありますが、すべて COM 固有の手法が含まれます。 DirectX API には、ほとんどの DirectX COM オブジェクトの作成を簡略化するさまざまなヘルパー関数とメソッドが含まれています。
- COM オブジェクトの有効期間を制御するには、COM 固有の手法を使用する必要があります。
- COM サーバー (通常は DLL) を明示的に読み込む必要はありません。 また、COM コンポーネントを使用するために静的ライブラリにリンクすることもありません。 各 COM コンポーネントには、アプリケーションが COM オブジェクトを識別するために使用する一意の登録識別子 (グローバル一意識別子または GUID) があります。 アプリケーションによってコンポーネントが識別され、COM ランタイムによって正しい COM サーバー DLL が自動的に読み込まれます。
- COM はバイナリ仕様です。 COM オブジェクトは、さまざまな言語で記述したり、さまざまな言語からアクセスしたりできます。 オブジェクトのソース コードについて何も知る必要はありません。 たとえば、Visual Basic アプリケーションでは、C++ で記述された COM オブジェクトが日常的に使用されます。
コンポーネント、オブジェクト、インターフェイス
コンポーネント、オブジェクト、インターフェイスの違いを理解することが重要です。 さりげない使い方では、そのプリンシパル インターフェイスの名前で参照されるコンポーネントまたはオブジェクトが読み上げられる場合があります。 ただし、用語は交換可能ではありません。 コンポーネントは、任意の数のインターフェイスを実装できます。オブジェクトは コンポーネントのインスタンスです。 たとえば、すべてのコンポーネントが IUnknown インターフェイスを実装する必要がある一方で、通常は少なくとも 1 つの追加インターフェイスを実装し、多くを実装する場合があります。
特定のインターフェイス メソッドを使用するには、オブジェクトをインスタンス化するだけでなく、そのメソッドから正しいインターフェイスを取得する必要もあります。
さらに、複数のコンポーネントが同じインターフェイスを実装する場合があります。 インターフェイスは、論理的に関連する一連の操作を実行するメソッドのグループです。 インターフェイス定義では、メソッドの構文とその一般的な機能のみが指定されます。 特定の一連の操作をサポートする必要がある COM コンポーネントは、適切なインターフェイスを実装することでこれを行うことができます。 一部のインターフェイスは高度に特殊化されており、1 つのコンポーネントによってのみ実装されます。その他は、さまざまな状況で役立ち、多くのコンポーネントによって実装されています。
コンポーネントがインターフェイスを実装する場合は、インターフェイス定義内のすべてのメソッドをサポートする必要があります。 言い換えると、任意のメソッドを呼び出し、そのメソッドが存在することを確信できる必要があります。 ただし、特定のメソッドの実装方法の詳細は、コンポーネントによって異なる場合があります。 たとえば、コンポーネントごとに異なるアルゴリズムを使用して、最終的な結果が得られます。 また、メソッドが非実際的な方法でサポートされる保証もありません。 コンポーネントは一般的に使用されるインターフェイスを実装する場合がありますが、メソッドのサブセットのみをサポートする必要があります。 残りのメソッドは引き続き正常に呼び出されますが、値E_NOTIMPLを含む HRESULT (結果コードを表す標準的な COM 型) が返されます。 インターフェイスが特定のコンポーネントによって実装される方法については、そのドキュメントを参照してください。
COM 標準では、発行後にインターフェイス定義を変更しないようにする必要があります。 たとえば、作成者は、既存のインターフェイスに新しいメソッドを追加することはできません。 作成者は、代わりに新しいインターフェイスを作成する必要があります。 そのインターフェイスに必要なメソッドに制限はありませんが、一般的な方法は、次世代インターフェイスに古いインターフェイスのすべてのメソッドと新しいメソッドを含める方法です。
インターフェイスが複数の世代を持つことは珍しいことではありません。 通常、すべての世代は基本的に全体的に同じタスクを実行しますが、具体的には異なります。 多くの場合、COM コンポーネントは、特定のインターフェイスの系列の現在および以前のすべての世代を実装します。 これにより、古いアプリケーションはオブジェクトの古いインターフェイスを引き続き使用できますが、新しいアプリケーションでは新しいインターフェイスの機能を利用できます。 通常、インターフェイスの降下グループはすべて同じ名前と、生成を示す整数を持っています。 たとえば、元のインターフェイスの名前が IMyInterface (世代 1 を意味する) の場合、次の 2 つの世代は IMyInterface2 と IMyInterface3 と呼ばれます。 DirectX インターフェイスの場合は、通常、DirectX のバージョン番号に対して連続する世代の名前が付けられます。
GUID
GUID は、COM プログラミング モデルの重要な部分です。 最も基本的な GUID は 128 ビット構造です。 ただし、GUID は、2 つの GUID が同じでないことを保証するように作成されます。 COM では、2 つの主な目的のために GUID が広範囲に使用されます。
- 特定の COM コンポーネントを一意に識別する。 COM コンポーネントを識別するために割り当てられる GUID はクラス識別子 (CLSID) と呼ばれ、関連付けられている COM コンポーネントのインスタンスを作成するときに CLSID を使用します。
- 特定の COM インターフェイスを一意に識別する。 COM インターフェイスを識別するために割り当てられる GUID はインターフェイス識別子 (IID) と呼ばれ、コンポーネント (オブジェクト) のインスタンスから特定のインターフェイスを要求するときに IID を使用します。 インターフェイスの IID は、インターフェイスを実装するコンポーネントに関係なく同じになります。
便宜上、DirectX ドキュメントでは通常、コンポーネントとインターフェイスを GUID ではなく、わかりやすい名前 ( ID3D12Device など) で参照しています。 DirectX ドキュメントのコンテキスト内では、あいまいさはありません。 サードパーティは、 ID3D12Device というわかりやすい名前のインターフェイスを作成することが技術的に可能です (有効にするには別の IID が必要になります)。 ただし、わかりやすくするために、これはお勧めしません。
したがって、特定のオブジェクトまたはインターフェイスを参照する唯一の明確な方法は、その GUID です。
GUID は構造体ですが、多くの場合、GUID は同等の文字列形式で表されます。 GUID の文字列形式の一般的な形式は 32 桁の 16 進数で、形式は 8-4-4-4-12 です。 つまり、{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} です。各 x は 16 進数に対応します。 たとえば、 ID3D12Device インターフェイスの IID の文字列形式は {189819F1-1DB6-4B57-BE54-1821339B85F7} です。
実際の GUID は使用がやや不十分で、誤入力しやすいため、通常は同等の名前も提供されます。 コードでは、 パラメーターの引数 riid
を D3D12CreateDevice に渡す場合など、関数を呼び出すときに、実際の構造体の代わりにこの名前を使用できます。 通常の名前付け規則では、インターフェイスまたはオブジェクトのわかりやすい名前にそれぞれIID_またはCLSID_を付加します。 たとえば、 ID3D12Device インターフェイスの IID の名前はIID_ID3D12Device。
Note
DirectX アプリケーションは、 および uuid.lib
とdxguid.lib
リンクして、さまざまなインターフェイスとクラス GUID の定義を提供する必要があります。 Visual C++ やその他のコンパイラでは 、__uuidof 演算子言語拡張機能がサポートされていますが、これらのリンク ライブラリとの明示的な C スタイルのリンケージもサポートされ、完全に移植可能です。
HRESULT 値
ほとんどの COM メソッドは、 HRESULT と呼ばれる 32 ビット整数を返します。 ほとんどの方法では、HRESULT は基本的に 2 つの主要な情報を含む構造です。
- メソッドが成功したか失敗したか。
- メソッドによって実行される操作の結果に関する詳細情報。
一部のメソッドは、 でWinerror.h
定義されている標準セットから HRESULT 値を返します。 ただし、メソッドは、より特殊な情報を含むカスタム HRESULT 値を自由に返します。 通常、これらの値はメソッドの参照ページに記載されています。
メソッドの参照ページで見つかる HRESULT 値の一覧は、多くの場合、返される可能性のある値のサブセットにすぎません。 通常、この一覧には、メソッドに固有の値と、メソッド固有の意味を持つ標準値のみが含まれます。 明示的に文書化されていない場合でも、メソッドがさまざまな標準 HRESULT 値を返す可能性があると想定する必要があります。
HRESULT 値はエラー情報を返すためによく使用されますが、エラー コードとは考えてはいけません。 成功または失敗を示すビットが詳細情報を含むビットとは別に格納されるという事実により、 HRESULT 値には任意の数の成功コードと失敗コードを含めることができます。 慣例により、成功コードの名前には、S_、失敗コードの前に E_ が付けられます。 たとえば、最もよく使用される 2 つのコードはS_OKとE_FAILで、それぞれ単純な成功または失敗を示します。
COM メソッドがさまざまな成功コードまたは失敗コードを返す可能性があるということは、 HRESULT 値をテストする方法に注意する必要があることを意味します。 たとえば、成功した場合は S_OK の戻り値を文書化し、そうでない場合はE_FAILする仮定のメソッドを考えてみましょう。 ただし、 メソッドは、他のエラーコードや成功コードも返す可能性があることを覚えておいてください。 次のコード フラグメントは、 メソッドによって返される HRESULT 値をhr
含む単純なテストを使用する危険性を示しています。
if (hr == E_FAIL)
{
// Handle the failure case.
}
else
{
// Handle the success case.
}
障害が発生した場合、このメソッドはE_FAILのみを返します (他のエラー コードは返しません)、このテストは機能します。 ただし、特定のメソッドを実装して、特定の障害コードのセット (E_NOTIMPLまたはE_INVALIDARG) を返す方が現実的です。 上記のコードでは、これらの値は正しく成功と解釈されません。
メソッド呼び出しの結果に関する詳細情報が必要な場合は、関連する 各 HRESULT 値をテストする必要があります。 ただし、メソッドが成功したか失敗したかにのみ関心がある場合があります。 HRESULT 値が成功または失敗を示すかどうかをテストする堅牢な方法は、Winerror.h で定義されている次のいずれかのマクロに値を渡すことです。
- マクロは成功コードの場合は TRUE、失敗コードの場合は
SUCCEEDED
FALSE を返します。 - エラー コードの場合は TRUE、成功コードの場合は
FAILED
FALSE を返します。
そのため、次のコードに示すように、 マクロを FAILED
使用して、上記のコード フラグメントを修正できます。
if (FAILED(hr))
{
// Handle the failure case.
}
else
{
// Handle the success case.
}
この修正されたコード フラグメントは、E_NOTIMPLとE_INVALIDARGをエラーとして適切に処理します。
ほとんどの COM メソッドは構造化 された HRESULT 値を返しますが、小さい数値では HRESULT を使用して単純な整数を返します。 暗黙的に、これらのメソッドは常に成功します。 この種類の HRESULT を SUCCEEDED マクロに渡すと、マクロは常に TRUE を返します。 HRESULT を返さない一般的に呼び出されるメソッドの例として、ULONG を返す IUnknown::Release メソッドがあります。 このメソッドは、オブジェクトの参照カウントを 1 ずつデクリメントし、現在の参照カウントを返します。 参照カウントの詳細については、「 COM オブジェクトの有効期間の管理 」を参照してください。
ポインターのアドレス
いくつかの COM メソッド参照ページを表示すると、次のように実行される可能性があります。
HRESULT D3D12CreateDevice(
IUnknown *pAdapter,
D3D_FEATURE_LEVEL MinimumFeatureLevel,
REFIID riid,
void **ppDevice
);
通常のポインターは C/C++ 開発者にとってよく知られていますが、COM では多くの場合、追加のレベルの間接参照が使用されます。 この第 2 レベルの間接参照は、 **
型宣言に続く 2 つのアスタリスクで示され、変数名には通常、 の pp
プレフィックスが付きます。 上記の関数の場合、 ppDevice
パラメーターは通常、void へのポインターのアドレスと呼ばれます。 実際には、この例では、 ppDevice
は ID3D12Device インターフェイスへのポインターのアドレスです。
C++ オブジェクトとは異なり、COM オブジェクトのメソッドには直接アクセスしません。 代わりに、 メソッドを公開するインターフェイスへのポインターを取得する必要があります。 メソッドを呼び出すには、C++ メソッドへのポインターを呼び出すのと基本的に同じ構文を使用します。 たとえば、 IMyInterface::D oSomething メソッドを 呼び出すには、次の構文を使用します。
IMyInterface * pMyIface = nullptr;
...
pMyIface->DoSomething(...);
第 2 レベルの間接参照の必要性は、インターフェイス ポインターを直接作成しないという事実から生まれます。 上記の D3D12CreateDevice メソッドなど、さまざまなメソッドのいずれかを呼び出す必要があります。 このようなメソッドを使用してインターフェイス ポインターを取得するには、変数を目的のインターフェイスへのポインターとして宣言し、その変数のアドレスを メソッドに渡します。 つまり、ポインターのアドレスを メソッドに渡します。 メソッドが戻ると、変数は要求されたインターフェイスを指し、そのポインターを使用してインターフェイスのメソッドのいずれかを呼び出すことができます。
IDXGIAdapter * pIDXGIAdapter = nullptr;
...
ID3D12Device * pD3D12Device = nullptr;
HRESULT hr = ::D3D12CreateDevice(
pIDXGIAdapter,
D3D_FEATURE_LEVEL_11_0,
IID_ID3D12Device,
&pD3D12Device);
if (FAILED(hr)) return E_FAIL;
// Now use pD3D12Device in the form pD3D12Device->MethodName(...);
COM オブジェクトの作成
COM オブジェクトを作成するには、いくつかの方法があります。 これらは、DirectX プログラミングで最も一般的に使用される 2 つです。
- 間接的に、オブジェクトを作成する DirectX メソッドまたは関数を呼び出します。 メソッドは オブジェクトを作成し、 オブジェクトのインターフェイスを返します。 この方法でオブジェクトを作成すると、返されるインターフェイスを指定できる場合もあれば、インターフェイスが暗黙的に指定される場合もあります。 上記のコード例は、Direct3D 12 デバイス COM オブジェクトを間接的に作成する方法を示しています。
- 直接、オブジェクトの CLSID を CoCreateInstance 関数に渡します。 関数は オブジェクトのインスタンスを作成し、指定したインターフェイスへのポインターを返します。
1 回、COM オブジェクトを作成する前に、 CoInitializeEx 関数を呼び出して COM を初期化する必要があります。 オブジェクトを間接的に作成する場合、オブジェクト作成メソッドはこのタスクを処理します。 ただし、 CoCreateInstance を使用してオブジェクトを作成する必要がある場合は、 CoInitializeEx を明示的に呼び出す必要があります。 完了したら、 CoUninitialize を呼び出して COM を初期化解除する必要があります。 CoInitializeEx を呼び出す場合は、CoUninitialize の呼び出しと一致させる必要があります。 通常、COM を明示的に初期化する必要があるアプリケーションは、スタートアップ ルーチンでこれを行い、クリーンアップ ルーチンで COM を初期化解除します。
CoCreateInstance を使用して COM オブジェクトの新しいインスタンスを作成するには、オブジェクトの CLSID が必要です。 この CLSID が一般公開されている場合は、リファレンス ドキュメントまたは適切なヘッダー ファイルに記載されています。 CLSID が一般公開されていない場合、オブジェクトを直接作成することはできません。
CoCreateInstance 関数には、5 つのパラメーターがあります。 DirectX で使用する COM オブジェクトの場合は、通常、次のようにパラメーターを設定できます。
rclsid これを、作成するオブジェクトの CLSID に設定します。
pUnkOuter を に設定します nullptr
。 このパラメーターは、オブジェクトを集計する場合にのみ使用されます。 COM 集計については、このトピックの範囲外です。
dwClsContext CLSCTX_INPROC_SERVERに設定します。 この設定は、オブジェクトが DLL として実装され、アプリケーションのプロセスの一部として実行されることを示します。
Riid 返すインターフェイスの IID に設定します。 関数は オブジェクトを作成し、ppv パラメーターで要求されたインターフェイス ポインターを返します。
Ppv これを、関数が返すときに 指定 riid
されたインターフェイスに設定されるポインターのアドレスに設定します。 この変数は要求されたインターフェイスへのポインターとして宣言する必要があり、パラメーター リスト内のポインターへの参照は (LPVOID *) としてキャストする必要があります。
オブジェクトを間接的に作成する方法は、上記のコード例で説明したように、通常ははるかに簡単です。 オブジェクト作成メソッドにインターフェイス ポインターのアドレスを渡し、 メソッドで オブジェクトを作成し、インターフェイス ポインターを返します。 オブジェクトを間接的に作成する場合、メソッドが返すインターフェイスを選択できない場合でも、多くの場合、オブジェクトの作成方法についてさまざまなことを指定できます。
たとえば、上記のコード例に示すように、返されるデバイスでサポートする必要がある最小 D3D 機能レベルを指定する値を D3D12CreateDevice に渡すことができます。
COM インターフェイスの使用
COM オブジェクトを作成すると、作成メソッドはインターフェイス ポインターを返します。 その後、そのポインターを使用して、インターフェイスのメソッドのいずれかにアクセスできます。 構文は、C++ メソッドへのポインターで使用される構文と同じです。
追加のインターフェイスの要求
多くの場合、作成メソッドから受け取るインターフェイス ポインターだけが必要な場合があります。 実際、オブジェクトが IUnknown 以外のインターフェイスを 1 つだけエクスポートするのは比較的一般的です。 ただし、多くのオブジェクトは複数のインターフェイスをエクスポートするため、それらのいくつかへのポインターが必要な場合があります。 作成メソッドによって返されるインターフェイスよりも多くのインターフェイスが必要な場合は、新しいオブジェクトを作成する必要はありません。 代わりに、オブジェクトの IUnknown::QueryInterface メソッドを使用して、別のインターフェイス ポインターを要求します。
CoCreateInstance を使用してオブジェクトを作成する場合は、IUnknown インターフェイス ポインターを要求してから IUnknown::QueryInterface を呼び出して、必要なすべてのインターフェイスを要求できます。 ただし、この方法は、1 つのインターフェイスのみが必要な場合は不便です。また、返されるインターフェイス ポインターを指定できないオブジェクト作成メソッドを使用する場合は、まったく機能しません。 実際には、すべての COM インターフェイスが IUnknown インターフェイスを拡張するため、通常は明示的な IUnknown ポインターを取得する必要はありません。
インターフェイスの拡張は、概念的には C++ クラスからの継承に似ています。 子インターフェイスは、親インターフェイスのすべてのメソッドと、それ自体の 1 つ以上を公開します。 実際には、多くの場合、"拡張" の代わりに "inherits from" が使用されます。 覚えておく必要があるのは、継承がオブジェクトの内部にあるということです。 アプリケーションでオブジェクトのインターフェイスを継承または拡張することはできません。 ただし、子インターフェイスを使用して、子または親のいずれかのメソッドを呼び出すことができます。
すべてのインターフェイスは IUnknown の子であるため、オブジェクトに対して既に持っているインターフェイス ポインターのいずれかで QueryInterface を呼び出すことができます。 その場合は、要求するインターフェイスの IID と、メソッドが戻るときにインターフェイス ポインターを含むポインターのアドレスを指定する必要があります。
たとえば、次のコード フラグメントは IDXGIFactory2::CreateSwapChainForHwnd を呼び出してプライマリ スワップ チェーン オブジェクトを作成します。 このオブジェクトは、いくつかのインターフェイスを公開します。 CreateSwapChainForHwnd メソッドは IDXGISwapChain1 インターフェイスを返します。 その後、後続のコードは IDXGISwapChain1 インターフェイスを使用して QueryInterface を 呼び出し、 IDXGISwapChain3 インターフェイスを 要求します。
HRESULT hr = S_OK;
IDXGISwapChain1 * pDXGISwapChain1 = nullptr;
hr = pIDXGIFactory->CreateSwapChainForHwnd(
pCommandQueue, // For D3D12, this is a pointer to a direct command queue.
hWnd,
&swapChainDesc,
nullptr,
nullptr,
&pDXGISwapChain1));
if (FAILED(hr)) return hr;
IDXGISwapChain3 * pDXGISwapChain3 = nullptr;
hr = pDXGISwapChain1->QueryInterface(IID_IDXGISwapChain3, (LPVOID*)&pDXGISwapChain3);
if (FAILED(hr)) return hr;
Note
C++ では、明示的な IID とキャスト ポインターではなく、 マクロを使用IID_PPV_ARGS
できます。 pDXGISwapChain1->QueryInterface(IID_PPV_ARGS(&pDXGISwapChain3));
これは、多くの場合、 QueryInterface だけでなく、作成方法にも使用されます。 詳細については、 combaseapi.h を参照してください。
COM オブジェクトの有効期間の管理
オブジェクトが作成されると、システムは必要なメモリ リソースを割り当てます。 オブジェクトが不要になった場合は、破棄する必要があります。 システムはそのメモリを他の目的で使用できます。 C++ オブジェクトを使用すると、そのレベルで操作している場合、またはスタックとスコープの有効期間を使用するだけで、 演算子と delete
演算子を使用してオブジェクトの有効期間を直接new
制御できます。 COM では、オブジェクトを直接作成または破棄することはできません。 この設計の理由は、同じオブジェクトがアプリケーションの複数の部分で使用されるか、場合によっては複数のアプリケーションによって使用される可能性があるためです。 これらの参照の 1 つがオブジェクトを破棄する場合、他の参照は無効になります。 代わりに、COM は参照カウントのシステムを使用して、オブジェクトの有効期間を制御します。
オブジェクトの参照カウントは、そのインターフェイスの 1 つが要求された回数です。 インターフェイスが要求されるたびに、参照カウントがインクリメントされます。 アプリケーションは、そのインターフェイスが不要になったときにインターフェイスを解放し、参照カウントをデクリメントします。 参照カウントが 0 より大きい限り、オブジェクトはメモリ内に残ります。 参照カウントが 0 に達すると、オブジェクト自体が破棄されます。 オブジェクトの参照カウントについて何も知る必要はありません。 オブジェクトのインターフェイスを適切に取得して解放する限り、オブジェクトは適切な有効期間を持ちます。
参照カウントを適切に処理することは、COM プログラミングの重要な部分です。 これを行わないと、メモリ リークやクラッシュが簡単に発生する可能性があります。 COM プログラマが行う最も一般的な間違いの 1 つは、インターフェイスのリリースに失敗することです。 この場合、参照カウントはゼロに達せず、オブジェクトは無期限にメモリ内に残ります。
Note
Direct3D 10 以降では、オブジェクトの有効期間ルールが若干変更されています。 特に、 ID3DxxDeviceChild から派生したオブジェクトは、親デバイスを追い抜くことはありません (つまり、所有している ID3DxxDevice が 0 refcount にヒットした場合、すべての子オブジェクトもすぐに無効になります)。 また、 Set メソッドを使用してオブジェクトをレンダー パイプラインにバインドしても、これらの参照は参照数を増やしません (つまり、弱い参照です)。 実際には、これは、デバイスを解放する前にすべてのデバイス子オブジェクトを完全に解放することによって処理するのが最適です。
参照カウントのインクリメントとデクリメント
新しいインターフェイス ポインターを取得するたびに、 IUnknown::AddRef の呼び出しによって参照カウントをインクリメントする必要があります。 ただし、アプリケーションでは通常、このメソッドを呼び出す必要はありません。 オブジェクト作成メソッドを呼び出すか、 IUnknown::QueryInterface を呼び出してインターフェイス ポインターを取得すると、オブジェクトは参照カウントを自動的にインクリメントします。 ただし、既存のポインターのコピーなど、他の方法でインターフェイス ポインターを作成する場合は、 IUnknown::AddRef を明示的に呼び出す必要があります。 それ以外の場合、元のインターフェイス ポインターを解放すると、ポインターのコピーを使用する必要がある場合でも、オブジェクトが破棄される可能性があります。
自分またはオブジェクトが参照カウントをインクリメントしたかどうかにかかわらず、すべてのインターフェイス ポインターを解放する必要があります。 インターフェイス ポインターが不要になったら、 IUnknown::Release を呼び出して参照カウントをデクリメントします。 一般的な方法は、 へのすべてのインターフェイス ポインターを nullptr
初期化してから、解放されたときに に戻 nullptr
す方法です。 この規則を使用すると、クリーンアップ コード内のすべてのインターフェイス ポインターをテストできます。 アクティブでない nullptr
ものはアクティブであり、アプリケーションを終了する前に解放する必要があります。
次のコード フラグメントは、前に示したサンプルを拡張して、参照カウントの処理方法を示しています。
HRESULT hr = S_OK;
IDXGISwapChain1 * pDXGISwapChain1 = nullptr;
hr = pIDXGIFactory->CreateSwapChainForHwnd(
pCommandQueue, // For D3D12, this is a pointer to a direct command queue.
hWnd,
&swapChainDesc,
nullptr,
nullptr,
&pDXGISwapChain1));
if (FAILED(hr)) return hr;
IDXGISwapChain3 * pDXGISwapChain3 = nullptr;
hr = pDXGISwapChain1->QueryInterface(IID_IDXGISwapChain3, (LPVOID*)&pDXGISwapChain3);
if (FAILED(hr)) return hr;
IDXGISwapChain3 * pDXGISwapChain3Copy = nullptr;
// Make a copy of the IDXGISwapChain3 interface pointer.
// Call AddRef to increment the reference count and to ensure that
// the object is not destroyed prematurely.
pDXGISwapChain3Copy = pDXGISwapChain3;
pDXGISwapChain3Copy->AddRef();
...
// Cleanup code. Check to see whether the pointers are still active.
// If they are, then call Release to release the interface.
if (pDXGISwapChain1 != nullptr)
{
pDXGISwapChain1->Release();
pDXGISwapChain1 = nullptr;
}
if (pDXGISwapChain3 != nullptr)
{
pDXGISwapChain3->Release();
pDXGISwapChain3 = nullptr;
}
if (pDXGISwapChain3Copy != nullptr)
{
pDXGISwapChain3Copy->Release();
pDXGISwapChain3Copy = nullptr;
}
COM スマート ポインター
これまでのコードでは、 と を明示的に呼び出 Release
し AddRef
、 IUnknown メソッドを使用して参照カウントを維持しています。 このパターンでは、プログラマは、考えられるすべてのコードパスでカウントを適切に維持することを覚えておく必要があります。 これにより、複雑なエラー処理が発生する可能性があり、C++ 例外処理を有効にすると、実装が特に困難になる可能性があります。 C++ の方が優れたソリューションは、 スマート ポインターを使用することです。
winrt::com_ptr は、 C++/WinRT 言語プロジェクションによって提供されるスマート ポインターです。 これは、UWP アプリに使用するために推奨される COM スマート ポインターです。 C++/WinRT には C++17 が必要であることに注意してください。
Microsoft::WRL::ComPtr は、Windows ランタイム C++ テンプレート ライブラリ (WRL) によって提供されるスマート ポインターです。 このライブラリは "純粋な" C++ であるため、(C++/CX または C++/WinRT を介して) Windows ランタイム アプリケーションや Win32 デスクトップ アプリケーションに使用できます。 このスマート ポインターは、Windows ランタイム API をサポートしていない古いバージョンの Windows でも機能します。 Win32 デスクトップ アプリケーションでは、 を使用
#include <wrl/client.h>
してこのクラスのみを含め、必要に応じてプリプロセッサ シンボル__WRL_CLASSIC_COM_STRICT__
も定義できます。 詳細については、「 COM スマート ポインターの再検討」を参照してください。CComPtr は、 Active Template Library (ATL) によって提供されるスマート ポインターです。 Microsoft::WRL::ComPtr は、いくつかの微妙な使用の問題に対処するこの実装の新しいバージョンであるため、このスマート ポインターの使用は新しいプロジェクトには推奨されません。 詳細については、「 CComPtr と CComQIPtr を作成して使用する方法」を参照してください。
DirectX 9 での ATL の使用
DirectX 9 で Active Template Library (ATL) を使用するには、ATL 互換性のためにインターフェイスを再定義する必要があります。 これにより、 CComQIPtr クラスを適切に使用して、インターフェイスへのポインターを取得できます。
ATL のインターフェイスを再定義しない場合は、次のエラー メッセージが表示されるのでわかります。
[...]\atlmfc\include\atlbase.h(4704) : error C2787: 'IDirectXFileData' : no GUID has been associated with this object
次のコード サンプルは、IDirectXFileData インターフェイスを定義する方法を示しています。
// Explicit declaration
struct __declspec(uuid("{3D82AB44-62DA-11CF-AB39-0020AF71E433}")) IDirectXFileData;
// Macro method
#define RT_IID(iid_, name_) struct __declspec(uuid(iid_)) name_
RT_IID("{1DD9E8DA-1C77-4D40-B0CF-98FEFDFF9512}", IDirectXFileData);
インターフェイスを再定義した後、 Attach メソッドを使用して、 ::D irect3DCreate9 によって返されるインターフェイス ポインターにインターフェイスをアタッチする必要があります。 そうしないと、スマート ポインター クラスによって IDirect3D9 インターフェイスが正しく解放されません。
CComPtr クラスは、オブジェクトの作成時と CComPtr クラスへのインターフェイスの割り当て時に、インターフェイス ポインターで IUnknown::AddRef を内部的に呼び出します。 インターフェイス ポインターのリークを回避するには、 ::D irect3DCreate9 から返されるインターフェイスで **IUnknown::AddRef を呼び出さないでください。
次のコードは、 IUnknown::AddRef を呼び出さずにインターフェイスを適切に解放します。
CComPtr<IDirect3D9> d3d;
d3d.Attach(::Direct3DCreate9(D3D_SDK_VERSION));
前のコードを使用します。 IUnknown::AddRef の後に IUnknown::Release を呼び出す次のコードは使用しないでください。また、::D irect3DCreate9 によって追加された参照は解放されません。
CComPtr<IDirect3D9> d3d = ::Direct3DCreate9(D3D_SDK_VERSION);
Direct3D 9 では、この方法で Attach メソッドを使用する必要がある唯一の場所であることに注意してください。
CComPTR クラスと CComQIPtr クラスの詳細については、ヘッダー ファイルの定義をAtlbase.h
参照してください。