EternalWindows
COMサーバー / COMサーバーサンプル

今回は、これまで取り上げてきたコードで構成されたサーバーのサンプルを示します。 まず、DLLのdefファイルを示します。

LIBRARY	"mydll"

EXPORTS
	DllCanUnloadNow PRIVATE
	DllGetClassObject PRIVATE
	DllRegisterServer PRIVATE
	DllUnregisterServer PRIVATE

LIBRARYには開発環境のプロジェクト名を指定し、 EXPORTS以下には外部から呼ばれることになる関数を記述します。 各関数が呼ばれるタイミングを再確認すると、DllCanUnloadNowはCoFreeUnusedLibrariesによる呼び出し、 DllGetClassObjectはCoGetClassObjectによる呼び出し、 DllRegisterServerとDllUnregisterServerはサーバーを登録/解除するプロセスに呼び出されます。

次に、ヘッダーファイル(sample.hと仮定)の定義を示します。

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;
};

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);
};

extern void LockModule(BOOL bLock);
extern BOOL CreateRegistryKey(HKEY hKeyRoot, LPTSTR lpszKey, LPTSTR lpszValue, LPTSTR lpszData);

CMyServerがオブジェクトのクラスになり、CMyServerFactoryがクラスオブジェクトのクラスになります。

次にソースファイルを示します。 SHDeleteKeyを呼び出す関係上、shlwapi.hのインクルードとshlwapi.libへのリンクが必要になります。

#include <windows.h>
#include <shlwapi.h>
#include "sample.h"

#pragma comment (lib, "shlwapi.lib")

const CLSID CLSID_MyServer = {0x112143a6, 0x62c1, 0x4478, {0x9e, 0x8f, 0x87, 0x26, 0x99, 0x25, 0x5e, 0x2e}};
const TCHAR g_szClsid[] = TEXT("{112143A6-62C1-4478-9E8F-872699255E2E}");
const TCHAR g_szProgid[] = TEXT("Sample.MyServer");

LONG      g_lLocks = 0;
HINSTANCE g_hinstDll = NULL;


// CMyServer


CMyServer::CMyServer()
{
	m_cRef = 1;
	m_hFile = INVALID_HANDLE_VALUE;

	LockModule(TRUE);
}

CMyServer::~CMyServer()
{
	if (m_hFile != INVALID_HANDLE_VALUE)
		CloseHandle(m_hFile);

	LockModule(FALSE);
}

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;
}

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

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

	return m_cRef;
}

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

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

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);
}

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;
}

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;
}


// CMyServerFactory


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;
}

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

	return 2;
}

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

	return 1;
}

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;
}

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

	return S_OK;
}


// DLL Export


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

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;
}

STDAPI DllRegisterServer(void)
{
	TCHAR szModulePath[MAX_PATH];
	TCHAR szKey[256];

	wsprintf(szKey, TEXT("CLSID\\%s"), g_szClsid);
	if (!CreateRegistryKey(HKEY_CLASSES_ROOT, szKey, NULL, TEXT("COM Server Sample")))
		return E_FAIL;

	GetModuleFileName(g_hinstDll, szModulePath, sizeof(szModulePath) / sizeof(TCHAR));
	wsprintf(szKey, TEXT("CLSID\\%s\\InprocServer32"), g_szClsid);
	if (!CreateRegistryKey(HKEY_CLASSES_ROOT, szKey, NULL, szModulePath))
		return E_FAIL;
	
	wsprintf(szKey, TEXT("CLSID\\%s\\InprocServer32"), g_szClsid);
	if (!CreateRegistryKey(HKEY_CLASSES_ROOT, szKey, TEXT("ThreadingModel"), TEXT("Apartment")))
		return E_FAIL;
	
	wsprintf(szKey, TEXT("CLSID\\%s\\ProgID"), g_szClsid);
	if (!CreateRegistryKey(HKEY_CLASSES_ROOT, szKey, NULL, (LPTSTR)g_szProgid))
		return E_FAIL;

	wsprintf(szKey, TEXT("%s"), g_szProgid);
	if (!CreateRegistryKey(HKEY_CLASSES_ROOT, szKey, NULL, TEXT("COM Server Sample")))
		return E_FAIL;
  
	wsprintf(szKey, TEXT("%s\\CLSID"), g_szProgid);
	if (!CreateRegistryKey(HKEY_CLASSES_ROOT, szKey, NULL, (LPTSTR)g_szClsid))
		return E_FAIL;

	return S_OK;
}

