EternalWindows
ホスト API / マネージオブジェクトの作成

前節では、CLRのホスティングを通じて、マネージコードのDLLを使用することに成功しました。 これにより、ネイティブアプリケーションから.NETの機能を使用できるようになったわけですが、 .NETの機能を使用するために、マネージコードのDLLを用意しておくのは煩わしいことでもあります。 できることなら、ホストから直接.NETの機能を使用したいところですが、実はこれが非常に難しいのです。 たとえば、ExecuteInDefaultAppDomainで呼び出すメソッドは、 次のようなシグネチャを持っている必要がありました。

static int pwzMethodName (String pwzArgument)

このようにシグネチャが限定されているというのは、非常に強い制約であるといえます。 型が1つでも違う場合はメソッドを呼び出すことはできませんから、 次のようなメソッドも対象外になります。

static DialogResult Show (string text)

これは、System.Windows.Forms名前空間に定義されているMessageBoxクラスのShowメソッドです。 staticが付いている点と引数の型がstringであることは条件と一致しますが、 戻り値の型がDialogResultになっているため、呼び出しても失敗することになります。 このようなことから分かるように、ExecuteInDefaultAppDomainで既存の機能を使用することは多くの場合失敗します。

クラスの静的メソッドを呼び出すのではなく、クラスのオブジェクトを作成し、それのインスタンスメソッドを呼び出す方法があります。 クラスが引数無しのコンストラクタを持っている場合は、これが可能です。

BOOL CreateObject(_AppDomain *pAppDomain, _Random **ppRandom)
{
	HRESULT       hr;
	BSTR          bstrAssemblyFile;
	BSTR          bstrTypeName;
	VARIANT       var;
	_ObjectHandle *pObjectHandle;

	bstrAssemblyFile = SysAllocString(L"C:\\Windows\\Microsoft.NET\\Framework\\v2.0.50727\\mscorlib.dll");
	bstrTypeName = SysAllocString(L"System.Random");
	hr = pAppDomain->CreateInstanceFrom(bstrAssemblyFile, bstrTypeName, &pObjectHandle);
	SysFreeString(bstrTypeName);
	SysFreeString(bstrAssemblyFile);
	if (FAILED(hr))
		return FALSE;

	hr = pObjectHandle->Unwrap(&var);
	pObjectHandle->Release();
	if (FAILED(hr))
		return FALSE;

	hr = var.pdispVal->QueryInterface(IID_PPV_ARGS(ppRandom));
	VariantClear(&var);

	return SUCCEEDED(hr);
}

_AppDomainは、AppDomainを識別するインターフェースです。 AppDomainの詳細については次節で説明します。 CreateInstanceFromを呼び出すと、第1引数のアセンブリから第2引数のクラスのオブジェクトが作成されます。 ただし、このオブジェクトはマネージオブジェクトであるため、 ネイティブアプリケーションが直接使用することはできません。 よって、_ObjectHandle::Unwrapでオブジェクトをアクセスするためのプロキシを取得するようにします。 このプロキシはオブジェクトへアクセスするためのインターフェースを実装しているため、 作成したオブジェクトがSystem.Randomならば、_Randomというインターフェースを取得できます。 さて、後はこの_Randomを使用すれば乱数を取得できるように思えますが、 実を言うとこのインターフェースは具体的なメソッドを一切持っていません。 つまり、pRandom->Nextのようにして乱数を取得することはできません。 ただし、_RandomはIDispatchを継承しているため、 IDispatchを通じて動的にメソッドを呼び出すことは可能です。 具体的には、IDispatch::GetIDsOfNamesでメソッドのDISPIDを取得し、 それをIDispatch::Invokeに指定します。 ちなみに、System.Runtime.InteropServices名前空間で定義されているインターフェース(_Assemblyなど)に関しては、 適切なメソッドを持つ場合があります。

COMクライアント(ホスト)がマネージオブジェクトへアクセスするために使用するプロキシは、 正確にはCCW(COM Callable Wrapper)と呼ばれています。 CCWの役割は、COMクライアントとマネージオブジェクトの間で相互運用マーシャリングを行い、実際にマネージオブジェクトのメソッドを呼び出すことです。 相互運用マーシャリングとは、アンマネージヒープとマネージヒープの間で、メソッドのデータを適切に受け渡すことを意味します。 一方、マネージオブジェクトがCOMクライアントへの参照を持っている場合、 それはRCW(Runtime Callable Wrapper)と呼ばれるプロキシになります。 RCWは、マネージオブジェクトとCOMクライアントの間で相互運用マーシャリングを行い、実際にCOMクライアントのメソッドを呼び出します。 COMの世界では、アパート間をまたいでオブジェクトへアクセスする際にマーシャリングが行われますが、 この動作はCOMマーシャリングと呼ばれています。 対象となるオブジェクトが呼び出し側スレッドと異なるアパートに属している場合は、 COMマーシャリングが行われた後に相互運用マーシャリングが行われます。

今回のプログラムは、System.Randomオブジェクトを作成してNextメソッドを呼び出しています。

#include <windows.h>
#include <mscoree.h>
#include <corerror.h>

#import "C:\\Windows\\Microsoft.NET\\Framework\\v2.0.50727\\mscorlib.tlb" raw_interfaces_only	
using namespace mscorlib;

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

