COM | ゲームプログラマ志望が福岡で叫ぶ 『絶望』

ゲームプログラマ志望が福岡で叫ぶ 『絶望』

プログラマーになりたい!!!!! あ、風のうわさで聞いた最近若者で流行っているトゥイッターなるものを始めてみました (・ト・) @toshi_desu_yo

COM オブジェクトとは

コンポーネント オブジェクト モデル ( COM ) オブジェクト。
ダイナミックリンクライブラリ ( DLL ) として実装するのが最も一般出来。

COMオブジェクトの処理は、公開している関数の呼び出しで実行される。

COMオブジェクトと C++ のオブジェクトのやり取りは似ている。


しかし、顕著な相違点がある。

・COMオブジェクトはC++オブジェクトよりもカプセル化の方法が厳密。
 特定の公開館数を利用したい場合、オブジェクトを生成して、その関数を保持しているインターフェイスを取得しなければならない。
・COMオブジェクトとC++オブジェクトの作成方法。
 COM固有の方法でCOMオブジェクトは作成される。
 DirectXAPIには様々な生成を容易にする関数が用意されている。
・オブジェクトの寿命はCOM特有の技法を用いる
・COMオブジェクトはDLLに含まれているが、COMを生成するに当たって自動的にDLLがロードされるので静的ライブラリのリンクをする必要がない。
・COMはバイナリなので様々な言語で記述でき、様々な言語でアクセスできる。


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

オブジェクトとインターフェイスの違い。

・オブジェクトはインターフェイスを1つ以上継承していることがある。
 例えば全てのオブジェクトは IUnknown インターフェイスを継承している。
 そのインターフェイスの関数を使いたい場合、オブジェクトの作成だけではなく
 正しいインターフェイスを取得する必要がある。



※注意
 インターフェイスを継承したオブジェクトはそのインターフェイス定義に含まれる全ての関数をサポートしなければならない。
 言い換えれば関数を呼び出すとき、それが存在するかを心配する必要はない。
 ただし、各メソッドの実装方法はオブジェクトによって異なる場合がある。
 結果は同じでもオブジェクトにより使用するアルゴリズムが異なるということ。
 そして、メソッドすべての機能がサポートされているというわけでもない。
 サポートされていないメソッドも正常に呼び出すことはできるが、 E_NOTIMAPL が返される。
 書くオブジェクトのドキュメントを参照して書くオブジェクトでのインターフェイスの実装状況を確認することを勧める。
 COM標準では、既存のインターフェイスに新しいメソッドを追加できない。
 変更するのではなく、新しいインターフェイスを作成する必要がある。
 
 1つのインターフェイスに複数の世代が存在する。
 多くの場合、オブジェクトはすべての世代のインターフェイスを公開する。
 古いアプリケーションでは古いインターフェイスを使い、新しいアプリケーションでは新しいインターフェイスの機能を利用することができる。

 初代が IMyInterface ならば 第二世代は IMyInterface2, 第三世代は IMyInterface3
 と、世代を示す整数が付加される。
 
 DirectXでは頭にDirectXのバージョン番号が振られている。


【GUID】
 グローバル一意識別子。
 128bitの構造体で同じGUIDは存在しない。
 
 COMでは次の2つの目的のため、GUIDを広く使う。
 
 ・特定のCOMオブジェクトを識別するため。
  COMオブジェクトに割り当てられたGUIDをクラス識別子 CLSID と呼ぶ。
  CLSIDは、そのCOMオブジェクトのインスタンスを作成する時に使われる。
 ・特定のCOMインターフェイスを識別するため。
  特定のCOMインターフェイスに割り当てられたGUIDをインターフェイス識別子 IID と呼ぶ。
  IIDはオブジェクトから特定のインターフェイスを要求する時に使用される。
  公開しているオブジェクトが異なっても、同じおんた~フェイスであれば IIDも同じ。

※オブジェクトやインターフェイスを呼び出すときは わかりやすいように IDirect3D9 などの説明的な名前が使用される。
 このため、混同される心配はないが、厳密には同じ説明的名前を持つオブジェクト、インターフェイスが存在しないという保証はない。
 特定のオブジェクトまたはインターフェイスを間違いなく表すことの出来る唯一の方法は GUID によるものである。
 GUID は typedef で等価な文字列で表現されていることもある。
 GUIDは 32進数の整数による 8-4-4-4-12 形式 { xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx } となる。( xは16進数の整数に対応する )
 例えばIDirect3D9 インターフェイスのIIDを文字列形式で表すと次のようになる。
  { 1DD9E8DA-1C77-4D40-B0CF-98FEFDFF9512 }

