2012年10月21日日曜日

COMインターフェースの基礎知識

COMの概要に関するMSの解説ページはここが分かり易そうです。

Direct3Dに限らず、Windowsの多くのライブラリがCOM(Component Object Model)というソフトウェア規格の元に実装されています。

例えばDirect3D11のチュートリアル中に登場する変数の型名で・・・

ID3D11Device
ID3D11DeviceContext
IDXGISwapChain
ID3D11RenderTargetView

の様に頭に「I」が付く型は、COMのインターフェースを表しています。インターフェースは普通のクラス型ではなく、COMという規格に基づいた型なのです。
(C++やJavaなどのオブジェクト指向言語では、抽象クラスに相当する機構をインターフェースと呼んだりしますが、ここではそっちのインターフェースの話はしないのでインターフェース=COMインターフェースと思って下さい。)

ただし、インターフェースと言ってもほとんど普通のクラスの様に扱えます。注意すべき点は、初期化時にnewやコンストラクタを使わない事です。
Direct3Dでは大抵、そのインターフェース専用の初期化用の関数が用意されているので、その関数にインターフェースのポインタを渡します。
メソッド(メンバ関数)へのアクセスは->を使います。

基本的に、派生されたインターフェースが派生元のインターフェースのメソッドを利用できる点もクラスと同じです。

例えばDirect3Dを学ぶ時におそらく真っ先に知る
ID3D11Device
というインターフェースの説明のページに

IUnknown
   ID3D11Device
という記述がありますが、
これはID3D11DeviceがIUnknownから派生している事を示しています。従ってID3D11Deviceというインターフェースを通してIUnknownが持つメソッドも使えます。

実はID3D11Deviceだけでなく、COMのあらゆるインターフェースは、最初の継承元としてIUnknownを持ちます。

では、IUnknownという哲学的な(?)名前のインターフェースにはどの様な役割があるのでしょうか?
Direct3Dを使うに当たって一番重要なのはIUnknownが持つRelease()というメソッドです。これは

インターフェースの解放処理

を行います。つまり、そのインターフェースに使われたオブジェクトやメモリを開放します。通常のクラス型をポインタにnewして構築した場合はdeleteによってオブジェクトのメモリを解放するのですが、COMインターフェースの場合はdeleteせず、替わりにRelease()を呼ぶという分けです。
既に解放済みのインターフェースを再度Release()してはいけないのはdeleteと同じです。


インターフェースの宣言
例としてTutorial01のデバイスの生涯を追ってみましょう。
デバイスは システム中のグラフィック装置を代表するDirect3Dにおいて最も基本的なインターフェースであり、名前はID3D11Deviceです。Tutorial01.cppのソースコードの初めの方に
//------------------------
// Global Variables
//------------------------
ID3D11Device* g_pd3dDevice = NULL;

という風にグローバル変数として宣言されています。
(デバイス以外の変数は省略しました)

グローバル変数として、(つまりあらゆる{}の外で)宣言されているので、このTutorial01.cpp中の、あらゆる場所からこの変数にアクセスする事が出来ます。
まずこれに g_pd3dDevice = NULL;
という風にNULLを入れています。
(NULLは、そこにマウスカーソルを当てれば分かるように
 #define NULL 0
です。)
つまりこの時点では ID3D11Device*型の変数 g_pd3dDevice に0が代入されています。
ご存知だと思いますが、ポインタに0を入れておくのは、それがまだ初期化前の状態だという事を明らかにするためです。C++ではグローバル変数は元々0で初期化されるのですが、=NULLと書いておくとより安心感があるかも知れません。

インターフェースの初期化
デバイスを初期化してインターフェースを利用可能状態にしているのは、ソースコード中腹にあるInitDevice()関数の中です。

InitDevice() の中の真ん中辺りでD3D11CreateDeviceAndSwapChain()という関数を呼んでいます。この関数の引数にg_pd3dDeivceのアドレスを入れる事でデバイスの作成が完了します。(その他にも様々な引数を入れていますが、その辺の解説は別のページでする予定です。)

さて、このデバイスにはDirect3D11関係の他のインターフェースを作成する機能があります。 InitDevice() のさらに下の方に
hr = g_pd3dDevice->CreateRenderTargetView( pBackBuffer, NULL, &g_pRenderTargetView );
という箇所があります。これはデバイスを使ってレンダーターゲットビューというインターフェースを作成してる部分です。ただしここで覚えて欲しいのは、デバイスが持つ具体的な関数の内容ではなく、初期化を終えたインターフェースは->(アロー演算子)を通してメソッドにアクセス出来るという事です。

インターフェースの解体
さて、こうして作成、利用したインターフェースも最後は開放してやらないメモリリークになってしまいます。しかも自分で作ったクラスのnewとdeleteと同じような仕組みではメモリリークを検知できません。
チュートリアルではインターフェースの開放処理はCleanupDevice()という関数の中で指示されています。この関数の中でデバイスは
if( g_pd3dDevice ) g_pd3dDevice->Release();
という風に解放処理が書かれていると思います。
まずif(g_pd3dDevice)によってg_pd3dDeviceに値が入っている (つまり0(NULL)ではない) かどうかをチェックします。宣言時にはNULLを入れていましたが、 D3D11CreateDeviceAndSwapChain()によってうまくインターフェースが作成出来たなら、この時点でNULL以外が入っているでしょうから、ここでRelease()を呼ばれ、インターフェースが解放される事になります。また、解放済みのインターフェースを再度開放しないように、解放後にNULLを入れておくのが良い場合があります。