BOOL CreateObject(_AppDomain *pAppDomain, _Random **ppRandom);
HRESULT Invoke(IDispatch *pDispatch, LPOLESTR lpszName, WORD wFlags, VARIANT *pVarArray, int nArgs, VARIANT *pVarResult);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT         hr;
	ICorRuntimeHost *pRuntimeHost;
	_AppDomain      *pAppDomain;
	_Random         *pRandom;
	IUnknown        *pUnknown;
	VARIANT         varResult;
	TCHAR           szBuf[256];

	hr = CorBindToRuntimeEx(L"v2.0.50727", L"wks", 0, CLSID_CorRuntimeHost, IID_PPV_ARGS(&pRuntimeHost));
	if (FAILED(hr))
		return 0;

	pRuntimeHost->Start();

	pRuntimeHost->GetDefaultDomain(&pUnknown);
	pUnknown->QueryInterface(IID_PPV_ARGS(&pAppDomain));
	pUnknown->Release();

	if (!CreateObject(pAppDomain, &pRandom)) {
		pAppDomain->Release();
		pRuntimeHost->Stop();
		pRuntimeHost->Release();
		return 0;
	}

	hr = Invoke(pRandom, L"Next", DISPATCH_METHOD, NULL, 0, &varResult);
	if (SUCCEEDED(hr)) {
		wsprintf(szBuf, TEXT("乱数 %d"), varResult.lVal);
		MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
	}
	else {
		wsprintf(szBuf, TEXT("メソッドの呼び出しに失敗しました。 %x"), hr);
		MessageBox(NULL, szBuf, NULL, MB_ICONWARNING);
	}

	pRandom->Release();
	pAppDomain->Release();
	pRuntimeHost->Stop();
	pRuntimeHost->Release();

	return 0;
}

BOOL CreateObject(_AppDomain *pAppDomain, _Random **ppRandom)
{
	HRESULT       hr;
	BSTR          bstrAssemblyFile;
	BSTR          bstrTypeName;
	VARIANT       var;
	_ObjectHandle *pObjectHandle;

	bstrAssemblyFile = SysAllocString(L"C:\\Windows\\Microsoft.NET\\Framework\\v2.0.50727\\mscorlib.dll");
	bstrTypeName = SysAllocString(L"System.Random");
	hr = pAppDomain->CreateInstanceFrom(bstrAssemblyFile, bstrTypeName, &pObjectHandle);
	SysFreeString(bstrTypeName);
	SysFreeString(bstrAssemblyFile);
	if (FAILED(hr))
		return FALSE;

	hr = pObjectHandle->Unwrap(&var);
	pObjectHandle->Release();
	if (FAILED(hr))
		return FALSE;

	hr = var.pdispVal->QueryInterface(IID_PPV_ARGS(ppRandom));
	VariantClear(&var);

	return SUCCEEDED(hr);
}

HRESULT Invoke(IDispatch *pDispatch, LPOLESTR lpszName, WORD wFlags, VARIANT *pVarArray, int nArgs, VARIANT *pVarResult)
{
	DISPPARAMS dispParams;
	DISPID     dispid;
	DISPID     dispidName = DISPID_PROPERTYPUT;
	HRESULT    hr;
	
	hr = pDispatch->GetIDsOfNames(IID_NULL, &lpszName, 1, LOCALE_USER_DEFAULT, &dispid);
	if (FAILED(hr))
		return hr;
	
	dispParams.cArgs = nArgs;
	dispParams.rgvarg = pVarArray;
	if (wFlags & DISPATCH_PROPERTYPUT) {
		dispParams.cNamedArgs = 1;
		dispParams.rgdispidNamedArgs = &dispidName;
	}
	else {
		dispParams.cNamedArgs = 0;
		dispParams.rgdispidNamedArgs = NULL;
	}

	hr = pDispatch->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, wFlags, &dispParams, pVarResult, NULL, NULL);
	
	return hr;
}

_AppDomainや_Randomという型を使用するには、mscorlib.tlbというタイプライブラリをインポートしなければなりません。 これにより、mscorlib.tlbの中身を基に各種定義が記述されたヘッダーファイルが生成され、それが自動でインクルードされます。 using namespace mscorlibは必須ではありませんが、 これを記述することでmscorlib::_AppDomainという冗長な宣言を防ぐことができます。 今回のCorBindToRuntimeExでは、ICLRRuntimeHostではなくICorRuntimeHostを取得していますが、 これは前者のメソッドにAppDomainを取得するメソッドがないからです。 ICorRuntimeHost::GetDefaultDomainを呼び出せば既定のAppDomainを表すIUnknownを取得でき、 ここから_AppDomainを照会できます。 ちなみに、IID_AppDomainのようなIIDは定義されていないため、_AppDomainのIIDを知りたい場合は__uuidof(_AppDomain)のように記述します。

CreateObjectでSystem.Randomオブジェクトを作成したら、このNextメソッドを呼び出します。 既に述べたようにこれは、IDispatchのGetIDsOfNamesとInvokeで行えるため、その2つの呼び出しをラッピングしたInvokeという関数を用意しています。 第2引数はメソッドまたはプロパティの名前であり、第3引数は第2引数がメソッドの場合はDISPATCH_METHODになります。 第4引数と第5引数は引数とその数であり、第6引数が戻り値になります。 リファレンス上ではNextメソッドの戻り値はint型であり、varResultのvtメンバはVT_I4になります。 この場合は、lValから数値を参照することになります。 ちなみに、引数を指定したNextの呼び出しは、何故かDISP_E_BADPARAMCOUNTで失敗してしまいます。


戻る