実際のGUITは使いづらく誤入力しやすいので通常は GUID と共に等価な名前も用意される。
CoCreateInstanceなどの関数を呼び出すときに 上記の形式ではなく、等価な文字列が使用出来る。
インターフェイス( IID ) → IID_xxxx
オブジェクト( CLSID ) → XLSID_xxxx

が付けられる。
例えば IDirect3D9 インターフェイスの IID の名前は IID_IDirect3D8 となる。


【 HRESULT値 】

全ての COMメソッドは HRESULTと呼ばれる 32ビットの整数を返す。

・メソッドの成否
・メソッドの結果に関する詳細情報

この2つの情報が含まれている。

HRESULTは様々な成否を表すコードをいくつでも含めることが出来る。
慣習により、成功コードには S_xxx というプレフィックス、失敗には E_xxx というプレフィックスが付けられる。
単に成功、失敗を表すには S_OK, E_FAIL を使用する。


COMメソッドが成否を示す様々なコードを返すということは、 HERSULT値のテスト方法に注意が必要であることを意味する。
成功の場合は S_OK, 失敗の場合は E_FAIL が返されるという仮説に基づいてテストを行ったとする。 しかし、メソッドからはこれ以外の失敗または成功を表すコードが返される可能性もある。
次のコードは単純なテストを用いた場合の危険性を表す

HRESULT hr;

if ( hr == E_FAIL )
{
    // 失敗
}
else 
{
    // 成功
}

メソッドが失敗を示すために返す値が E_FAIL だけである限り、このテストは正しく機能する。
しかし E_NOTIMPL や E_INVALIDARG などのエラー値が返される可能性もある。
その場合、これらは成功として解釈され、アプリケーションでエラーが発生する原因となる。

関係のある各 HRESULT 値をテストする必要がある。
しかし、成功か失敗かだけを知りたい場合は

・ SUCCEEDED → 成功を示すコードに対しては TRUE, 失敗は FALSE
・ FAILED → 失敗を示すコードに対しては TRUE, 失敗は FALSE

マクロを使用すると良い。


FAIELD マクロを使用すると上記のコードは

if ( FAILED( hr ) )
{
    // 失敗
}
else
{
    // 成功
}

となる。
これだと E_NOTIMPLや R_INBALIDARG が返されてきても正しく処理する。



【ポインタのアドレス】

COMメソッドでは

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

のようなダブルポインタの表記がある。
C, C++ で使われる通常のポインタとは違う。

この間接的呼び出しは ** で示され、変数名に pp というプレフィックスが付く。

前述の ppRTeturnedDeviceInterface パラメータは 一般的に IDirect3DDevice9 インターフェイスのポインタのアドレスと呼ばれる。


インターフェイスのアドレスのアドレスをメソッドに渡す。
メソッド内でアドレスに対してオブジェクトのメモリをとるとメソッドが終わったとき要求されたインターフェイスを指した変数が完成する。




【 COMオブジェクトの作成 】

COMオブジェクトを作成する方法はいくつか存在する。
DirectXプログラミングでは次の2つの方法が最も一般的である。

・直接的な方法。
 オブジェクトのクラス識別子( CLSID ) を CoCreateInstance 関数に渡す。
 この関数は、識別子からオブジェクトのインスタンスを作成し、指定されたインターフェイスへのポインタを返す。
・間接的な方法。
 そのオブジェクトを作成する DirectXメソッドまたは関数を呼び出す。
 呼び出したメソッドがオブジェクトを作成し、そのオブジェクトのインターフェイスを返す。
 この方法でオブジェクトを作成する場合、通常は、返るインターフェイスを指定できないない。


オブジェクトを作成する際は CoInitialize 関数を呼び出してあらかじめ COM を初期化しておく必要がある。
間接的にオブジェクトを作成する場合は、オブジェクト作成メソッド内部でこの処理が行われている。
CoCrateInstance を使ってオブジェクトを作成する場合は、明示的に CoInitialize を呼び出さなければならない。
操作を完了したら CoUninitialize を呼び出して終了しなければならない。 
COMを明示的に初期化する必要があるアプリケーションでは、初期化の最初で COMを初期化し、終了時に状態を戻すのが一般的である。

