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

COM基礎の章では、オブジェクトを操作するクライアント側の視点からCOMを説明しましたが、 今回はCOMサーバーの作成を通じてCOMの基礎を説明します。 COMサーバーを作成するというのは、特定のCLSIDを持つオブジェクトをクラスとして定義し、 そのCLSIDを指定してCoCreateInstanceを呼び出せるようにするということです。 つまり、自分が作成したオブジェクトを誰かに使用してもらうことを意味します。 COMサーバーの種類には、DLLによるインプロセスサーバーとEXEによるアウトプロセスサーバーがありますが、 今回はインプロセスサーバーとして作成することにします。

COMサーバーを作成するためには、何よりもまずオブジェクト(COMオブジェクト)を実装する必要があります。 オブジェクトとは、インターフェースを継承したC++クラスのことであり、 クライアントはこのインターフェースを通じてオブジェクトを操作します。 クラスとインターフェースが分離していれば、 クライアントはオブジェクトの詳細(クラスのメンバなど)を知らなくても操作できることになりますから、 このような仕組みになっているのです。 作成したC++クラスがCOMオブジェクトとして認識されるためには、 COMの規則通りIUnknownを継承することになります。

class CMyServer : public IUnknown
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();

	CMyServer();
	~CMyServer();

private:
	LONG m_cRef;
};

QueryInterfaceとAddRef、そしてReleaseはIUnknownによって定義されているメソッドであり、 これらは純粋仮想関数として定義されています。 よって、IUnknownを実装するクラスはこれらを適切にオーバーライドする必要があります。 STDMETHODIMPは、次のように定義されています。

#define STDMETHODIMP            HRESULT STDMETHODCALLTYPE
#define STDMETHODIMP_(type)     type STDMETHODCALLTYPE

既存のインターフェースのメソッドは基本的に戻り値の型がHRESULTであるため、STDMETHODIMPという定義を使用します。 しかし、一部のメソッド(AddRefやRelease)は戻り値がHRESULTではないため、 このような場合はSTDMETHODIMP_(type)という定義を使用します。 この場合、typeに指定した値がメソッドの戻り値になります。 STDMETHODCALLTYPEは、__stdcallという呼び出し規約です。

オブジェクトがIUnknownを実装しなければならない理由は主に2あります。 1つは、クライアントが不要になったオブジェクトを開放できるようにするためであり、 これによりオブジェクトがいつまでもメモリに残るようなことを防ぐことができます。 このためには、サーバーはオブジェクトの寿命というものを管理することになるため、 一般に参照カウントと呼ばれるメンバを用意することになります。 今回の場合これはm_cRefであり、AddRefとReleaseによって増減することになります。

STDMETHODIMP_(ULONG) CMyServer::AddRef()
{
	return m_cRef++;
}

STDMETHODIMP_(ULONG) CMyServer::Release()
{
	if (--m_cRef == 0) {
		delete this;
		return  0;
	}

	return m_cRef;
}

参照カウントにおける大原則は、0より大きい場合にオブジェクトは存在し、0の場合はオブジェクトを破棄するという点です。 AddRefはオブジェクトの参照カウントを上げるためのメソッドであるため、単純に参照カウントをインクリメントするだけで構いません。 一方、Releaseはオブジェクトの参照カウントを下げると同時に、 参照カウントが0になったオブジェクトを破棄する役割があります。 よって、参照カウントが0になった場合は、deleteで自分自身を破棄しています。 AddRefとReleaseの戻り値は、両方とも現在の参照カウントになります。

クライアントが複数のスレッドを作成し、その両方のスレッドでオブジェクトを操作する可能性がある場合は、 上記の実装を少し工夫することになります。 具体的には、複数のスレッドが同じ変数を同時に使うことを防ぐために、 Interlocked系の関数を呼び出すことになります。

STDMETHODIMP_(ULONG) CMyServer::AddRef()
{
	return InterlockedIncrement(&m_cRef);
}

STDMETHODIMP_(ULONG) CMyServer::Release()
{
	if (InterlockedDecrement(&m_cRef) == 0) {
		delete this;
		return  0;
	}

	return m_cRef;
}

InterlockedIncrementは第1引数の変数の中身をインクリメントし、 戻り値はインクリメント後の値を返します。 InterlockedDecrement第1引数の変数の中身をデクリメントし、 戻り値はデクリメント後の値を返します。

さて、オブジェクトの参照カウントを増減するためには、当然ながら参照カウントを予め初期化しておく必要があります。 これは、一般にオブジェクトのコンストラクタで行われることになります。

CMyServer::CMyServer()
{
	m_cRef = 1;
}

CMyServerに対してnewを実行すれば、コンストラクタが呼ばれてm_cRefに1が代入されることになります。 開発者によってはここでm_cRefに0を代入し、後から明示的にAddRefを呼び出すことで参照カウントを1にする例もあります。

オブジェクトがIUnknownを実装するもう1つの理由は、 自身が実装するインターフェースをクライアントに公開するためです。 クライアントはCoCreateInstanceを呼び出してオブジェクトを作成するわけですが、 その際に指定できるインターフェースはただ1つだけです。 これでは、このただ1つのインターフェースを使用することでしかオブジェクトを操作できませんから、 他のインターフェースを取得するためのメソッドが必要になります。

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

	if (IsEqualIID(riid, IID_IUnknown))
		*ppvObject = this;
	else
		return E_NOINTERFACE;

	AddRef();
	
	return S_OK;
}

クライアントはインターフェースを取得したい段階になると、 そのインターフェースのIIDを指定してQueryInterfaceを呼び出します。 このときオブジェクトは、指定されたIIDを自身が実装するインターフェースのIIDと一致するか調べるためIsEqualIIDを呼び出します。 今回のオブジェクトはIUnknownを実装するだけですから、指定されたIIDがIUnknownのIIDである場合は、 オブジェクトのアドレスをppvObjectに格納します。 つまり、IUnknownを使用してオブジェクトを操作することを許可したということです。 このように、クライアントがオブジェクトを参照できるようになった場合は、 AddRefを呼び出してオブジェクトの参照カウントをインクリメントしておきます。 指定されたIIDで識別されるインターフェースをオブジェクトが実装していない場合は、 E_NOINTERFACEを返します。 このとき、ppvObjectには必ずNULLが格納されているようにしてください。 呼び出し側によっては関数の成否を戻り値ではなく、ppvObjectがNULLであるかで判断することも考えられるからです。


戻る