STDAPI DllUnregisterServer(void)
{
	TCHAR szKey[256];

	wsprintf(szKey, TEXT("CLSID\\%s"), g_szClsid);
	SHDeleteKey(HKEY_CLASSES_ROOT, szKey);
	
	wsprintf(szKey, TEXT("%s"), g_szProgid);
	SHDeleteKey(HKEY_CLASSES_ROOT, szKey);

	return S_OK;
}

BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD dwReason, LPVOID lpReserved)
{
	switch (dwReason) {

	case DLL_PROCESS_ATTACH:
		g_hinstDll = hinstDll;
		DisableThreadLibraryCalls(hinstDll);
		return TRUE;

	}

	return TRUE;
}


// Function


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


BOOL CreateRegistryKey(HKEY hKeyRoot, LPTSTR lpszKey, LPTSTR lpszValue, LPTSTR lpszData)
{
	HKEY  hKey;
	LONG  lResult;
	DWORD dwSize;

	lResult = RegCreateKeyEx(hKeyRoot, lpszKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL);
	if (lResult != ERROR_SUCCESS)
		return FALSE;

	if (lpszData != NULL)
		dwSize = (lstrlen(lpszData) + 1) * sizeof(TCHAR);
	else
		dwSize = 0;

	RegSetValueEx(hKey, lpszValue, 0, REG_SZ, (LPBYTE)lpszData, dwSize);
	RegCloseKey(hKey);
	
	return TRUE;
}

最後に、COMサーバーを実際に登録する方法について説明します。 サーバーはDllRegisterServerに登録に関するコードを記述しているため、 これを呼び出すためのアプリケーションが必要になりますが、 正にこの用途のためにregsvr32.exeが存在します。 このアプリケーションをコマンドプロントから次のように起動すれば、 DLLのDllRegisterServerが呼ばれることになります。

regsvr32 C:\sample.dll

上記のように、regsvr32の後にスペースを空けてDLLのフルパスを指定します。 失敗する場合は管理者として実行しているかを確認してください。 登録を解除する場合は次のようになります。

regsvr32 /u C:\sample.dll

/uを指定することで、DLLのDllUnregisterServerが呼ばれるようになります。 ちなみに、登録したDLLを新しいものに変更したい場合は登録を解除する必要はありません。 その新しいDLLを既存のDLLに上書きするだけで十分です。

COMサーバーのテスト

次に示すコードは、今回作成したCOMサーバーが正しく動作するかをテストします。 テストの内容としては、CLSIDとProgIDの相互変換、IPersistFileとISequentialStreamの取得、 Releaseによるオブジェクトの破棄などになります。

#include <windows.h>