CoCreateInstance でCOMオブジェクトの新しいインスタンスを作成するには、オブジェクトのCLSIDが必要。
公開されている CLSID はリファレンスドキュメントまたは該当するヘッダーファイルに記述されている。
CLSIDが公開されていない場合は、そのオブジェクトを直接作成することはできない。

CoCreateInstance 関数には5つのパラメータがある。
DirectXで使うCOMオブジェクトを作成する場合は、通常、次のパラメータを設定する。


・rclsid → 作成するオブジェクトの CLSID 
・pUnkOuter → NULL設定。 このパラメータはオブジェクトを結合するときに使用する
・dwClsContext → CLSCTX_INPROV_SERVER に設定する。
 この設定はオブジェクトがダイナミックリンクライブラリ( DLL )として実行され、アプリケーションの一部として実行されることを示す。
・riid → 要求するインターフェイスのインターフェイス識別子( IID ) に設定する。
 この関数はオブジェクトを作成して、要求されたインターフェイスへのポインタを ppv パラメータに返す。
・ppv → 関数が戻った時に riid が指定するインターフェイスに設定されるポインタのアドレス。
 



通常はオブジェクトを間接的に作成するほうが格段に簡単。
オブジェクト作成関数にインターフェイスポインタのアドレスを渡すだけだから。
後は関数内部がすべて行ってくれる。
通常、間接的な方法でオブジェクトを作成する場合は、メソッドが返すインターフェイスを選択できない。
ただし、オブジェクトの作成補法について様々な事項を指定できる。

たとえば、次のコードは CreateDevice メソッドを呼び出して、ディスプレイアダプタを表すデバイスオブジェクトを作成している。
このメソッドはオブジェクトの IDirect3DDevice9 インターフェイスへのポインタを返す。
最初の4つのパラメータはオブジェクトを作成するために必要な各種の情報を指定し、5番目のパラ目^他はインターフェイスポインタを受け取る。

IDirect3DDevice9* g_pd3dDevice = NULL;
...
if ( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT,
                             3DDEVTYPE_HAL,
                             hWnd,
                             D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                             &d3dpp,
                             &g_pd3dDevice ) ) )
    return E_FAIL;




【COMインターフェイスの使い方】
COMオブジェクトを作成すると、作成メソッドがインターフェイスポインタを返す。
このポインタを使って、そのインターフェイスの任意のメソッドにアクセスできる。


●追加のインターフェイスの要求

通常は、作成メソッドから受け取るインターフェイスポインタだけで用は足りる。
実際、オブジェクトが継承するインターフェイスが IUnknown 以外には1つだけであることも多い。
しかし、多くのオブジェクトは複数のインターフェイスを継承する。
このため、複数のインターフェイスへのポインタが必要になる場合もある。
その場合、新しいオブジェクトを作成する必要はない。
オブジェクトの IUnknown::QueryInterface メソッドを使って別のインターフェイスポインタを要求できる。

CoCreateInstance でオブジェクトを作成した場合は、IUnknown インターフェイスポインタを要求した後、 QueryInterface を呼び脱ことによって、必要なすべてのインターフェイスを要求できる。
ただし、この方法は、必要なインターフェイスが1つだけの場合は不便である。
また返すインターフェイスポインタを指定できないオブジェクト作成メソッドを使う場合は、全く役に立たない。
すべての COM インターフェイスは IUnknown インターフェイスを拡張するので、通常は明示的な IUnknown ポインタを取得する必要はない。

インターフェイスの拡張は、C++ のクラスの継承に似ている。
子インターフェイスは親インターフェイスのすべてのメソッドと子独自のメソッドを1つ以上公開する。
実際「拡張」の代わりに「継承」という表現を使うことも多い。
ただし、継承はオブジェクト内部で行われるものである。
アプリケーションがオブジェクトのインターフェイスを継承または拡張することはできない。
しかし、子インターフェイスを使うと、子または親インターフェイスの任意のメソッドを呼び出せる。

すべてのインターフェイスは IUnknown の子である。
このため、オブジェクトに対して取得したどのインターフェイスポインタを使っても QueryInterface を呼び出せる。
その場合は、要求するインターフェイスのインターフェイス識別子( IID )とインターフェイスポインタのアドレスを指定する必要がある。


