COM オブジェクトの概要

COM オブジェクトは、基本的に、1 つ以上のタスクを実行するためにアプリケーションで使用されるブラック ボックスです。それらは一般的に、DLL として実装されます。従来の DLL のように、COM オブジェクトは、任意のサポートされているタスクを実行するためにアプリケーションで呼び出すことができるメソッドを公開します。アプリケーションは、C++ オブジェクトとやり取りする場合と同じ方法で、COM オブジェクトとやり取りします。しかし、両者にはいくつか異なる点が存在します。

  • COM オブジェクトでは、C++ オブジェクトよりも厳密なカプセル化が強制されます。単純にオブジェクトを作成してパブリック メソッドを呼び出すことはできません。COM オブジェクトのパブリック メソッドは、1 つ以上のインターフェイスにグループ化されます。メソッドを使用するには、オブジェクトを作成して、そのオブジェクトから適切なインターフェイスを取得する必要があります。通常、インターフェイスには、オブジェクトの特定の機能へのアクセスを提供する、関連のあるメソッド一式が含まれています。たとえば、IDirect3DCubeTexture9 インターフェイスには、キューブ テクスチャー リソースを操作できるようにするメソッドが含まれています。インターフェイスに含まれていないメソッドにはアクセスできません。
  • COM オブジェクトは、C++ オブジェクトと同じ方法では作成できません。COM オブジェクトの作成方法はいくつかありますが、すべて、COM 固有のテクニックを伴います。DirectX API には、ほとんどの DirectX オブジェクトの作成を簡略化するさまざまなヘルパー関数とメソッドが含まれています。
  • オブジェクトの有効期間を制御するには、COM 固有のテクニックを使用する必要があります。
  • COM オブジェクトを明示的に読み込む必要はありません。COM オブジェクトは、通常は DLL 内に含まれています。しかし、COM オブジェクトを使用するために、DLL を明示的に読み込むことや、スタティック ライブラリへリンクする必要はありません。各 COM オブジェクトには、オブジェクトの作成に使用される一意な登録済み識別子があります。COM によって正しい DLL が自動的に読み込まれます。
  • COM はバイナリ仕様です。COM オブジェクトは、さまざまな言語で記述およびアクセスできます。オブジェクトのソース コードについて知る必要は何もありません。たとえば、Visual Basic アプリケーションでは、C++ で記述された COM オブジェクトを当たり前のように使用します。

ここでは、以下のトピックについて説明します。

  • オブジェクトとインターフェイス
  • GUID
  • HRESULT 値
  • ポインターのアドレス

オブジェクトとインターフェイス

オブジェクトとインターフェイスの違いを理解することは重要です。簡単な使用方法では、オブジェクトは、その主要なインターフェイスの名前によって参照されることがあります。ただし、厳密に言うと、この 2 つの用語は相互に置き換え可能ではありません。オブジェクトは、任意の数のインターフェイスを公開できます。たとえば、全てのオブジェクトが IUnknown インターフェイスを公開する必要があると同時に、それらは通常 1 つ以上のインターフェイスをさらに公開します。そして、多数のインターフェイスを公開する可能性があります。特定のメソッドを使用するには、オブジェクトを作成するだけでなく、適切なインターフェイスを取得する必要もあります。

また、複数のオブジェクトで、同じインターフェイスを公開する可能性があります。インターフェイスは、指定された一連の操作を実行するメソッドのグループです。インターフェイス定義では、メソッドの構文とそれらの一般的な機能が指定されます。特定の一連の処理をサポートする必要がある COM オブジェクトでは、適切なインターフェイスを公開することによってそれを実現できます。一部のインターフェイスは、非常に特化されており、単一のオブジェクトでしか公開されません。それ以外のインターフェイスは、さまざまな状況で役に立ち、多くのオブジェクトで公開されます。極端な例は IUnknown インターフェイスで、これは、すべての COM オブジェクトで公開される必要があります。

    オブジェクトでインターフェイスを公開する場合は、そのオブジェクトで、インターフェイス定義内のすべてのメソッドがサポートされている必要があります。つまり、すべてのメソッドを呼び出すことができ、それらの存在を確信できなければなりません。ただし、特定のメソッドの実装方法の詳細は、オブジェクトによって異なる可能性があります。たとえば、異なるオブジェクトでは、異なるアルゴリズムを使用して、最終的な結果に到達する可能性があります。また、メソッドが、単純でない方法でサポートされるという保証はありません。オブジェクトにおいて、一般的に使用されるインターフェイスを公開していても、メソッドのサブセットしかサポートする必要がない場合があります。まだ残りのメソッドを呼び出すことはできますが、E_NOTIMPL が返されます。特定のオブジェクトによるインターフェイスの実装方法を確認するには、そのドキュメントを参照してください。

COM 規格では、インターフェイス定義は、一旦公開したら変更してはいけません。たとえば、既存のインターフェイスに新しいメソッドを追加することはできません。代わりに、新しいインターフェイスを作成する必要があります。インターフェイス内に存在しなければならないメソッドに関して制限はありませんが、一般的な方法では、次世代のインターフェイスに、古いインターフェイスのすべてのメソッドと、任意の新しいメソッドを含めます。

1 つのインターフェイスに複数の世代があることは珍しくありません。通常は、全世代で、基本的にすべて同じタスクが実行されますが、それらの詳細は異なります。多くの場合、オブジェクトでインターフェイスのすべての世代が公開されます。それにより、新しいアプリケーションで新しいインターフェイスの機能を利用しながら、古いアプリケーションでオブジェクトの古いインターフェイスを使い続けることができます。一般的に、インターフェイスのすべてのファミリは、世代を示す整数が付いた同じ名前を持ちます。たとえば、元のインターフェイスが IMyInterface という名前だった場合は、次の 2 世代には IMyInterface2IMyInterface3 という名前が付けられます。DirectX インターフェイスの場合は、後続の世代には、通常は DirectX のバージョン番号に由来する名前が付けられます。

GUID

GUID は、COM プログラミング モデルの重要な一部です。最も基本的な GUID は、128 ビットの構造体です。ただし、GUID は、同じ GUID は 2 つとないということが保証される方法で作成されます。COM では、2 つの主な目的のために GUID が幅広く使用されます。

  1. 特定の COM オブジェクトを一意に識別するには

    COM オブジェクトへ割り当てられた GUID は CLSID と呼ばれ、関連付けられている COM オブジェクトのインスタンスを作成する場合に使用します。

  2. 特定の COM インターフェイスを一意に識別するには

    特定の COM インターフェイスに関連付けられた GUID は IID と呼ばれ、オブジェクトから特定のインターフェイスを要求する場合に使用します。インターフェイスの IID は、そのインターフェイスを公開したオブジェクトに関わらず同じとなります。

    便宜上、ドキュメントにおいては、通常はオブジェクトとインターフェイスを IDirect3D9 のようなわかりやすい名前で呼びます。ドキュメント内での混同の危険性はめったにありません。ただし、厳密には、別のオブジェクトやインターフェイスに同じ名前が付いていないという保証はありません。特定のオブジェクトやインターフェイスを明確に示す唯一の方法は、それを GUID で示す方法です。

GUID は構造体ですが、多くの場合、同義の文字列で表されます。GUID の一般的な文字列形式は、8-4-4-4-12 という形式の 32 桁の 16 進数です。つまり、{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} となり、ここでの各 x は16 進数値に相当します。たとえば、IDirect3D9 インターフェイスの IID の文字列形式は、{1DD9E8DA-1C77-4D40-B0CF-98FEFDFF9512} となります。

実際の GUID は、使いにくくミスタイプが起こりやすいため、通常は、同様の対応する名前が提供されます。CoCreateInstance などの関数を呼び出すときに、実際の構造体の代わりにこの名前を使用できます。通例の命名規則では、インターフェイスまたはオブジェクトのわかりやすい名前の先頭に、それぞれ IID_ または CLSID_ を追加することになっています。たとえば、IDirect3D9 インターフェイスの IID の名前は、IID_IDirect3D9 となります。

HRESULT 値

すべての COM メソッドは、HRESULT と呼ばれる 32 ビット整数を返します。ほとんどのメソッドでは、基本的に、HRESULT は 2 つの主要な情報が含まれる構造体です。

  1. メソッドが成功したか失敗したかには関係ありません。
  2. メソッドによってサポートされる処理の結果に関するより詳細な情報です。

メソッドの中には、Winerror.h で定義されている標準セットからしか HRESULT 値を返さないものもあります。しかし、メソッドは、より専門的な情報を持つカスタムの HRESULT 値を自由に返すことができます。これらの値は、通常はメソッドのリファレンス ページに記載されています。

    メソッドのリファレンス ページに記載されている HRESULT 値の一覧は、多くの場合、返される可能性がある値のサブセットのみです。一般的に、一覧には、メソッド固有の値と、メソッド固有の意味を持つ標準の値しか記載されません。ドキュメントに明示的に記載されていない場合でも、メソッドがさまざまな標準の HRESULT 値を返す可能性があることを前提とする必要があります。

