EternalWindows
COM基礎 / IUnknownの役割

前節では、COMにおけるオブジェクトの正体がC++クラスのインスタンスであり、 それを識別するためにインターフェースが存在することを説明しました。 しかし、COMの「様々なオブジェクトを統一的に扱う」という条件を満たすためには、 このようなインターフェースと実装の分離だけでは不十分です。 なぜなら、オブジェクトが実装した独自のインターフェースというのは、 サーバーのソースファイルやヘッダーファイル上に存在するものであり、 たとえばクライアント上でIFileControlという型の変数を宣言しても、 当然ながらそれは有効な型として認識されません。 こうしたことからオブジェクトは、クライアントが知り得る既存のインターフェースを実装する必要があります。

既存のインターフェースとは、その定義がWindows SDKに含まれるヘッダーファイルに記述されており、 さらにMSDNで調べてインターフェースの詳細を知ることのできるインターフェースのことです。 こうしたインターフェースの中で、あらゆるオブジェクトが実装しなければならないのはIUnknownインターフェースです。 このインターフェースは、あらゆるオブジェクトにとって必要になるメソッドを定義しており、 オブジェクトがCOMのオブジェクトであるためにはこのインターフェースを必ず実装することになります。 次に、IUnknownの定義の擬似コードを示します。

EXTERN_C const IID IID_IUnknown;
IUnknown
{
	virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) = 0;
	virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;
	virtual ULONG STDMETHODCALLTYPE Release(void) = 0;
}

見て分かるように、IUnknownは3つのメソッドを定義しています。 これらのメソッドは全て純粋仮想関数ですから、継承するクラスは全てのメソッドを適切にオーバーライドすることになります。 3つのメソッドの中で特に重要なのは、Releaseというメソッドです。 このメソッドはオブジェクトを破棄するためのメソッドであり、 これ呼び出すことでオブジェクトのために確保されたメモリが開放されます。 作成したオブジェクトの破棄というのはどのようなオブジェクトにも必要になるものですから、 IUnknownにはこのようなメソッドが含まれているわけです。 IIDとは、インターフェースを識別するための識別子のことであり、その実体は一意のGUIDです。 クライアントは、IUnknownを要求する場合などにIID_IUnknownという定義を使用します。

IUnknownを除くすべてのインターフェースは、IUnknownを継承しなければなりません。 つまり、どのようなインターフェースにもIUnknownにおける3つのメソッドは含まれます。 この点を踏まえた場合、Releaseの実行は複数回行われる可能性がありますが、 その実行の度にオブジェクトを無条件に破棄するわけにはいきません。 言い換えれば、現在のオブジェクトを参照するインターフェースが他にも存在する限り、オブジェクトを破棄してはならいのです。 こうしたことからCOMでは、オブジェクトが参照カウントと呼ばれるメンバを持っており、 Releaseが呼ばれた場合はこのカウントを1つ減らします。 そして、0になった場合にオブジェクトが破棄されるようになっています。 参照カウントを増加させるのはAddRefというメソッドですが、 これはインターフェースを取得した際に内部で呼ばれています。

IUnknownのQueryInterfaceは、オブジェクトが指定されたインターフェースを実装するかを確認します。 たとえば、Windows Vista以降ではIPersistFile(用途は後述)を実装しているけれども、 それ以前のバージョンではIPersistFileを実装していないというオブジェクトがあったとします。 この場合、クライアントが無条件にIPersistFileのメソッドを呼び出すコードになっていると、 クライアントを実行した環境がWindows Vista以前である場合にアクセス違反が生じることになります。 理由は、その環境ではオブジェクトがIPersistFileを実装していないからです。 こうしたことから、全てのオブジェクトは自身が実装するインターフェースをクライアントが確認できるようするために、 QueryInterfaceというメソッドを実装する必要があります。

