EternalWindows
COMサーバー / クラスオブジェクトの実装

COMオブジェクトを実装するサーバーは、そのオブジェクトをクライアントが取得できるような仕組みを用意しておく必要があります。 クライアントがオブジェクトを取得する際に呼び出す関数はCoCreateInstanceです。 この関数は内部でCoGetClassObjectを呼び出し、指定されたCLSIDから関連するDLLのパスを取得する共に、 そのDLLのDllGetClassObjectを呼び出します。 この関数はいわばクライアントとサーバーが接点を持つための関数であるため、 サーバーはこの関数を必ずエクスポートする必要があります。 これにより、CoGetClassObjectはサーバーが実装するクラスオブジェクトを取得できるようになります。

CoGetClassObjectが取得するクラスオブジェクトというのは、 前節までで作成してきたCMyServerとは異なるものです。 クライアントが必要としているのはCMyServerのアドレスなのですが、 その前にクラスオブジェクトの取得が先に行われることになっているのです。 これは何故かと言うと、オブジェクト(CMyServer)を管理するオブジェクト(クラスオブジェクト)というものが非常に便利な存在だからです。 たとえば、複数のオブジェクトを作成する場合にクラスオブジェクトというものが存在すれば、 オブジェクトの総数を返したり目的のオブジェクトを検索したりすることができるようになります。

先に述べたように、クラスオブジェクトはオブジェクトを管理するオブジェクトですから、 当然ながらオブジェクトを作成する機能を持っていなければなりません。 クラスオブジェクトが独自のインターフェースでこの機能を実装していては、 クラスオブジェクトを取得した側はそれを理解することができませんから、 既存のインターフェースであるIClassFactoryを実装する必要があります。

class CMyServerFactory : public IClassFactory
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	
	STDMETHODIMP CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppvObject);
	STDMETHODIMP LockServer(BOOL fLock);
};

IClassFactoryを継承したCMyServerFactoryというクラスを新たに定義しています。 IClassFactoryはクラスオブジェクトが継承するインターフェースですから、 クライアントが操作するCMyServerに継承させてはいけません。 メソッドについては、QueryInterfaceからReleaseまでがIUnknownのメソッドであり、 CreateInstanceとLockServerがIClassFactoryのメソッドです。 名前から想像ができるようにCreateInstanceがオブジェクトを作成するメソッドであり、 CoGetClassObjectはこのメソッドを呼び出してオブジェクトを作成します。

STDMETHODIMP CMyServerFactory::CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppvObject)
{
	CMyServer *p;
	HRESULT   hr;

	*ppvObject = NULL;

	if (pUnkOuter != NULL)
		return CLASS_E_NOAGGREGATION;

	p = new CMyServer();
	if (p == NULL)
		return E_OUTOFMEMORY;

	hr = p->QueryInterface(riid, ppvObject);
	p->Release();

	return hr;
}

pUnkOuterは、クライアントが集約と呼ばれるメカニズムを使用する場合にNULLでない値が格納されます。 集約をサポートするつもりがない場合は、CLASS_E_NOAGGREGATIONを返して問題ありません。 pUnkOuterの確認が終われば、次はCreateInstanceの役割通りオブジェクトを作成します。 オブジェクトはC++のクラスにおけるインスタンスですから、newで作成すればよいでしょう。 そして最後に、オブジェクトがriidをサポートするかを確認することになりますが、 これに失敗した場合はオブジェクトが作成されたままになってしまいます。 よって、Releaseを呼び出すことによって、参照カウントが1の場合はオブジェクトを破棄できるようにしておきます。 QueryInterfaceが成功した場合は参照カウントが2になるため、 Releaseによってオブジェクトが破棄されることはありません。

CoGetClassObjectはクラスオブジェクトのCreateInstanceを呼び出すわけですが、 このクラスオブジェクトはどのようにして取得することになるのでしょうか。 これは冒頭で述べたように、サーバーが実装しているDllGetClassObjectを呼び出して取得します。

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID *ppv)
{
	static CMyServerFactory serverFactory;
	HRESULT hr;

	*ppv = NULL;
	
	if (IsEqualCLSID(rclsid, CLSID_MyServer))
		hr = serverFactory.QueryInterface(riid, ppv);
	else
		hr = CLASS_E_CLASSNOTAVAILABLE;

	return hr;
}