g_pd3dDevice = NULL;

これらをまとめて行うマクロを書くと、次の様になります。
マクロの定義例
#define SAFE_REL(COMI) if(COMI){COMI->Release(); COMI = NULL;}
マクロの利用例
SAFE_REL(g_pd3dDevice)

マクロを利用すると書くのが楽になるし「三回同じ名前のインターフェース名を書いたつもりが、一部に他のインターフェースの名前が混じり込んでいた」という様なミスを撲滅出来ます。

話がそれますが 、インターフェースではなく普通のポインタ解放用のマクロも用意しておくと便利だと思います。

#define SAFE_DEL(x) if(x){ delete x; x=NULL;}

あと、ベクター配列に確保したインターフェース解放マクロもあると便利かも知れません。
#define SAFE_REL_VEC(COMIVec) for(unsigned int i = 0; i < COMIVec.size(); ++i)\
{ SAFE_REL(COMIVec[i]) COMIVec[i] = NULL; };

 コピーする場合

インターフェースは、ポインタを使って扱う訳ですが、これをコピーして使う場合、どういうことになるのでしょうか?
例えば上の例ではデバイスを作りました。

ID3D11Device* g_pd3dDevice;

そしてこれを D3D11CreateDeviceAndSwapChain()によって初期化しました。
さてここで、このデバイスを他のスコープから利用できるようにしたい場合があるかも知れません。その場合は、もう一つポインタを用意する事になるでしょう。例として新しいデバイスのポインタの名前をDevice2にするとしましょう。

 ID3D11Device* Device2;

この時、元のインターフェースのポインタをDevice2にコピーしてやればDevice2を使っても、同じデバイスインターフェースにアクセスできる様になります。

 Device2 =   g_pd3dDevice;

この様に、同じインターフェースのインスタンスを共同で使いたい場合は、単にポインタをコピーしてアクセス方法を増やすことが出来ます。

ただし、
コピーしたインターフェースを解放する
場合はどうするのでしょうか?

これは、 コピー先かコピー元の一つだけのインターフェースポインタをRelease()します。
普通のオブジェクトのポインタに対するnewとdeleteと話の本質は同じです。上の例ではインターフェース自体は一つしか作らず、ポインタをコピーしてアクセス方法を増やしただけなので、何度もリリースすれば多重解放を引き起こします。

AddRef()の利用
COMのMS公式の解説では、コピーする度にインターフェースをAddRef()する事を推奨しています。その場合はコピーしたインターフェース毎にRelease()をする必要があります。(詳しくはこれから参照カウントの項で説明します。)
AddRef()というのもIUnknownのメソッドです。AddRef()を呼び出す度に内部の参照カウントがカウントアップされます。

参照カウントについて
COMインターフェースは参照カウント用の変数を保持しています。「参照カウント」というのは、そのインターフェースをどれだけのポインタから参照しているかを表す数字です。

例えばデバイスを例にするなら
ID3D11Device* g_pd3dDevice; 
と書いた時点では、まだインターフェースのポインタを宣言しただけなので、参照カウントは0です。
 D3D11CreateDeviceAndSwapChain()を呼んでデバイスインターフェースを初期化した時点で参照カウントが1になります。
そしてもし、
ID3D11Device* Device2 =   g_pd3dDevice;
と書いたら事実上、デバイスへの参照は2つになる訳です。
しかしプログラマーが手動で
Device2->AddRef();
みたいに書いてやらないと、デバイス内の参照カウントは2になりません。(1のままです。)

Release()のメカニズム
実はRelease()を呼び出した時、まずは参照カウントがマイナス1されます。そしてその際、参照カウントが0に至れば実際にインターフェースが解体されます。つまり参照カウントが0にならなければ、他にもそのインターフェースを参照しているポインタがあるはずなので、解体しないというポリシーなのです。

MSが推奨しているのは、コピーする度にAddRef()し、そのコピーが不要になったらRelease()する事で、インターフェースの寿命を管理する事です。
事実上は、コピー時にAddRef()を全くしないで一度だけRelease()を呼んでも同じ事です。
ライブラリに用意されている関数を使って初期化した時だけはAddRef()を呼ばなくても自動的に参照カウントがプラス1されるので、このあと
プログラマがそのインターフェースのポインタをコピーしたとしても、あるいは、しなかったとしてもAddRef()さえしなければ、Release()も一回のままで済みます。

解放忘れのチェック

さて、何やかんや言って、実際に自分がコーディングした時にインターフェース(COMオブジェクト)の解放がちゃんと行えているかどうかは気になると思います。
Direct3D11なら
解放忘れのチェックを行う場合、デバイス作成時に
D3D11CreateDeviceAndSwapChain()の引数の
UINT Flagsの部分でD3D11_CREATE_DEVICE_DEBUGを指定していれば、アプリケーション終了時にインターフェースの解放漏れがあった場合、出力ウインドウに表示されます。
Tutorial01ではDebugビルド時には、この設定になるようにコーディングされています。

 UINT createDeviceFlags = 0;
#ifdef _DEBUG
createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

hr = D3D11CreateDeviceAndSwapChain( NULL, g_driverType, NULL, createDeviceFlags, featureLevels, numFeatureLevels,
D3D11_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice, &g_featureLevel, &g_pImmediateContext );


これでデバッグビルド実行時にインターフェースの解放漏れがあると、上のようにダダダッとメッセージが出力されるでしょう。

Direct3D10の場合は…無理臭いな。
ググっても自分のサイトばっか引っかかってしまう…。

0 件のコメント: