EternalWindows
COMサーバー / 既存のインターフェース

前節のオブジェクトはIUnknownを実装していましたが、 実際の開発ではIUnknownだけを実装しても意味がありません。 IUnknownは、参照カウントの増減やインタフェースを問い合わせることが目的ですから、 実際にオブジェクトを操作するためにはまた別のインターフェースを実装する必要があります。 このための選択肢として、独自のカスタムインターフェースを実装する方法と、 予め定義された既存のインターフェースを実装する方法がありますが、 今回は既存のインターフェースについて説明します。 次に、定義されているいくつかのインターフェースを示します。

インターフェース 説明
IPersistFile ファイルのロードや保存を行うメソッドを持っている。
ISequentialStream データの読み取りや書き込みを行うメソッドを持っている。
IOleWindow ウインドウハンドルを取得するメソッドを持っている。 このインターフェースを実装するオブジェクトは、ほぼ例外なくウインドウを表示しているはずである。
IServiceProvider 自身とは別のオブジェクトを提供するメソッドを持っている。 目的のインターフェースをオブジェクトのQueryInterfaceから取得できなった場合は、 QueryInterfaceからIServiceProviderを取得し、IServiceProvider::QueryServiceによって目的のインターフェースを取得すると面白い。
IObjectWithSite オブジェクトがIObjectWithSiteを実装しているのであれば、 クライアントはそのオブジェクトに何らかのオブジェクトを渡すことができる。 渡されるオブジェクトが実装すべきインターフェースは状況によって異なる。

既存のインターフェースを使用する最大の利点は、 このインターフェースの用途が既に明確になっているという点です。 たとえば、クライアントはIPersistFileがファイルのロードや保存に使用されることを知っていますから、 これをオブジェクトに対して問い合わせて成功すれば、 そのオブジェクトはファイルのロードや保存をサポートするということが分かります。 基本的にクライアントはオブジェクトの詳細を知らないわけですが、 こうした既存のインターフェースをオブジェクトが実装することで、 既存のインターフェースを通じたオブジェクトの操作が可能になるのです。

オブジェクトが持つべき機能を考える場合、その機能が既存のインターフェースで表せないかどうかは常に考えるべきです。 たとえば、オブジェクトがファイルのロードという機能を必要するのであれば、 先に示したIPersistFileを使用すればよいでしょう。 また、オブジェクトがデータの読み取りや書き込みを必要するのであれば、 ISequentialStreamを使用することができます。 次に、これら2つのインターフェースを継承したCMyServerの定義を示します。

class CMyServer : public IPersistFile, public ISequentialStream
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	
	STDMETHODIMP GetClassID(CLSID *pClassID);
	STDMETHODIMP IsDirty();
	STDMETHODIMP Load(LPCOLESTR pszFileName, DWORD dwMode);
	STDMETHODIMP Save(LPCOLESTR pszFileName, BOOL fRemember);
	STDMETHODIMP SaveCompleted(LPCOLESTR pszFileName);
	STDMETHODIMP GetCurFile(LPOLESTR *ppszFileName);

	STDMETHODIMP Read(void *pv, ULONG cb, ULONG *pcbRead);
	STDMETHODIMP Write(const void *pv, ULONG cb, ULONG *pcbWritten);
	
	CMyServer();
	~CMyServer();

private:
	LONG   m_cRef;
	HANDLE m_hFile;
};

上記コードでは、オブジェクトの必須となるIUnknownを明示的に継承していませんが、これは問題ではありません。 理由は、IUnknownを除く全てのインターフェースがIUnknownを継承するようになっているからです。 このようなインターフェースの継承というのはよくあることであり、 たとえばIPersistFileはIPersistを継承するようになっています。 インターフェースとメソッドの対応については、GetClassIDがIPersistのメソッドであり、 IsDirtyからGetCurFileがIPersistFile、ReadとWriteがISequentialStreamのメソッドになります。

前節のQueryInterfaceではIUnknownを考慮するだけでしたが、 今回のCMyServerはIUnknown以外のインターフェースも継承しています。 よって、QueryInterfaceではそれらのインターフェースも考慮することになります。

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

	if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IPersist) || IsEqualIID(riid, IID_IPersistFile) || IsEqualIID(riid, IID_ISequentialStream))
		*ppvObject = this;
	else
		return E_NOINTERFACE;

	AddRef();
	
	return S_OK;
}

CMyServerが継承しているインターフェースは、IUnknown、IPersist、IPersistFile、ISequentialStreamです。 オブジェクトはこれらのインターフェースで識別することができますから、 これらのIIDが送られた場合はオブジェクトのアドレスを返すようにしています。 しかし、実際にはこの実装は大きな問題があります。 それは、インスターフェースのvptrが常に最初(IPersistFile)のvtblを識別するという点です。

