EternalWindows
COMアプリケーション / メインスレッドの処理

COMサーバーの章では、COMオブジェクトをインプロセスサーバー(DLL)として実装しましたが、 今回はオブジェクトをアウトプロセスサーバー(EXE)で実装する方法について説明します。 インプロセスサーバーは、クライアントに対して何らかの機能を提供したい場合に使用しますが、 アウトプロセスサーバーはそのような機能を提供する一方で、 そのアプリケーション本来の動作も行いたい場合に使用します。 たとえば、IEはクライアントに対してインターフェースを公開する一方で、 ブラウザとしての動作も行っているため、アウトプロセスサーバーの好例であるといえます。 現在作成しているアプリケーションが外部との通信を想定しており、 その通信方法としてCOMインターフェースを使用したい場合は、 アプリケーションをアウトプロセスサーバーとして設計するとよいでしょう。

COMオブジェクトをアウトプロセスサーバーで実装するといっても、 それがCOMサーバーであることには変わりありませんから、 インプロセスサーバーとの共通点も多くあります。 たとえば、インプロセスサーバーではIClassFactoryを実装したクラスオブジェクトを作成していましたが、 このような処理はアウトプロセスサーバーでも行います。 また、インプロセスサーバーでは、DllRegisterServerという関数でサーバーの情報を登録できるようになっていましたが、 このような登録処理もアウトプロセスサーバーは行います。 アウトプロセスサーバーは、DLLのように専用の関数をエクスポートすることはありませんから、 コマンドラインを基に登録処理を行うかどうかを判断します。

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	if ((lstrcmpA(lpszCmdLine, "-RegServer") == 0) || (lstrcmpA(lpszCmdLine, "/RegServer") == 0)) {
		RegisterServer();
		return 0;
	}
	else if ((lstrcmpA(lpszCmdLine, "-UnregServer") == 0) || (lstrcmpA(lpszCmdLine, "/UnregServer") == 0)) {
		UnregisterServer();
		return 0;
	}
	else
		;

	// クラスオブジェクトの登録などを行う

	return 0;
}

既に述べたように、アウトプロセスサーバーはexe形式のアプリケーションですから、 通常のWindowsアプリケーションと同じようにWinMainから始まります。 lpszCmdLineに-RegServerか/RegServerが含まれる場合は登録処理を行うのが一般的であるため、 RegisterServerという自作関数でそれを行っています。 一方、lpszCmdLineに-UnregServerか/UnregServerが含まれる場合は登録を解除するために、 UnregisterServerを呼び出しています。 レジストリに登録する情報はインプロセスサーバーとほぼ同様ですが、 InProcServer32にDLLのフルパスを書き込むのではなく、 LocalServer32にEXEへのフルパスを書き込む点が異なります。

オブジェクトがどのような種類のサーバーで実装されていたとしても、 クライアントがオブジェクトを取得するためにCoCreateInstanceを呼び出すのは変わりありません。 インプロセスサーバーの場合は、CoCreateInstanceでDLLがロードされることになっていましたが、 アウトプロセスサーバーの場合はCoCreateInstanceでEXEが起動されることになります。 起動されたEXEは、CoRegisterClassObjectでクラスオブジェクトを登録することになります。

HRESULT CoRegisterClassObject(
  REFCLSID rclsid,
  LPUNKNOWN pUnk,
  DWORD dwClsContext,
  DWORD flags,
  LPDWORD lpdwRegister
);

rclsidは、オブジェクトのCLSIDを指定します。 クライアントはこのCLSIDをCoCreateInstanceに指定できるようになります。 pUnkは、クラスオブジェクトのアドレスを指定します。 dwClsContextは、CLSCTX_LOCAL_SERVERを指定します。 flagsは、主にREGCLS_SINGLEUSEかREGCLS_MULTIPLEUSEを指定します。 lpdwRegisterは、登録したクラスオブジェクトを識別する値を受け取る変数のアドレスを指定します。

CoCreateInstanceにCLSCTX_LOCAL_SERVERを指定した場合、 CLSIDを基にレジストリのLocalServer32が参照され、EXEへのフルパスが取得されます。 起動されたEXEはCoRegisterClassObjectを呼び出し、 これによってIClassFactory::CreateInstanceが呼び出され、 CoCreateInstanceの呼び出し側にオブジェクトのアドレスが返ることになります。 CoRegisterClassObjectは、指定されたCLSIDをSCMに登録するようになっています。 この際、REGCLS_SINGLEUSEを指定している場合は、 サーバーが存在している間にCoCreateInstanceが呼ばれても新しくプロセスが作成されることになります。 一方、REGCLS_MULTIPLEUSEを指定している場合は、 サーバーが存在している間にCoCreateInstanceが呼ばれても新しくプロセスは作成されません。 既に起動しているプロセスのIClassFactory::CreateInstanceを呼び出すことによって、 オブジェクトのアドレスが返されることになります。 ちなみに、クライアントがCoCreateInstanceではなくCoGetClassObjectを呼び出した場合は、 IClassFactory::CreateInstanceは呼ばれません。

