EternalWindows
アパートメント / インターフェースマーシャリング

スレッドが属するアパートとオブジェクトが属するアパートが異なる場合、 本来ならばスレッドはそのオブジェクトにアクセスすべきではないといえます。 たとえば、オブジェクトがSTAに属しているのであれば、 オブジェクトは自身と同じSTAに属しているスレッドからのアクセスのみを想定しているため、 異なるアパートのスレッドからメソッドが呼ばれては困るわけです。 しかし、COMではアプリケーションから見えない背後でインターフェースのマーシャリングとアンマーシャリングを行い、 スレッドがオブジェクトにアクセスできるためのオブジェクトを内部で作成しています。 インターフェースのマーシャリングとは、オブジェクトとオブジェクトが属するアパートの識別情報をストリームに格納することであり、 これをオブジェクトにアクセスしたいスレッド上でアンマーシャリングすると、 オブジェクトにアクセスするためのオブジェクトが返ることになります。 こうして返されたオブジェクトが前節で述べたプロキシであり、スレッドはこのプロキシのメソッドを呼び出すことで、 透過的に実オブジェクトのメソッドを呼び出しています。 アパート及びリモート間におけるプロトコルは、STAからMTA、MTAからSTAへのアクセスがRPCであり、 STAからSTAへのアクセスはWindowsメッセージが使用されています。

インターフェースのマーシャリングは、スレッドがCoCreateInstanceを呼び出した際に暗黙的に行われることもありますが、 スレッドがCoMarshalInterThreadInterfaceInStreamを呼び出して明示的に行うこともできます。 スレッドが実オブジェクトへのポインタを持っており、 このオブジェクトに他のスレッドからもアクセスできるようにさせたい場合はこの関数を呼び出します。 マーシャリングによって作成されたストリームを受け取ったスレッドは、 CoGetInterfaceAndReleaseStreamを呼び出してアンマーシャリングを行います。 これにより、スレッドはオブジェクトへアクセスするためのプロキシを取得できるようになります。 次に、インターフェースのマーシャリングとアンマーシャリングを行う例を示します。

#include <windows.h>
#include <shlobj.h>

DWORD WINAPI ThreadProc(LPVOID lpParameter);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT      hr;
	HANDLE       hThread;
	IShellLink   *pShellLink;
	IPersistFile *pPersistFile;
	IStream      *pStream;
	
	CoInitialize(NULL);

	hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShellLink));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	pShellLink->SetPath(L"C:\\sample.txt");
	pShellLink->QueryInterface(IID_PPV_ARGS(&pPersistFile));
	pShellLink->Release();
	
	CoMarshalInterThreadInterfaceInStream(IID_IPersistFile, pPersistFile, &pStream);
	
	hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, pStream, 0, NULL);
	MessageBox(NULL, TEXT("ボタンを押すと終了します。"), TEXT("OK"), MB_OK);
	CloseHandle(hThread);

	pPersistFile->Release();

	CoUninitialize();
	
	return 0;
}

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	IStream      *pStream = (IStream *)lpParameter;
	IPersistFile *pPersistFile;
	
	CoInitialize(NULL);

	CoGetInterfaceAndReleaseStream(pStream, IID_PPV_ARGS(&pPersistFile));
	pPersistFile->Save(L"C:\\sample.lnk", TRUE);
	pPersistFile->Release();
	
	CoUninitialize();

	return 0;
}

STAに属するオブジェクトにアクセスできるのは、オブジェクトと同じSTAに属しているスレッドのみです。 よって、本来ならばオブジェクトにアクセスできるのは、オブジェクトを作成したメインスレッドのみであるはずです。 しかし、CoMarshalInterThreadInterfaceInStreamでインターフェースをマーシャリングし、 これを別スレッドに渡してアンマーシャリングすると、 その別スレッドもオブジェクトにアクセスできるようになります。 これらの関係を図で表すと次のようになります。

メインスレッドと別スレッドは共にCoInitializeを呼び出していましたから、それぞれSTAに属することになります。 ただし、最初に作成されたSTAはメインSTAと呼ばれるのが一般的です。 メインスレッドではSTAのオブジェクト(CLSID_ShellLink)を作成していましたから、 そのオブジェクトはメインSTAに属することになり、メインスレッドはオブジェクトに直接アクセスすることができます。 一方、別スレッドはオブジェクトと異なるアパートに存在するため、オブジェクトに直接アクセスすることはできません。 よって、アンマーシャリングによって作成されたプロキシを通じてアクセスすることになります。