あるオブジェクトが複数のインターフェースを継承しているということは、 そのオブジェクトを複数のインターフェースで識別できることを意味しています。 たとえば、現在使用しているインターフェースに有用なメソッドが含まれていない場合は、 有用なメソッドを持つインターフェースをQueryInterfaceで取得することができます。 つまり、状況に応じて使用するインターフェースを変更できるということです。 次のコードでは、最初はオブジェクトをIUnknownで識別しているわけですが、 ある状況になるとIPersistFileでオブジェクトを識別しています。

IUnknown     *pUnknown;
IPersistFile *pPersistFile;
HRESULT      hr;

// オブジェクトを作成する。成功すると参照カウントが1になる。
CoCreateInstance(..., &pUnknown);

// オブジェクトがIPersistFileを実装しているかを確認。成功すると参照カウントが1つ増えて2になる。
hr = pUnknown->QueryInterface(IID_IPersistFile, (void **)&pPersistFile);
if (FAILED(hr)) {
	pUnknown->Release();
	return 0;
}

// 取得したpPersistFileを使用する。
pPersistFile->Load(...);

// この呼び出しで参照カウントが1になる。
pPersistFile->Release(); 

// この呼び出しで参照カウントが0になり、オブジェクトが破棄される。
pUnknown->Release(); 

まず、CoCreateInstanceを呼び出してオブジェクトを作成し、そのアドレスをIUnknown型の変数で受け取ります。 オブジェクトはIUnknownを必ず実装していますから、この処理には成功するはずです。 通常は、IUnknown型の変数でオブジェクトを識別することはないのですが、 今回はサンプルのため例外とします。 次に、オブジェクトがIPersistFileを実装しているかを調べるために、 IPersistFileのIIDをQueryInterfaceに指定します。 ここで関数が失敗した場合は、オブジェクトがIPersistFileを実装していないことを意味しますから、 クライアントはオブジェクトをIPersistFileで操作することはできません。 よって、後続の処理を実行するわけにはいきませんから、作成したオブジェクトをIUnknown::Releaseで破棄してreturnしています。 QueryInterfaceが成功した場合はIPersistFile::Loadでオブジェクトを操作し、 これを終えた場合はオブジェクトを破棄するためにReleaseを2回実行します。 この2回の理由は、現在オブジェクトがIUnknownとIPersistFileの2つで参照されているからであり、 これに伴ってオブジェクトの参照カウントも2になっているからです。 Releaseを呼び出す順番は、最近取得したインターフェースから行うのが原則です。

IUnknownはオブジェクトが必ず実装すべきインターフェースですが、 必須ではないにしても、実装したほうがよいインターフェースというのは数多く存在します。 たとえば、先ほどから述べているIPersistFileは、 ファイルのロードや保存を行うメソッドを持つインターフェースです。 もし、自分が作成するオブジェクトに、こうしたロードや保存という機能が必要になるのであれば、 IPersistFileは可能な限り実装するべきといえるでしょう。 そうすることで、サーバーは独自のインターフェースを定義する必要がなくなりますし、 クライアントに対しても「IPersistFileを実装しているということは、IPersistFile::Loadでファイルを読み込めるということか」 という前提を持たせることができます。 あるインターフェースの実装が何を意味するかを考えることは、オブジェクトを扱う上でとても重要なことです。

独自のインターフェースを実装しても、そのインターフェースの情報を予めクライアントに公開しておけば、 そのオブジェクトを操作できるのは確かです。 しかし、そうであってもオブジェクトは、可能な限り既存のインターフェースを実装するべきです。 たとえば、クライアントが動的に何らかのオブジェクトのアドレスを取得したとします。 クライアントは、そのオブジェクトがどのようなインターフェースを実装しているか全く分かりませんが、 "試しに"IPersistFileをQueryInterfaceで問い合わせてこれが成功すれば、ある1つの確かな事が言えます。 それは、このオブジェクトがファイルのロードや保存をサポートするという点です。 つまり、自分が知らないオブジェクトを取得したとしても、 既存のインターフェースをひたすら問い合わせていけば、 そのオブジェクトの使い方というものが見えくるようになるのです。 もし、オブジェクトがIFileControlのような独自のインターフェースを実装していたとすれば、 決してクライアントはこのオブジェクトを操作できなかったことでしょう。


戻る