【 COMオブジェクトの有効期限の管理 】
オブジェクトが作成されると、システムは必要なメモリリソースを割り当てる。
必要がなくなったオブジェクトは破棄する必要がある。
C++ では new, delete の各演算子でオブジェクトの有効期限を直接制御できる。
COMでは、オブジェクトを直接作成、または破棄できない。
これは同じオブジェクトを複数のアプリケーションが使っている可能性があるからである。
このようなオブジェクトを1つのアプリケーションが破棄すると、ほかのアプリケーションでは高い確率でエラーが発生する。
COMでは 参照カウント と呼ばれるシステムを使ってオブジェクトの有効期限を制御している。

オブジェクトの参照カウントは、そのオブジェクトのいずれかのインターフェイスが要求された回数を示す。
インターフェイスが要求されるたびに、参照カウントはインクリメントされる。
必要のなくなったインターフェイスをアプリケーションが解放すると、参照カウントはデクリメントされる。
オブジェクトは参照カウントが0にならない限り、メモリ上に保持される。
0になるとオブジェクトは破棄される。
オブジェクトの参照カウントについて知る必要はない。
オブジェクトのインターフェイスを正しく取得して解放する限り、オブジェクトの有効期限は適切に制御される。


”重要”
COMプログラミングでは、参照カウントを正しく処理することが非常に重要である。
この処理を正しく行わないと、メモリリークが生じやすくなる。
COMプログラマが最も犯しやすいミスの一つがインターフェイスの解放し忘れである。
インターフェイスが解放されないと、参照家運がいつまでも0にならず、オブジェクトはメモリに残り続ける。


●参照カウントのインクリメントとデクリメント

新しいインターフェイスポインタを取得するたびに、 IUnknown::AddRef を呼び出して参照カウントをインクリメントする必要がある。
ただし、通常アプリケーションでこのメソッドを呼び出す必要はない。
オブジェクト作成メソッドまたは IUnknown::QueryInterface を呼び出してインターフェイスポインタを取得した場合、オブジェクトは自動的に参照カウントをインクリメントする。
しかし、既存のポインタをコピーするなど、その他の方法でインターフェイスポインタを作成した場合は、 IUnknown::AddRef を明示的に呼び出す必要がある。
これを行わないと、元のインターフェイスポインt名が解放した時に、そのポインタのコピーを継続して使う必要があっても、オブジェクトが破棄されることがある。

インターフェイスポインタが不要になった場合は IUnknown::Release を呼び出して参照カウントをデクリメントする。
インターフェイスポインタはNULLで初期化し、解放時に再び NULL に設定するので、 NULLかどうかでインターフェイスのアクティブを検証することができる。
アプリケーションを終了する前に解放する必要がある。



【 CによるCOMオブジェクトへのアクセス 】

COMプログラミングで最も一般的に使われている言語は C++ であるが、 C を使っても COMオブジェクトにアクセスできる。
COMを使う方法は、操作は比較的わかりやすいが、複雑な構文が必要になる。

・すべてのメソッドでパラメータリストの最初にパラメータが1つ追加される。
 このパラメータはインターフェイスポインタに設定する必要がある。
・オブジェクトの vtable を明示的に参照する必要がある。

すべてのCOMオブジェクトにはそれぞれ vtable があり、この中には、そのオブジェクトが公開するメソッドへのポインタのリストが含まれている。
インターフェイスポインタは vtable 内の場所を示している。
そこには、呼び出されたメソッドへのポインタが格納されている。
C++ では、 vtable は基本的に不可視である。
ただし、CでCOMメソッドを呼び出す場合は、間接呼び出しのレベルを1つ追加して、vtable を明示的に参照する必要がある。

次のコードは C++ での呼び出し規則を使って IDirectplay8Peer::Initialize メソッドを呼び出す方法を示している。


g_pDP->Initialize( NULL, DirectPlayMessageHandler, 0 );


C で同じメソッドを呼び出すには、次の構文を使用する。
vtable ポインタの名前は lpVtbl と表記される。


g_pDP->lpVtbl->Initialize( g_pDP, NULLm DirectPlayMessageHandler, 0 );



【 マクロによるDirectX COMメソッドの呼び出し 】

COMインターフェイスの多くには、アプリケーションでより簡単にメソッドを使えるように、
各メソッド用に定義されたマクロが用意されている。
これらのマクロは、インターフェイスの宣言と同じヘッダーファイルで定義されている。
マクロは、 C, C++ の両方のアプリケーションで使えるように設計されている。
C++ マクロを使うには _cplusplus を定義する必要がある。
これを定義しない場合は、 Cマクロが使われる。
マクロの構文はどちらの言語でも同じであるが、ヘッダーファイルに含まれる一連のマクロ定義は異なり、それぞれの適切な呼び出し表記に合わせて拡張されている。

たとえば、d3d.h ヘッダーファイル内の次のコードでは、 IDirect3D9::GetAdapterIdentifier メソッドに対する C と C++ 両方のマクロが定義されている。


...
#define IDirect3D9_GetAdapterIdentifier( p, a, b, c ) ( p )->lpVtbl->GetAdapterIdentifier( p, a, b, c )
...
#else
...
#define IDirect3D9_GetAdapterIdentifier( p, a, b, c ) ( p )->GetAdapterIdentifier( a, b, c )
...
#endif


これらのマクロのいずれかを使うには まず、関連図けられているインターフェイスへのポインタを取得する必要がある。
マクロの最初のパラメータに、このポインタを設定する。
その他のパラメータは、メソッドのパラメータにマップされる。
マクロの戻り値は、メソッドが返す HRESULT 値である。
次のコードはマクロを使って IDirect3D9::GetAdapterIdentifier メソッドを呼び出している。
ここで、 pD3D は IDirect3D9 インターフェイスへのポインタを表す。



hr = IDirect3D9_GetAdapterIdentifier( pD3D, Adapter, dwFlags, pIdentifier );



【 DirectX 9.0 COMインターフェイスでの ATLの使用 】

Active Template Library ( ATL ) を使うためには、 ATL との互換性のためにインターフェイスを再定義する必要がある。
これにより CComQIPtr クラスを正しく使って、インターフェイスへのポインタを取得できる。


Attachメソッドを使用して ::Direct3DCreate9が返すインターフェイスポインタにインターフェイスをアタッチする必要がある。
こうしないと、 IDirect3D9インターフェイスはスマートポインタクラスによって正しく開放されない。


オブジェクトが生成された時と CComPtr クラスにインターフェイスが割り当てられたときに、 CComPtr クラスはインターフェイスポインタで内部的に IUnknown::AddRef を呼び出す。
インターフェイスポインタのリークを防ぐため ::Direct3DCreate9 が返すインターフェイスで IUnknown::AddRef を呼び出してはならない。

次のコードは IUnknown::AddRef を呼び出さずにインターフェイスを正しく開放する

CComPtr< IDirect3D9 > d3d;
d3d,Attach( ::Direct3DCrate9( D3D_SDK_VERSION ) );



上のコードを使用する。次のコードは使ってはならない。

CComPtr< IDirect3D9 > d3d = ::DIrect3DCreate9( D3D_SDK_VERSION );




【 IUnknown インターフェイス 】

すべての COMオブジェクトは IUnknown というインターフェイスをサポートする。
このインターフェイスは、オブジェクトの有効期間を制御する機能と、オブジェクトが実装するほかのインターフェイスを取得する機能を提供する。

IUnknown は次の3つのメソッドを持つ。

AddRef → インターフェイスの参照カウントを1ずつ増やす
QueryInterface → オブジェクトが特定の COM インターフェイスをサポートしているかどうかを判別する。
 インターフェイスをサポートしている場合、システムはオブジェクトの参照カウントを増やす。
 アプリケーションは、そのインターフェイスをすぐに使える。
Release → インターフェイスの参照カウントを1ずつ減らす。


AddRef と Release はオブジェクトの参照カウントを管理する。
たとえば、 オブジェクトを作成すると、そのオブジェクトの参照カウントは1に設定される。
関数は、そのオブジェクトのインターフェイスへのポインタを返すたびに、そのポインタを返すたびに、そのポインタを使って AddRef を呼び出し、参照カウントをインクリメントしなければならない。
また、 AddRef を呼び出した場合は、Releaseを呼び出さなければならない。
ポインタを破棄する前にはそのポインタを使って、 Release を呼び出さなければならない。
オブジェクトの参照カウントが0になると、オブジェクトは自動的に破棄される。

QueryInterface メソッドは、オブジェクトが特定のインターフェイスをサポートしているかどうかを調べる。
 目的のインターフェイスがサポートされている場合、そのインターフェイスのポインタが返る。

次に、そのインターフェイスのメソッドを使って、オブジェクトとやり取りができるようになる。
インターフェイスへのポインタが正常に帰った場合、 QueryInterface は非明示的に AddRef を呼び出して、参照カウントをインクリメントする。
このため、インターフェイスへのポインタを破棄する前にアプリケーションは Release を呼び出して、参照カウントをデクリメントしなければならない。



これも飽きた