メインSTAに属するオブジェクトにアクセスできるのは、オブジェクトと同じアパートに属しているメインスレッドだけであるため、 このメインスレッドにメソッド呼び出しを行わせるための通知を、プロキシは送らなければなりません。 しかし、メインスレッドはメインスレッドで実行したいコードがあるでしょうし、 外部からのメソッド呼び出しをどう受けてどう呼び出せばよいのかという疑問が残ります。 実はスレッドがCoInitializeを呼び出した場合は、非表示のウインドウが作成されることになっており、 プロキシはこのウインドウに対してPostMessageでメッセージを送ります。 メインスレッドがウインドウを表示するGUIスレッドであれば、 このメッセージはメッセージループによって取得できるため、 メッセージの処理はメインスレッドによって行われることになります。 スレッドがメッセージキューからメッセージを取得するという関係上、 次のようなコードには注意する必要があります。

#include <windows.h>
#include <shlobj.h>

HANDLE g_hEvent;

DWORD WINAPI ThreadProc(LPVOID lpParameter);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT      hr;
	HANDLE       hThread;
	IShellLink   *pShellLink;
	IPersistFile *pPersistFile;
	IStream      *pStream;
	
	CoInitialize(NULL);

	hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShellLink));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	pShellLink->SetPath(L"C:\\sample.txt");
	pShellLink->QueryInterface(IID_PPV_ARGS(&pPersistFile));
	pShellLink->Release();
	
	CoMarshalInterThreadInterfaceInStream(IID_IPersistFile, pPersistFile, &pStream);

	g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
	
	hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, pStream, 0, NULL);
	WaitForSingleObject(g_hEvent, INFINITE);
	CloseHandle(hThread);
	
	CloseHandle(g_hEvent);
	pPersistFile->Release();
	CoUninitialize();
	
	return 0;
}

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	IStream      *pStream = (IStream *)lpParameter;
	IPersistFile *pPersistFile;
	
	CoInitialize(NULL);

	CoGetInterfaceAndReleaseStream(pStream, IID_PPV_ARGS(&pPersistFile));
	pPersistFile->Save(L"C:\\sample.lnk", TRUE);
	pPersistFile->Release();

	SetEvent(g_hEvent);
	
	CoUninitialize();

	return 0;
}

このコードではメインスレッドがWaitForSingleObjectで待機することになっており、 それは別スレッドがSetEventを呼び出すまで続きます。 別スレッドがIPersistFile::Saveを呼び出した場合は、メソッドの呼び出し要求がメインスレッドに送られことになりますが、 メインスレッドはSetEventが呼ばれるまで待機するため、メッセージが処理されることはありません。 この場合は、IPersistFile::Saveも制御を返さないことになるため、両スレッドのコードの進行は完全に停止することになります。 こうした問題を防ぐために、オブジェクトをマーシャリングしたストリームを渡すスレッドは、 メッセージループを持ったGUIスレッドでなければなりません。

スレッドが何らかの重要な処理に入る際には、別スレッドからの要求を拒否したり後回しにしたりしたいかもしれません。 このような場合、スレッドはIMessageFilterを実装したオブジェクトを作成し、これをCoRegisterMessageFilterで登録するようにします。 そうすると、別スレッドがメソッドを呼び出した際にIMessageFilter::HandleInComingCallが呼ばれるため、 ここでメソッドの呼び出しに応えるかを返すことができます。

#include <windows.h>
#include <shlobj.h>

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

	STDMETHODIMP_(DWORD) HandleInComingCall(DWORD dwCallType, HTASK htaskCaller, DWORD dwTickCount, LPINTERFACEINFO lpInterfaceInfo);
	STDMETHODIMP_(DWORD) RetryRejectedCall(HTASK htaskCallee, DWORD dwTickCount, DWORD dwRejectType);
	STDMETHODIMP_(DWORD) MessagePending(HTASK htaskCallee, DWORD dwTickCount, DWORD dwPendingType);

	CMessageFilter();
	~CMessageFilter();

private:
	LONG m_cRef;
};

