EternalWindows
アパートメント / 接続の強弱

これまで述べてきたように、別アパートまたは別プロセスに存在するオブジェクトにアクセスする場合は、 プロキシとスタブと呼ばれるオブジェクトが内部的に使用されています。 クライアントはオブジェクトのメソッドを呼び出しているようで、実際にはプロキシのメソッドを呼び出しており、 このプロキシがサーバーのオブジェクトのスタブと接続し、スタブにデータを送るようになっています。 多くの場合、このようなプロキシとスタブの通信を気にする必要はありませんが、 これら両者の接続に強弱があることを知っていれば、 オブジェクトを実装する際に役立つことがあります。

まず強い接続とは、クライアントがオブジェクト(及びスタブ)を破棄できない接続のことを指します。 クライアントがReleaseを呼び出した場合は、スタブのReleaseが呼ばれることになりますが、 ここからさらにオブジェクトのReleaseが呼ばれることは基本的にありません。 仮に呼ばれるとしてもAddRefなどで予め参照カウンタを調整しておき、Releaseで参照カウンタが0にならないようにしています。 このような調整はスタブの参照カウンタでも行われているため、クライアントはReleaseによってスタブやオブジェクトを破棄することはできません。 強い接続では、オブジェクトを作成したサーバー自身にオブジェクトを破棄させることが狙いであるため、 クライアントがオブジェクトを勝手に削除できてしまっては困るわけです。 これに対して弱い接続におけるスタブは、自身やオブジェクトの参照カウンタを調整しようとはしません。 つまり、クライアントがReleaseを呼び出した場合は、スタブが破棄されることもありますし、 スタブがオブジェクトのReleaseを呼び出すことによって、オブジェクトが破棄されることもあります。 ただし、サーバーは基本的に内部でオブジェクトを参照していると思われますから、 スタブはともかくオブジェクトが破棄されることは基本的にないといえるでしょう。 話を整理すると、強い接続ではクライアントがスタブとオブジェクトを破棄することはできませんが、 弱い接続の場合はスタブを破棄することができるということになります。

接続の強弱を身近に感じる例としては、ROT(Running Object Table)におけるオブジェクトの登録処理があります。 ROTは、現在実行中のオブジェクトを格納できるグローバルなテーブルであり、 サーバーがここにオブジェクトを登録すると、クライアントはROTを通じてオブジェクトを取得できるようになります。 次に、ROTにオブジェクトを登録するサーバーの例を示します。

#include <windows.h>

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

	CObject();
	~CObject();

private:
	LONG m_cRef;
};

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	DWORD               dwRegister;
	IMoniker            *pMoniker;
	CObject             *pObject;
	IRunningObjectTable *pRunningObjectTable;

	CoInitialize(NULL);

	pObject = new CObject();

	GetRunningObjectTable(0, &pRunningObjectTable);
	
	CreateItemMoniker(L"!", L"sample", &pMoniker);
	pRunningObjectTable->Register(0, pObject, pMoniker, &dwRegister);

	MessageBox(NULL, TEXT("終了します。"), TEXT("サーバー"), MB_OK);

	pRunningObjectTable->Revoke(dwRegister);
	pRunningObjectTable->Release();
	pObject->Release();
	CoUninitialize();

	return 0;
}


// CObject


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

CObject::~CObject()
{
}

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

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

	AddRef();

	return S_OK;
}

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

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

	return m_cRef;
}

ROTにアクセスするには、GetRunningObjectTableでIRunningObjectTableを取得します。 Registerの第2引数には登録したいオブジェクトを指定し、第3引数にはオブジェクトを名前で識別するためのモニカを指定します。 CObjectはIUnknownしか実装していませんが、実際の開発ではクライアントにとって役立つインターフェースを実装することになります。 MessageBoxを呼び出しているのはサーバーが直ちに終了するのを防ぐためであり、 このタイミングで次のクライアントを実行します。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	IMoniker            *pMoniker;
	IEnumMoniker        *pEnumMoniker;
	IBindCtx            *pBindCtx;
	IRunningObjectTable *pRunningObjectTable;
	LPOLESTR            lpszDisplayName;

	CoInitialize(NULL);

	GetRunningObjectTable(0, &pRunningObjectTable);
	pRunningObjectTable->EnumRunning(&pEnumMoniker);
	
	CreateBindCtx(NULL, &pBindCtx);

	while (pEnumMoniker->Next(1, &pMoniker, NULL) == S_OK) {
		pMoniker->GetDisplayName(pBindCtx, NULL, &lpszDisplayName);
		if (lstrcmpW(lpszDisplayName, L"!sample") == 0) {
			IUnknown *p;
			HRESULT  hr;
			hr = pRunningObjectTable->GetObject(pMoniker, &p);
			if (SUCCEEDED(hr)) {
				MessageBox(NULL, TEXT("オブジェクトを取得しました。"), TEXT("クライアント"), MB_OK);
				p->Release();
			}
		}

		pMoniker->Release();
	}
	
	pBindCtx->Release();
	pEnumMoniker->Release();
	pRunningObjectTable->Release();
	CoUninitialize();

	return 0;
}