HRESULT 値は、エラー情報を返すためによく使用されますが、それらをエラー コードとして考えないようにしてください。実際に、成功や失敗を示すビットは、詳細情報を含むビットとは別々に格納されます。これにより、HRESULT 値に任意の数の成功コードと失敗コードを含めることが可能になります。規則により、成功コードの名前の先頭には S_ が、失敗コードの名前の先頭には E_ が付けられます。たとえば、最もよく使用される 2 つのコードは S_OK と E_FAIL であり、それぞれ単純に成功と失敗を示します。

COM メソッドがさまざまな成功コードや失敗コードを返す可能性があるということは、HRESULT 値のテスト方法には気を付けなければならないということを意味します。たとえば、成功した場合に S_OK、失敗した場合に E_FAIL という戻り値を返すとドキュメントに記載されているメソッドがあると仮定します。そのような場合でも、メソッドがその他の失敗コードや成功コードを返す可能性があることを覚えておいてください。次のコードは、単純なテストを使用する危険性を示しています。 ここでの hrには、メソッドによって返された HRESULT 値が含まれています。

if(hr == E_FAIL)
{
    //Handle the failure
}

else
{
    //Handle the success
}  

メソッドが失敗を示すために E_FAIL しか返さないならば、このテストは正しく機能します。しかし、メソッドは、E_NOTIMPL や E_INVALIDARG などのエラー値を返す可能性もあります。その値は成功として解釈され、おそらく、アプリケーションの失敗を引き起こします。

メソッド呼び出しの結果に関する詳細情報が必要な場合は、関連する各 HRESULT 値をテストする必要があります。しかし、メソッドが成功したかどうかにしか関心がない場合があります。HRESULT 値が成否どちらを示しているかをテストする堅固な方法としては、その値を、Winerror.h で定義されている次のいずれかのマクロに渡す方法があります。

  • SUCCEEDED マクロは、成功コードには TRUE を、失敗コードには FALSE を返します。
  • FAILED マクロは、失敗コードには TRUE を、成功コードには FALSE を返します。

次のコードで示すように、FAILED マクロを使用することによって、上のコードを修正できます。

if(FAILED(hr))
{
   //Handle the failure
}

else
{
   //Handle the success
}

このコードでは、E_NOTIMPL と E_INVALIDARG を失敗として正しく扱います。

ほとんどの COM メソッドは、構造化された HRESULT 値を返しますが、少数のメソッドは、HRESULT を使用して単純な整数を返します。これらのメソッドは、暗黙的に常に成功します。この種の HRESULT を SUCCEEDED マクロに渡す場合は、常に TRUE が返されます。よく使用される例としては、IUnknown::Release メソッドがあります。このメソッドは、オブジェクトのリファレンス カウントを 1 つ減らし、現在のリファレンス カウントを返します。リファレンス カウントの詳細については、「COM オブジェクトの有効期間の管理」を参照してください。

ポインターのアドレス

COM メソッドのリファレンス情報を数ページ参照すると、おそらく、次のようなコードを目にします。

HRESULT CreateDevice(..., IDirect3DDevice9 **ppReturnedDeviceInterface);

通常のポインターは C/C++ 開発者にはお馴染みのものですが、COM では、多くの場合、さらにもう 1 段階の間接参照が使用されます。この 2 段階の間接参照は、型宣言の後の 2 つのアスタリスク (**) で表され、変数名には、通常は pp というプレフィクスが付きます。上の例の場合、 ppReturnedDeviceInterface パラメーターは、通常は、IDirect3DDevice9 インターフェイスへのポインターのアドレスと呼ばれます。

C++ オブジェクトと異なり、COM オブジェクトのメソッドには直接アクセスできません。代わりに、メソッドを公開するインターフェイスへのポインターを取得する必要があります。そのメソッドを呼び出すには、基本的に、C++ メソッドへのポインターを呼び出す場合と同じ構文を使用します。たとえば、IMyInterface::DoSomething メソッドを呼び出すには、次のような構文を使用します。

IMyInterface *pMyIface;
...
pMyIface->DoSomething(...);

2 段階の間接参照の必要性は、直接インターフェイス ポインターを作成しないという事実からもたらされています。さまざまなメソッドからいずれかを呼び出す必要があります (最初の例で示した CreateDevice メソッドなど)。このようなメソッドを使用してインターフェイス ポインターを取得するには、目的のインターフェイスへのポインターとして変数を宣言してから、その変数のアドレスをメソッドに渡します。つまり、ポインターのアドレスをメソッドに渡します。メソッドから返されると、その変数は目的のインターフェイスを指すようになっており、インターフェイスの任意のメソッドを呼び出すためにそのポインターを使用できるようになります。インターフェイス ポインターの使用方法の詳細については、「COM インターフェイスの使用」を参照してください。