DWORD WINAPI ThreadProc(LPVOID lpParameter);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT        hr;
	HANDLE         hThread;
	IShellLink     *pShellLink;
	IPersistFile   *pPersistFile;
	IStream        *pStream;
	IMessageFilter *pMessageFilter;
	
	CoInitialize(NULL);

	hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShellLink));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	pShellLink->SetPath(L"C:\\sample.txt");
	pShellLink->QueryInterface(IID_PPV_ARGS(&pPersistFile));
	pShellLink->Release();
 
	CoMarshalInterThreadInterfaceInStream(IID_IPersistFile, pPersistFile, &pStream);
	
	pMessageFilter = new CMessageFilter;
	CoRegisterMessageFilter(pMessageFilter, NULL);
	
	hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, pStream, 0, NULL);
	MessageBox(NULL, TEXT("ボタンを押すと終了します。"), TEXT("OK"), MB_OK);
	CloseHandle(hThread);

	pMessageFilter->Release();
	pPersistFile->Release();
	CoUninitialize();
	
	return 0;
}

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	IStream        *pStream = (IStream *)lpParameter;
	IPersistFile   *pPersistFile;
	IMessageFilter *pMessageFilter;
	
	CoInitialize(NULL);

	pMessageFilter = new CMessageFilter;
	CoRegisterMessageFilter(pMessageFilter, NULL);
	
	CoGetInterfaceAndReleaseStream(pStream, IID_PPV_ARGS(&pPersistFile));
	pPersistFile->Save(L"C:\\sample.lnk", TRUE);
	pPersistFile->Release();

	pMessageFilter->Release();
	CoUninitialize();

	return 0;
}

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

CMessageFilter::~CMessageFilter()
{
}

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

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

	AddRef();
	
	return S_OK;
}

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

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

	return m_cRef;
}

STDMETHODIMP_(DWORD) CMessageFilter::HandleInComingCall(DWORD dwCallType, HTASK htaskCaller, DWORD dwTickCount, LPINTERFACEINFO lpInterfaceInfo)
{
	return SERVERCALL_REJECTED;
}

STDMETHODIMP_(DWORD) CMessageFilter::RetryRejectedCall(HTASK htaskCallee, DWORD dwTickCount, DWORD dwRejectType)
{
	int nId;

	nId = MessageBox(NULL, TEXT("メソッド呼び出しが拒否されました。再試行しますか"), TEXT("OK"), MB_YESNO);
	if (nId == IDYES)
		return 0;
	else
		return -1;
}

STDMETHODIMP_(DWORD) CMessageFilter::MessagePending(HTASK htaskCallee, DWORD dwTickCount, DWORD dwPendingType)
{
	return PENDINGMSG_WAITDEFPROCESS;
}

別スレッドがIPersistFile::Saveを呼び出した場合は、メインスレッドのIMessageFilter::HandleInComingCallが呼ばれることになります。 ここでSERVERCALL_ISHANDLEDを返した場合は、実際にメインスレッドがIPersistFile::Saveを呼び出すことになり、 別スレッドのIPersistFile::Saveは制御を返すことになります。 一方、SERVERCALL_REJECTEDやSERVERCALL_RETRYLATERを返した場合は、メソッドは呼び出されないことになります。 もし、このような拒否を別スレッドが検出したいのであれば、別スレッドもCoRegisterMessageFilterを呼び出しておくことになります。 今回は簡単のためメインスレッドが使用したクラスと同じクラスを使用していますが、 実際の開発ではクラスは分けたほうがよいでしょう。 メインスレッドがSERVERCALL_REJECTEDやSERVERCALL_RETRYLATERを返せば、別スレッドのRetryRejectedCallが呼ばれることになり、 戻り値で返した値の時間(ミリ秒)だけ経過したら、再びHandleInComingCallの呼び出しを行うことができます。 ちなみに、0から99までの値を指定した場合はHandleInComingCallが直ちに呼ばれることになります。 一方、-1を返した場合はメソッドの呼び出しは失敗となり、メソッドはRPC_E_CALL_REJECTEDを返すことになります。 RetryRejectedCallの第3引数はHandleInComingCallの戻り値であり、 これがSERVERCALL_REJECTEDの場合は要求の拒否、SERVERCALL_RETRYLATERの場合は処理を後に回すことを意味します。 つまり、第3引数がSERVERCALL_REJECTEDである場合は、再試行してもメソッドを呼び出すことは基本的にありませんし、 上記の場合はHandleInComingCallの戻り値が固定になっているため、メソッドが成功することはありません。 MessagePendingは、別スレッドがメソッドの処理が行われるのを待っている間に、通常のWindowsメッセージが自スレッドに送られた際に呼ばれます。

GITについて