serverFactoryという変数の型はCMyServerFactoryになっています。 このクラスはIClassFactoryを継承していましたから、serverFactoryがクラスオブジェクトということになります。 クラスオブジェクトは、サーバーのDLLがロードされている間は常に存在すべきであるため、 動的に作成するのではなく静的変数として宣言しています。 クラスオブジェクトのアドレスを返す条件は、指定されたCLSIDのオブジェクト(CMyServer)を自身が作成できるかどうかで決定するため、 rcrclsidの値がオブジェクトのCLSIDと一致するかを確認します。 一致する場合は、クラスオブジェクトがriidで識別されるインターフェースを実装するかを確認するために、 クラスオブジェクトのQueryInterfaceを呼び出します。 一致しない場合は、CLASS_E_CLASSNOTAVAILABLEを返します。 ちなみに、CLSIDで識別されるオブジェクトはCOMクラス、またはcoclassと呼ばれることがあります。 今回の場合、CMyServerがCOMクラスということになります。

サーバーがエクスポートする関数には、基本的にSTDAPIという定義を使用します。 これは次のように定義されています。

EXTERN_C HRESULT STDAPICALLTYPE

この定義から分かるように関数の戻り値はHRESULTであり、呼び出し規約は__stdcallになります。 EXTERN_Cはextern "C"のことであり、関数の名前装飾を防止します。

IClassFactoryが定義するメソッドには、CreateInstanceの他にLockServerがあります。 このメソッドはサーバーをロックするためのもので、クライアントはサーバーのDLLをアンロードさせたくない場合に呼び出します。

STDMETHODIMP CMyServerFactory::LockServer(BOOL fLock)
{
	LockModule(fLock);

	return S_OK;
}

fLockはサーバーをロックする場合にTRUEが格納され、ロックを解除する場合にFALSEが格納されます。 戻り値は常にS_OKで構いません。 自作関数のLockModuleの実装は、次のようになっています。

void LockModule(BOOL bLock)
{
	if (bLock)
		InterlockedIncrement(&g_lLocks);
	else
		InterlockedDecrement(&g_lLocks);
}

g_lLocksは現在のロック数を表すグローバル変数です。 bLockがTRUEの場合はInterlockedIncrementで1つ増加し、 bLockがFALSEの場合はInterlockedDecrementで1つ減少します。 g_lLocksの値を基にDLLのアンロードを行うかを決定するのは、DllCanUnloadNowです。

STDAPI DllCanUnloadNow()
{
	return g_lLocks == 0 ? S_OK : S_FALSE;
}

この関数は、CoFreeUnusedLibrariesを呼び出した際に呼ばれます。 CoFreeUnusedLibrariesを呼び出すということは、サーバーのDLLがアンロードされることを望んでいるため、 ロック数が0である場合はS_OKを返してアンロードを許可します。 逆にロック数が0でない場合は、サーバーはまだロックされているということですから、 アンロードを許可しないことを示すためにS_FALSEを返します。

クライアントが明示的にLockServerを呼び出さなくても、ロック数をカウントすべき時は存在します。 たとえば、クライアントがCoCreateInstanceではなくCoGetClassObjectを呼び出した場合、 クライアントはクラスオブジェクトを操作できるようになりますから、 このような状態の際にサーバーのDLLがアンロードされることがあってはいけません。 つまり、クライアントがサーバーの何らかのオブジェクトを参照している場合は、 自動でサーバーをロックするような仕組みが必要になります。 オブジェクトの参照に関わるメソッドはAddRefとReleaseであるため、 これらを適切に実装します。

STDMETHODIMP_(ULONG) CMyServerFactory::AddRef()
{
	LockModule(TRUE);

	return 2;
}