const CLSID CLSID_MyServer = {0x112143a6, 0x62c1, 0x4478, {0x9e, 0x8f, 0x87, 0x26, 0x99, 0x25, 0x5e, 0x2e}};

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT           hr;
	CLSID             clsid;
	OLECHAR           *pszProgId;
	IPersistFile      *pPersistFile;
	ISequentialStream *pSequentialStream;
	ULONG             uRead;
	TCHAR             szBuf[256];
	char              szData[256];

	CoInitialize(NULL);

	hr = ProgIDFromCLSID(CLSID_MyServer, &pszProgId);
	if (FAILED(hr)) {
		MessageBox(NULL, TEXT("CLSIDからProgIDの変換に失敗しました。"), NULL, MB_ICONWARNING);
		CoUninitialize();
		return 0;
	}

	hr = CLSIDFromProgID(pszProgId, &clsid);
	if (FAILED(hr)) {
		MessageBox(NULL, TEXT("ProgIDからCLSIDの変換に失敗しました。"), NULL, MB_ICONWARNING);
		CoUninitialize();
		return 0;
	}

	hr = CoCreateInstance(CLSID_MyServer, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pPersistFile));
	if (FAILED(hr)) {
		wsprintf(szBuf, TEXT("IPersistFileの取得に失敗しました。%08x"), hr);
		MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
		CoUninitialize();
		return 0;
	}

	hr = pPersistFile->Load(TEXT("C:\\sample.txt"), STGM_READ);
	if (FAILED(hr)) {
		MessageBox(NULL, TEXT("ファイルのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		pPersistFile->Release();
		CoUninitialize();
		return 0;
	}

	hr = pPersistFile->QueryInterface(IID_PPV_ARGS(&pSequentialStream));
	if (FAILED(hr)) {
		wsprintf(szBuf, TEXT("ISequentialStreamの取得に失敗しました。%08x"), hr);
		MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
		pPersistFile->Release();
		CoUninitialize();
		return 0;
	}

	hr = pSequentialStream->Read(szData, 5, &uRead);
	szData[uRead] = '\0';
	MessageBoxA(NULL, szData, "OK", MB_OK);

	pSequentialStream->Release();
	if (pPersistFile->Release() != 0) {
		MessageBox(NULL, TEXT("オブジェクトが開放されていません。"), NULL, MB_ICONWARNING);
		CoUninitialize();
		return 0;
	}
	
	MessageBox(NULL, TEXT("終了します。"), TEXT("OK"), MB_OK);

	CoUninitialize();
	
	return 0;
}

CLSID_MyServerという変数は、登録したCOMサーバーのCLSIDを格納しています。 この値は前節のプログラムと同様でなければなりません。 最初に確認を行っているのは、ProgIDFromCLSIDによるCLSIDからProgIDへの変換です。 これが成功した場合は、今度はCLSIDFromProgIDを呼び出してProgIDからCLSIDへ変換します。 これらが失敗する場合は、CLSIDの値が不正であるか、レジストリへの書き込みが正しく行われていない可能性があります。

CoCreateInstanceが失敗する原因は様々です。 まず、指定したCLSIDを持つレジストリキーが存在しない場合は関数が失敗します。 次に、第3引数に指定したコンテキストがCOMサーバーのそれと一致していない場合も失敗します。 レジストリにはInprocServer32で登録していたため、第3引数にはCLSCTX_INPROC_SERVERを指定する必要があるのですが、 これをCLSCTX_LOCAL_SERVERにした場合は失敗することになります。 また、第4引数に指定したIIDで識別されるインターフェースをオブジェクトが実装していない場合も失敗します。 そして以外に見落としやすいのは、COMサーバーが存在していないことによる失敗です。 レジストリにはサーバーの実体が存在するファイルパスを書き込んでいるわけですが、 このパスにサーバーのDLLが存在しない場合は関数が失敗します。 CoCreateInstanceの戻り値を確認すれば、エラーの原因を突き止めることができます。

IPersistFileを取得したら、これが正しく機能するかを確認するためにLoadを呼び出します。 これにより、前節のCMyServer::Loadが呼ばれることになります。 Loadが成功したら次はファイルを読み取ることになりますが、 これにはISequentialStreamが必要になります。 よって、QueryInterfaceでこれを取得することになります。 Readを呼び出して正しくファイル内のデータを取得できていれば、 オブジェクトのファイル操作は問題ないことになります。

CoCreateInstanceが成功した時点でオブジェクトの参照カウントは1になり、 さらにQueryInterfaceが成功したことによって現在の参照カウントは2になっています。 pSequentialStream->Releaseによって参照カウントが1になり、 さらにpPersistFile->Releaseによって参照カウントが0になるはずですから、 本当にそのようになっているかを確認します。 ここで失敗するということは、オブジェクトのためのメモリが開放されていないことを意味するため、 DLLのコードを見直す必要があります。

上記コードでは行っていませんが、クライアントによっては不要になったDLLをアンロードする目的でCoFreeUnusedLibrariesを呼び出すことがあります。 これが成功しているかを確認したい場合は、pPersistFile->Releaseの後に次のようなコードを記述すればよいでしょう。 DLLのパスは、C:\\sample.dllであるとします。

CoFreeUnusedLibraries();

if (GetModuleHandle(TEXT("C:\\sample.dll")) == NULL)
	MessageBox(NULL, TEXT("DLLはアンロードされています。"), TEXT("OK"), MB_OK);
else
	MessageBox(NULL, TEXT("DLLはアンロードされていません。"), NULL, MB_ICONWARNING);

GetModuleHandleは、第1引数のDLLがロードされている場合にそのロードされているアドレスを返します。 CoFreeUnusedLibrariesによってDLLが確かにアンロードされたのであれば戻り値はNULLになりますから、 DLLがロードされていない旨が表示されるはずです。 一方、CoFreeUnusedLibrariesをReleaseの前に実行した場合は、DLLがロードされている旨が表示されるはずです。 なぜなら、オブジェクトの参照中はDLLがロックされているはずですから、 サーバーのDllCanUnloadNowは0を返すことになり、DLLをアンロードするわけにはいかないからです。 なお、CoUninitializeを呼び出した場合は、DllCanUnloadNowが返す値を問わずDLLはアンロードされます。



戻る