EternalWindows
COM基礎 / インプロセスサーバー

COMにおけるインターフェースと実装の分離は、オブジェクトの詳細をクライアントから隠蔽することに成功しました。 つまり、クライアント内にCMyServerのようなクラスを定義する必要がなくなったわけですが、 これによってある1つの疑問が生じます。 それは、クライアント内でnew CMyServerというコードを実行できない関係上、 クライアントはどのようにしてオブジェクトを作成すればよいかという点です。 この解決策としてCOMでは、オブジェクトをCLSIDというGUIDで識別し、 そのCLSIDをCoCreateInstanceという関数に指定することで、 CLSIDで識別されるオブジェクトを作成できるようにしています。

HRESULT CoCreateInstance(
  REFCLSID rclsid,
  LPUNKNOWN pUnkOuter,
  DWORD dwClsContext,
  REFIID riid,
  LPVOID *ppv
);

rclsidは、作成するオブジェクトのCLSIDを指定します。 pUnkOuterは、集約に使用するインターフェースを指定します。 通常は集約を行わないため、NULLを指定することになります。 dwClsContextは、CLSCTX型で定義される定数を指定します。 オブジェクトをインプロセスサーバーとして扱いたい場合はCLSCTX_INPROC_SERVERを指定し、 アウトプロセスサーバーとして扱いたい場合はCLSCTX_LOCAL_SERVERを指定します。 riidは、オブジェクトの操作に使用するインターフェースのIIDを指定します。 オブジェクトは、このインターフェースを実装している必要があります。 ppvは、作成されたオブジェクトのアドレスを受け取る変数のアドレスを指定します。

CoCreateInstanceを呼び出すとオブジェクトが作成されてそのアドレスが返るわけですが、 これはどのような仕組みで実現されているのでしょうか。 現状では次のようなイメージを持っておいてください。 まず、レジストリから指定されたCLSIDと一致するキーを探し出し、 そのキーからCOMコンポーネントのパスを取得します。 COMコンポーネントとは、オブジェクトを実装するDLLまたはEXEのことであり、 前者をインプロセスサーバー、後者をアウトプロセスサーバーと呼びます。 インプロセスサーバーについて言及すると、パスを取得した後はそのDLLがプロセスのアドレス空間にロードされ、 ある特別の関数を呼び出されることになります。 そして、最終的にDLL自身がオブジェクトを作成し、そのアドレスがCoCreateInstanceの呼び出し側に返る仕組みとなっています。

COMにおける多くの関数やメソッドの戻り値はHRESULT型になっています。 この型はエラーコードを格納するための型であり、 SUCCEEDEDマクロに指定することで成功を意味しているかどうか、 FAILEDマクロに指定することで失敗を意味しているかどうかを確認することができます。 エラーコードの具体的な意味を特定したい場合は、 その値をwinerror.hに定義されている定数と比較することになります。 たとえば、CoCreateInstanceが0x800401F0を返した場合は、 winerror.hからCO_E_NOTINITIALIZEDという定義が発見できますから、 CoInitializeを呼び出していないことがエラーの原因であると分かります。 ちなみに、SUCCEEDEDマクロで成功する値は、原則としてS_OK(0)とS_FALSE(1)のみです。 S_FALSEは関数が失敗したことを意味するのではなく、 関数内で行われた条件式の判定がFALSEであることを意味します。

クライアントは、オブジェクトを操作するスレッドの数だけCoInitializeを呼び出す必要があります。 この関数を呼び出すことにより、スレッドはアパートの中に入り、オブジェクトを操作することが許可されます。

HRESULT CoInitialize(
  LPVOID pvReserved
);

pvReservedは、予約されているためNULLを指定します。 この関数を呼び出すとスレッドはSTA(アパートの一種)に入ることになりますが、 MTA(STAとは別のアパート)に入る場合はCoInitializeExを呼び出すことになります。 アパートの詳細については、別の章で取り上げる予定です。

COMを使用することによって生じた全てのリソース(メモリやDLL、RPC接続など)は、 CoUninitializeで開放することができます。

void CoUninitialize(void);

この関数は、CoInitializeを呼び出した回数だけ呼び出す必要があります。

COMクライアントのサンプルとして、CoCreateInstanceで既存のオブジェクトを作成してみましょう。 今回作成するオブジェクトはFileOpenDialogオブジェクトとし、 このオブジェクトが持つダイアログを表示する機能を使用します。 FileOpenDialogオブジェクトはWindows Vista以降のバージョンで存在し、 shobjidl.hのインクルードが必要です。

#include <windows.h>
#include <shobjidl.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT     hr;
	IFileDialog *pFileDialog;
	
	CoInitialize(NULL);

	hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileDialog));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}
	
	pFileDialog->Show(NULL);
	pFileDialog->Release();

	CoUninitialize();
	
	return 0;
}

FileOpenDialogオブジェクトはIFileDialogで識別することになるため、これを型としたpFileDialogという変数を宣言しています。 スレッドがCoCreateInstanceを呼び出すためには、最初にCoInitializeを呼び出しておく必要があります。 CoCreateInstanceの第1引数はオブジェクトのCLSIDであり、 今回はFileOpenDialogオブジェクトを識別するCLSID_FileOpenDialogを指定しています。 第3引数がCLSCTX_INPROC_SERVERであることから、 関数が成功した場合はこのオブジェクトを実装するDLLがロードされることになります。

CoCreateInstanceの第4引数には取得したいインターフェースのIIDを指定し、 第5引数にはオブジェクトを受け取る変数のアドレスを指定しなければなりません。 しかし、上記のCoCreateInstanceでは第4引数にIIDを指定しておらず、 さらに第5引数について省略しているようにも思えます。 この点について理解するために、まずは従来のCoCreateInstanceの呼び出し方を考察します。

hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_IFileDialog, (void **)&pFileDialog);

上記コードではCoCreateInstanceの引数通り、 第4引数にインターフェースのIIDを指定し、第5引数には変数のアドレスを指定しているわけですが、 実際にはある1つの問題が入る余地を残しています。 それは、第4引数に指定できるIIDが第5引数のインターフェースのIIDと異なっていてもコンパイルが成功するという点です。 たとえば、上記の第5引数はIFileDialog型の変数になっているため、第4引数にはIID_IFileDialogを指定しなければならないわけですが、 間違って別のIIDを指定してしまうこともないとは言い切れないわけです。 よって、このような問題を回避するためにIID_PPV_ARGSというマクロが用意されています。 このマクロは引数に指定された変数から適切なIIDを割り出すため、 IIDが変数の型と矛盾することは完全になくなります。 IID_PPV_ARGSマクロは、割り出したIIDと変数のアドレスをカンマで区切って返すため、 CoCreateInstanceのようなIIDと変数のアドレスが連続した引数の関数には、問題なく指定することができます。 古いSDKのヘッダファールには、IID_PPV_ARGSマクロが定義されていないことに注意してください。

IFileDialog::Showを呼び出すと、ファイルを開く時に使われるダイアログが表示されます。 今回は選択したファイルを使用して何を行っているわけではありませんが、 少なくとも、クライアントがインターフェースを通じてオブジェクトに1つの命令を与えたということは実感できると思います。 インターフェースが不要になった場合は、Releaseを呼び出すことでオブジェクトを破棄します。 COMに関する一切の操作が不要になった場合は、CoUninitializeを呼び出します。


戻る