STDMETHODIMP_(ULONG) CMyServerFactory::Release()
{
	LockModule(FALSE);

	return 1;
}

AddRefでロック数を1つ増やし、Releaseで1つ減らします。 このようにすれば、オブジェクトが参照されている間はDLLがアンロードされないことが保証されます。 AddRefとReleaseの戻り値は参照カウントですが、 クラスオブジェクトは常に存在するもので参照カウントというものは存在しません。 よって、0でない値を返すことで現在存在していることを伝えるようにします。 一般的にはAddRefで2を返し、Releaseで1を返すことが多いようです。

クラスオブジェクトのReleaseはクラスオブジェクトが不要になった時点で呼ばれるとして、 AddRefはどのようなタイミングで呼ばれることになるのでしょうか。 答えは、QueryInterfaceを呼び出してオブジェクトのアドレスを取得した時です。

STDMETHODIMP CMyServerFactory::QueryInterface(REFIID riid, void **ppvObject)
{
	*ppvObject = NULL;

	if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IClassFactory))
		*ppvObject = static_cast<IClassFactory *>(this);
	else
		return E_NOINTERFACE;

	AddRef();
	
	return S_OK;
}

QueryInterfaceの目的は、クラスオブジェクトがriidで示されるインターフェースを実装するかを調べ、 実装する場合はオブジェクトのアドレスを返すことです。 クラスオブジェクトはIUnknownとIClassFactoryを実装しているため、 これらのIIDとriidが一致する場合はppvObjectにクラスオブジェクトのアドレスを格納します。 これにより、クライアントはクラスオブジェクトを参照できるようになるためAddRefを呼び出します。

CoCreateInstanceとCoGetClassObject

CoCreateInstanceやCoGetClassObjectの内部コードに考察すると、COMについての知識がより深まります。 CoCreateInstanceは内部でCoGetClassObjectを呼び出すことから、 次のような実装になっているのではないかと推測できます。

HRESULT CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID *ppv)
{
	IClassFactory *pClassFactory;
	HRESULT       hr;
	
	hr = CoGetClassObject(rclsid, dwClsContext, NULL, IID_PPV_ARGS(&pClassFactory));
	if (SUCCEEDED(hr)) {
		hr = pClassFactory->CreateInstance(pUnkOuter, riid, ppv);
		pClassFactory->Release();
	}

	return hr;
}

CoGetClassObjectを呼び出すと第5引数からクラスオブジェクトのアドレスを取得できます。 クラスオブジェクトはIClassFactoryを実装しているはずですから、 IClassFactory型のアドレスを指定しても問題はありません。 関数が成功したら、IClassFactory::CreateInstanceを呼び出すことでオブジェクトを作成します。 これが成功した場合は、ppvにオブジェクトのアドレスが格納されますから、 CoCreateInstanceの呼び出し側はオブジェクトのアドレスを取得できたことになります。

CoGetClassObjectは、COMサーバーがエクスポートしているDllGetClassObjectを呼び出しますから、 次のような実装であると考えることができます。

HRESULT CoGetClassObject(REFCLSID rclsid, DWORD dwClsContext, COSERVERINFO pServerInfo, REFIID riid, LPVOID *ppv)
{
	HRESULT hr = REGDB_E_CLASSNOTREG;
	
	if (dwClsContext & CLSCTX_INPROC_SERVER) { // 要求がインプロセスサーバーであるかを確認。

		// レジストリからrclsidと一致するキーを探し、DLLのパスを取得。

		hmod = LoadLibrary(szFilePath); // そのDLLをロードする。

		lpfn = GetProcAddress(hmod, "DllGetClassObject"); // DllGetClassObjectのアドレスを取得する。

		hr = lpfn(rclsid, riid, ppv); // DllGetClassObjectを呼び出す。
	}

	return hr;
}

実際には、既にDLLがロードされているかを確認する処理などがあるかもしれませんが、 イメージとしては上記のコードで十分と思われます。 CLSIDがレジストリの何処に書き込まれているかについては、次節で取り上げます。



戻る