IRunningObjectTable::EnumRunningは、オブジェクトと共に登録されたモニカを列挙するIEnumMonikerを返します。 IEnumMoniker::Nextによって各モニカを列挙することができ、サーバーが登録したモニカを発見したら、 IRunningObjectTable::GetObjectによってモニカに関連するオブジェクトを取得します。 本来ならここでは、オブジェクトからインターフェースを照会して何かを行うことになりますが、 上記では単純にオブジェクトを開放しています。

クライアントがROTを通じてオブジェクトを取得するといっても、 クライアントとサーバーは互いに別プロセスであるため、 取得できるオブジェクトというのはプロキシになります。 IRunningObjectTable::Registerの第1引数にROTFLAGS_REGISTRATIONKEEPSALIVEを指定していない場合は、弱い接続が行われることになり、 プロキシのReleaseを呼び出した場合はサーバーのスタブのReleaseが呼ばれることになります。 これによりスタブは破棄されることになり、2回目のクライアントの実行ではプロキシを取得できないことになります。 一方、RegisterにROTFLAGS_REGISTRATIONKEEPSALIVEを指定した場合は、強い接続が行われることになり、 プロキシのReleaseを呼び出してもサーバーのスタブは破棄されません。 よって、サーバーが存在していれば、クライアントを何回実行してもオブジェクトを取得できることになります。 これらの点を考慮すると、サーバーはオブジェクトを一度だけ取得させたい場合に弱い接続を使用し、 取得回数を問わない場合に強い接続を使用すればよいでしょう。

IRunningObjectTable::Registerのように接続の強弱を決められるわけではなく、 既定で弱い接続を使用するようなオブジェクトがあったとして、それを強い接続に変更させることは可能なのでしょうか。 これには、主に2つの方法があります。 1つ目は、サーバーがオブジェクトに対してCoLockObjectExternalを呼び出す方法です。

CoLockObjectExternal(p, TRUE, FALSE);

CoLockObjectExternalは、第1引数のオブジェクトに関連するスタブのロック状態を第2引数のものに変更します。 この値に応じてスタブのAddRef/Releaseが呼ばれ、 オブジェクトがIExternalConnectionを実装している場合は、 AddConnection/ReleaseConnectionも呼ばれます。 つまり、第2引数がTRUEであればスタブは破棄されないようになり、FALSEであればスタブは破棄されるようになります。 第3引数は最後の参照から取り除かれた際にスタブを破棄するかどうかですが、これはFALSEでよいでしょう。 2つ目は、サーバーのオブジェクトがIExternalConnectionを実装する方法です。

DWORD CObject::AddConnection(DWORD extconn, DWORD reserved)
{
	if (extconn & EXTCONN_STRONG)
		InterlockedIncrement(&m_cExtRef);
	
	return m_cExtRef;
}

DWORD CObject::ReleaseConnection(DWORD extconn, DWORD reserved, BOOL fLastReleaseCloses)
{
	if (extconn & EXTCONN_STRONG) {
		InterlockedDecrement(&m_cExtRef);
		if (m_cExtRef == 0 && fLastReleaseCloses)
			CoDisconnectObject(this, 0);
	}

	return m_cExtRef;
}

メソッドの実装内容に関わらず、オブジェクトがQueryInterfaceでIExternalConnectionを返す時点で、強い接続が行われます。 このため、extconnには強い接続を含むEXTCONN_STRONGが常に含まれると思われます。 先のCObjectがIExternalConnectionを実装している場合、IRunningObjectTable::GetObjectでAddConnectionが呼ばれ、 p->ReleaseにてReleaseConnectionが呼ばれます。 つまり、オブジェクトが外部から参照される場合にAddConnectionが呼ばれ、 参照が不要になった場合にReleaseConnectionが呼ばれます。 ReleaseConnectionの第3引数がTRUEである場合は、 最後の参照から取り除かれた際にスタブを破棄しなければならないことを意味します。 よって、外部からの参照をカウントするm_cExtRefが0である場合は、 CoDisconnectObjectでスタブを破棄しています。 結局のところ、CoLockObjectExternalとIExternalConnectionのどちらがよいかというと、 単純にスタブをロックしたいだけであれば、CoLockObjectExternalを呼び出すとよいでしょう。 クライアントからの外部参照を検出したい場合は、IExternalConnectionを実装するようにします。

最後に、既定で強い接続が行われる例を述べます。 クラスオブジェクトを登録するCoRegisterClassObjectは、 複数のクライアントからのオブジェクトの作成要求に応えなければならないため、 特定のクライアントによるReleaseで無条件に破棄されるわけにはいきません。 よって、強い接続を使用し、外部からの参照数が0になった場合に自ら破棄を行うようになります。 この参照管理にはIExternalConnectionを使用することができますが、多くの場合はIClassFactory::LockServerが使用されています。 ROTと少し似た要素を持つIShellWindowsも強い接続を行います。 IRunningObjectTableが任意のオブジェクトの登録に使われるのに対して、 IShellWindowsはIShellBrowserを提供するオブジェクトを登録するのに使用されます。 IShellWindows::Registerで登録したオブジェクトは、IShellWindows::Itemで取得することができ、 このときオブジェクトがIExternalConnectionを実装している場合は、AddConnectionが呼ばれます。


戻る