クラスが仮想関数を持ったインターフェースを継承する場合、 コンパイラはクラスのためにvtblと呼ばれる関数テーブルを作成します。 そして、クラス及びインターフェースにはvptrと呼ばれる関数ポインタが暗黙的に追加され、 このvptrでvtblを識別することになります。 問題というのは、インターフェースのvptrが適切なvtblを識別していない場合に発生します。 今回のCMyServerで作成されるvtblは次の2つです。

クラスのメンバには2つのvptrが追加されています。 片方のvptrはIPersistFileに関するvtblを識別しており、 もう片方のvptrはISequentialStreamに関するvtblを識別しています。 どちらのインターフェースも共にIUnknownを継承しているため、 vtblの先頭にはIUnknownのメソッドが設定されます。 また、IPersistFileはIPersistを継承しているため、IUnknownの後にはIPersistのメソッドが続きます。 このメソッドの順番というのが非常に重要で、たとえばオブジェクトをISequentialStreamで識別している場合、 pSequentialStream->Readというコードは、vtblの先頭から4番目のメソッドを呼び出す命令として処理されます。 Readが呼ばれると述べていないことに注意してください。 もし、インターフェースのvptrがIPersistFileに関するvtblを識別している場合、 vtblの先頭から4番目のメソッドはGetClassIDになりますから、 Readを呼び出したつもりでも実際にはGetClassIDが呼ばれることになってしまいます。 こうしたことからインターフェースのvptrは、複数存在するvtblの中から適切なものを識別していなければなりません。

既に示したQueryInterfaceの問題点は、どのようなインターフェースが渡されてもオブジェクトのアドレスをそのまま返した点です。 このとき、適切なインターフェースでオブジェクトをキャストしていれば、 インターフェースのvptrは適切なvtblを識別するようになります。

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

	if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IPersist) || IsEqualIID(riid, IID_IPersistFile))
		*ppvObject = static_cast<IPersistFile *>(this);
	else if (IsEqualIID(riid, IID_ISequentialStream))
		*ppvObject = static_cast<ISequentialStream *>(this);
	else
		return E_NOINTERFACE;

	AddRef();
	
	return S_OK;
}

このコードでは、IID_IUnknown、IID_IPersist、IID_IPersistFileの場合にオブジェクトをIPersistFileでキャストしていますが、 IID_ISequentialStreamの場合はISequentialStreamでキャストしています。 これにより、ppvObjectはISequentialStreamに関するvtblを識別することになり、 呼び出し側はISequentialStreamのメソッドを呼び出せるようになります。 キャストについてはC言語のキャストでも問題ありませんが、C++言語のstatic_castを使用するのが一般的になっています。 ちなみに、今回の場合はIUnknownでオブジェクトをキャストすることはできません。 理由は、IUnknownがIPersistFileとISequentialStreamのどちらにも含まれるため、 コンパイラがどちらのvtblを参照すればよいかが分からないからです。

続いて、IPersistFileのメソッドについて見ていきます。 まずは、ファイルのオープンに使用するLoadを実装します。

STDMETHODIMP CMyServer::Load(LPCOLESTR pszFileName, DWORD dwMode)
{
	DWORD dwError;
	DWORD dwDesiredAccess;
	DWORD dwCreationDisposition;
	
	if (dwMode & STGM_READWRITE)
		dwDesiredAccess = GENERIC_READ | GENERIC_WRITE;
	else if (dwMode & STGM_WRITE)
		dwDesiredAccess = GENERIC_WRITE;
	else
		dwDesiredAccess = GENERIC_READ;

	if (dwMode & STGM_CREATE)
		dwCreationDisposition = CREATE_ALWAYS;
	else
		dwCreationDisposition = OPEN_EXISTING;

	m_hFile = CreateFile(pszFileName, dwDesiredAccess, 0, NULL, dwCreationDisposition, FILE_ATTRIBUTE_NORMAL, NULL);
	dwError = GetLastError();

	return HRESULT_FROM_WIN32(dwError);
}

Loadの第1引数はオープンしたいファイル名であり、第2引数はアクセスモードです。 このアクセスモードを上記のように解析すれば、CreateFileに指定すべき定数を導き出すことができます。 CreateFileで取得したファイルハンドルは後で必要になるため、メンバ変数として保存することになります。 Loadの戻り値はHRESULT型であるため、DWORD型であるGetLastErrorの戻り値をそのまま返すわけにはいきません。 よって、HRESULT_FROM_WIN32マクロでHRESULT型に変換することになります。