クラスオブジェクトの登録を解除したい場合は、CoRevokeClassObjectを呼び出します。

HRESULT CoRevokeClassObject(
  DWORD dwRegister
);

dwRegisterは、CoRegisterClassObjectのlpdwRegisterに返された値を指定します。

ここまでの内容を考慮したWinMainを次に示します。

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT       hr;
	DWORD         dwRegister;
	CClassFactory classFactory;
	int           nResult;

	if ((lstrcmpA(lpszCmdLine, "-RegServer") == 0) || (lstrcmpA(lpszCmdLine, "/RegServer") == 0)) {
		if (RegisterServer())
			MessageBox(NULL, TEXT("登録に成功しました。"), TEXT("OK"), MB_OK);
		else
			MessageBox(NULL, TEXT("登録に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	else if ((lstrcmpA(lpszCmdLine, "-UnregServer") == 0) || (lstrcmpA(lpszCmdLine, "/UnregServer") == 0)) {
		UnregisterServer();
		MessageBox(NULL, TEXT("登録を解除しました。"), TEXT("OK"), MB_OK);
		return 0;
	}
	else
		;

	CoInitializeEx(NULL, COINIT_MULTITHREADED);
	
	RegisterAndCreateWindow(hinst, nCmdShow);
	
	hr = CoRegisterClassObject(CLSID_MyServer, static_cast<IClassFactory *>(&classFactory), CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &dwRegister);
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}
	
	nResult = MessageLoop();
	
	CoRevokeClassObject(dwRegister);
	CoUninitialize();

	return nResult;
}

COMを使用するためには、CoInitializeまたはCoInitializeExを呼び出すことになります。 CoInitializeExにCOINIT_APARTMENTTHREADEDを指定した場合は、クライアントからのメソッドの処理をメインスレッドが行うようになりますが、 COINIT_MULTITHREADEDを指定した場合はCOMによって作成されたスレッドが行うようになります。 RegisterAndCreateWindowは、ウインドウクラスの登録とウインドウの作成を行います。 これは、クライアントからの呼び出しが発生する前に必要ですから、CoRegisterClassObjectの前に呼び出しておきます。 CoRegisterClassObjectの呼び出しによって、IClassFactory::CreateInstanceが呼び出されることになり、 クライアントにオブジェクトのアドレスが返ることになります。 オブジェクトからのメソッド呼び出しに応えるためには、サーバーが存在し続けなければならないため、 メッセージループで待機します。

クライアントに返されるオブジェクトというのは、今回のサーバーではウインドウを操作するオブジェクトになります。 このオブジェクトのクラスは、IWindowControlというカスタムインターフェースを継承しています。

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

	STDMETHODIMP Show(BOOL bShow);
	STDMETHODIMP GetTitle(LPWSTR *lplp);
	
	CMyServer();
	~CMyServer();
	
private:
	LONG m_cRef;
};

IWindowControlはカスタムインターフェースであるため、予めIDLファイルでインターフェースを定義しておくことになります。 そして、これをコンパイルすることによって作成されるヘッダーファイルをインクルードするようにします。 メソッドの実装は次のようになります。

STDMETHODIMP CMyServer::Show(BOOL bShow)
{
	ShowWindow(g_hwnd, bShow ? SW_SHOW : SW_HIDE);
	
	return S_OK;
}

STDMETHODIMP CMyServer::GetTitle(LPWSTR *lplp)
{
	int nLen;

	nLen = GetWindowTextLengthW(g_hwnd) + 1;

	*lplp = (LPWSTR)CoTaskMemAlloc(nLen * sizeof(WCHAR));

	GetWindowText(g_hwnd, *lplp, nLen);

	return S_OK;
}

Showは、ウインドウを表示または非表示にする場合に呼び出します。 g_hwndは、RegisterAndCreateWindowで予め初期化されています。 GetTitleは、ウインドウのタイトルを取得する場合に呼び出します。 当然ながらこのようなメソッドだけでは、クライアントがサーバーの機能を使用しているとは言い難いため、 実際の開発では意味のあるメソッドを用意しておくことになります。

サーバーはクライアントから呼び出しに応える一方で、GUIアプリケーションとしての動作も行わなければなりません。 しかし、今回はサンプルということで、WindowProcは意味のある実装になっていません。

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

上記の実装から分かるように、表示されるウインドウはクライアント領域に何も描画されていないシンプルなものになります。 これでは、クライアントがサーバーを起動しても仕方がないため、 実際の開発では意味のある実装を行っておくようにします。


戻る