CoMarshalInterThreadInterfaceInStreamを使用するにあたって、注意しなければならない点が2点ほどあります。 1つは、この関数でマーシャリングしたストリームが、CoGetInterfaceAndReleaseStreamで1回しかアンマーシャリングできないという点です。 もし、メインスレッドが複数のスレッドに対してストリームを渡したいとしても、 あるスレッドがCoGetInterfaceAndReleaseStreamを呼び出してしまっては、 もう1つのスレッドはアンマーシャリングできないことになります。 この問題を回避したい場合は、CoMarshalInterThreadInterfaceInStreamではなくCoMarshalInterfaceを呼び出すようにします。 CoMarshalInterfaceはプロセス外のマーシャリングもサポートする関数であり、 MSHLFLAGS_TABLESTRONGというフラグを指定すればアンマーシャリングを複数回行えるようになります。 ちなみに、CoMarshalInterThreadInterfaceInStreamの実装は、 CoMarshalInterfaceにMSHCTX_INPROCとMSHLFLAGS_NORMAL(アンマーシャリング1回)を指定したラッピングです。

CoMarshalInterThreadInterfaceInStreamのもう1つの特徴は、 この関数がプロキシのマーシャリングをサポートしないという点です。 たとえば、あるスレッドがCoGetInterfaceAndReleaseStreamを呼び出してインターフェースを取得し、 それをまた別スレッドに渡したくなったとします。 この場合、そのインターフェースをCoMarshalInterThreadInterfaceInStreamに指定することになりそうですが、 このインターフェースはプロキシを識別しているため、上手くいかないことになります。

アンマーシャリングの回数やプロキシのマーシャリングなどは、 GIT(Global Interface Table)を使用することで気にする必要がなくなります。 GITはプロセス単位で存在し、マーシャリング済みのストリームを複数登録することができます。 この登録時にはインターフェースを識別するCookieが返されることになっており、 このCookieがあればどのスレッドでもアンマーシャリングを行うことができます。


#include <windows.h>
#include <shlobj.h>

DWORD g_dwCookie;

DWORD WINAPI ThreadProc(LPVOID lpParameter);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT               hr;
	HANDLE                hThread;
	IShellLink            *pShellLink;
	IPersistFile          *pPersistFile;
	IGlobalInterfaceTable *pGlobalInterfaceTable;
	
	CoInitialize(NULL);

	hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShellLink));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	pShellLink->SetPath(L"C:\\sample.txt");
	pShellLink->QueryInterface(IID_PPV_ARGS(&pPersistFile));
	pShellLink->Release();

	CoCreateInstance(CLSID_StdGlobalInterfaceTable, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pGlobalInterfaceTable));
	pGlobalInterfaceTable->RegisterInterfaceInGlobal(pPersistFile, IID_IPersistFile, &g_dwCookie);
	
	hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL);
	MessageBox(NULL, TEXT("ボタンを押すと終了します。"), TEXT("OK"), MB_OK);
	CloseHandle(hThread);

	pGlobalInterfaceTable->RevokeInterfaceFromGlobal(g_dwCookie);
	pGlobalInterfaceTable->Release();

	pPersistFile->Release();

	CoUninitialize();
	
	return 0;
}

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	IPersistFile          *pPersistFile;
	IGlobalInterfaceTable *pGlobalInterfaceTable;
	
	CoInitialize(NULL);

	CoCreateInstance(CLSID_StdGlobalInterfaceTable, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pGlobalInterfaceTable));
	pGlobalInterfaceTable->GetInterfaceFromGlobal(g_dwCookie, IID_PPV_ARGS(&pPersistFile));

	pPersistFile->Save(L"C:\\sample.lnk", TRUE);
	pPersistFile->Release();

	pGlobalInterfaceTable->Release();
	CoUninitialize();

	return 0;
}

IGlobalInterfaceTable::RegisterInterfaceInGlobalを呼び出せば、指定したインターフェースをマーシャリングしてGITに登録することができます。 第3引数に返されるCookieは、複数のスレッドが参照できるようにグローバル変数で受け取っています。 別スレッドは、GetInterfaceFromGlobalにCookieを指定しますが、 これによってアンマーシャリングが行われてインターフェースが返ることになります。 IGlobalInterfaceTableはグローバルに定義しても構いませんが、 CLSID_StdGlobalInterfaceTableで取得できるオブジェクトは常に同一であるため、 必要な度にCoCreateInstanceを呼び出しても問題ありません。



戻る