既存のインターフェースを使用する欠点は、 そのインターフェースのメソッドを全てオーバーライドしなければならないという点です。 今回のIPersistFileで必要になるメソッドはLoadだけなのですが、 全てのメソッドが純粋仮想関数として定義されている以上、 自身にとって興味のないメソッドもオーバーライドすることになります。 ただし、肝心の実装については次のように省略することができます。

STDMETHODIMP CMyServer::GetClassID(CLSID *pClassID)
{
	return E_NOTIMPL;
}

STDMETHODIMP CMyServer::IsDirty()
{
	return E_NOTIMPL;
}

STDMETHODIMP CMyServer::Save(LPCOLESTR pszFileName, BOOL fRemember)
{
	return E_NOTIMPL;
}

STDMETHODIMP CMyServer::SaveCompleted(LPCOLESTR pszFileName)
{
	return E_NOTIMPL;
}

STDMETHODIMP CMyServer::GetCurFile(LPOLESTR *ppszFileName)
{
	return E_NOTIMPL;
}

メソッドを実装するには、そのメソッドの引数や動作内容を理解しなければなりませんが、 これを完全に理解して正確なコードを記述するのは難しいものがあります。 よって、どのように実装してよいか分からないメソッドについては、 実装できていないことを示すE_NOTIMPLを返すようにします。

ISequentialStreamのメソッドは次のようになっています。

STDMETHODIMP CMyServer::Read(void *pv, ULONG cb, ULONG *pcbRead)
{
	DWORD dwError;

	ReadFile(m_hFile, pv, cb, pcbRead, NULL);
	dwError = GetLastError();

	return HRESULT_FROM_WIN32(dwError);
}

STDMETHODIMP CMyServer::Write(const void *pv, ULONG cb, ULONG *pcbWritten)
{
	return E_NOTIMPL;
}

Readでは、ReadFileを呼び出してファイル内のデータを取得します。 ファイルの書き込みはサポートしないということで、WriteではE_NOTIMPLを返しています。

QISearchについて

DLLがshlwapi.hをインクルードしている場合は、 QueryInterfaceでQISearchという関数を呼び出すことができます。 この関数は、指定されたインターフェースをオブジェクトが実装しているかを調べると共に、 適切なインターフェースでキャストされたオブジェクトのアドレスを返す機能を持っています。 次に例を示します。

STDMETHODIMP CMyServer::QueryInterface(REFIID riid, void **ppvObject)
{
	QITAB qit[] = {
		&IID_IPersist, OFFSETOFCLASS(IPersist, CMyServer),
		&IID_IPersistFile, OFFSETOFCLASS(IPersistFile, CMyServer),
		&IID_ISequentialStream, OFFSETOFCLASS(ISequentialStream, CMyServer),
		{0}
	};

	return QISearch(this, qit, riid, ppvObject);
}

QISearchは、第2引数にQITAB構造体の配列を要求します。 最初のメンバはインターフェースのIIDであり、2番目のメンバは目的のインターフェースまでへのオフセットです。 このオフセットはOFFSETOFCLASSマクロで求めることができます。 配列の要素はオブジェクトが実装しているインターフェースの数だけ必要ですが、 IUnknownは含める必要はありません。 また、要素の終端は0で初期化されていなければなりません。

QITABENTというマクロを使用すると、QITABの初期化をさらに簡単にすることができます。

QITAB qit[] = {
	QITABENT(CMyServer, IPersist),
	QITABENT(CMyServer, IPersistFile),
	QITABENT(CMyServer, ISequentialStream),
	{0}
};

オブジェクトが実装するインターフェースによっては、QITABENTMULTIというマクロが必要になることもあります。 たとえば、オブジェクトがISequentialStreamではなく、IPersistStreamを実装していたとします。 IPersistStreamはIPersistを継承していますが、このIPersistはIPersistFileにも含まれていたはずです。 このようなとき、IPersistによるキャストはコンパイラによって曖昧であると判断されるため、 どちらか片方のインターフェースでキャストする必要があります。

QITAB qit[] = {
	QITABENTMULTI(CMyServer, IPersist, IPersistStream),
	QITABENT(CMyServer, IPersistFile),
	QITABENT(CMyServer, IPersistStream),
	{0}
};

QITABENTMULTIマクロを使用すると、IIDについては第2引数のインターフェースが使用されますが、 オブジェクトのキャストには第3引数のインターフェースが使用されます。 IPersistStream(IPersistFileでも可能)でキャストすれば、コンパイルエラーが発生することはありません。



戻る