EternalWindows
ホスト API / ホストとアドイン

アプリケーションの機能を動的に拡張する方法として、 外部開発者が作成したアドインをロードする方法はよく知られています。 アプリケーションがC/C++で記述されたネイティブアプリケーションである場合は、 ロードするDLLもC/C++で記述されたものが多いと思われますが、 CLRのホスティングを行う場合はマネージDLLをロードすることもできます。 この場合の最大の利点は、DLL内で未処理の例外が発生しても、ホストは強制終了しなくて済むというものです。 メソッドを通じたマネージコードの実行は、コード内で発生した例外がCCWによって検出されるため、 戻り値を通じてメソッドの成否を確認できるようになります。 コードが堅牢に設計されているか分からない外部のDLLでは、 例外を処理していないことも十分考えられますから、 この機能は非常に強力であるといえます。

ホストからアドインの機能を使用するにあたり、ICLRRuntimeHost::ExecuteInDefaultAppDomainを使用するのは好ましくありません。 この場合、アドインが用意するメソッドは、ExecuteInDefaultAppDomainが想定しているシグネチャと一致させなければならなくなり、 柔軟性に欠けることになります。 それではどうするのかというと、_AppDomain::CreateInstanceFromでアドインを表すオブジェクトを作成し、 これを独自に定義したCOMインターフェースで識別します。 これならば、メソッドは自由に決定することができます。 インターフェースの定義は、IDLファイルを通じて行います。

import "oaidl.idl";
import "ocidl.idl";

[
	object,
	uuid(8D2AA0D1-7B68-4b09-B857-16C2869A572E)
]
interface IHostAccess : IUnknown
{
	HRESULT ShowText(BSTR bstrText);
}

[
	object,
	uuid(21247B24-AB66-446c-A12E-2B7EAA2E1F36)
]
interface IAddIn : IUnknown
{
	HRESULT Initialize([in] IHostAccess *pHostAccess, BSTR bstrHostName);
	HRESULT Destroy();
}

まず、IAddInというインターフェースについて説明します。 このインターフェースはホストがアドインのオブジェクトを識別するためのもので、 _AppDomain::CreateInstanceFromから返されるオブジェクト(正確にはそれをUnwrapしたオブジェクト)が実装しています。 Initializeは、アドインに初期化の機会を与えるメソッドであり、 第1引数のIHostAccessはアドインがホストを識別するためのインターフェースです。 つまり、ホストがアドインの機能を使用するだけでなく、アドインもホストの機能を使用できるようにしています。 第2引数はホストの名前であり、任意の文字列を指定できます。 ホストは、このIDLファイルをコンパイルすることによって作成されたヘッダーファイルをインクルードします。 続いて、今回のアドインのコードを示します。

using System;
using System.Runtime.InteropServices;

[ComVisible(true), Guid("8D2AA0D1-7B68-4b09-B857-16C2869A572E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IHostAccess
{
    void ShowText([MarshalAs(UnmanagedType.BStr)]String s);
}

[ComVisible(true), Guid("21247B24-AB66-446c-A12E-2B7EAA2E1F36"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IPlugIn
{
    void Initialize(IHostAccess ha, [MarshalAs(UnmanagedType.BStr)]String s);
    void Destroy();
}

public class Class1 : IPlugIn
{
    static IHostAccess hostAccess;

    void IPlugIn.Initialize(IHostAccess ha, string s)
    {
        hostAccess = ha;
      
        hostAccess.ShowText(s);
    }

    void IPlugIn.Destroy()
    {
    }
}

ホストの通信を行うために、ホストのIDLファイルに記述されたインターフェースと同じものを定義します。 ホストからは渡されるBSTR型はマネージコードで扱うためにはマーシャリング必要であるため、 [MarshalAs(UnmanagedType.BStr)]を指定しています。 こうしたマーシャリングオプションの詳細については、「既定のマーシャリングの動作」という語句で検索すれば分かります。 Initializeでは、ホストから渡されたと文字列をstaticフィールドに保存しています。 このメソッドでは本来ならば初期化処理を行うべきですが、 今回はそうした処理がないため、IHostAccess::ShowTextを呼び出すだけにしています。 このメソッドはホストに第1引数の文字列を渡すためのもので、 上記では現在のスレッドが実行されているAppDomainのIDにしています。 ちなみに、このアドインを.NETアプリケーションから使用する場合は、 クラスがMarshalByRefObjectを継承しているのは、 続いて、ホストのコードを示します。

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

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

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

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

	STDMETHODIMP ShowText(BSTR bstrText);

private:
	LONG m_cRef;
};

BOOL CreateObject(_AppDomain *pAppDomain, IAddIn **ppPlugIn);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT         hr;
	ICorRuntimeHost *pRuntimeHost;
	IUnknown        *pUnknown;
	_AppDomain      *pAppDomain;
	IAddIn          *pAddIn;
	BSTR            bstrHostName;
	IHostAccess     *pHostAccess;

	hr = CorBindToRuntimeEx(L"v2.0.50727", L"wks", 0, CLSID_CorRuntimeHost, IID_PPV_ARGS(&pRuntimeHost));
	if (FAILED(hr))
		return 0;
	
	pRuntimeHost->Start();
	
	pRuntimeHost->CreateDomain(L"ad2", NULL, &pUnknown);
	pUnknown->QueryInterface(IID_PPV_ARGS(&pAppDomain));
	pUnknown->Release();

	if (!CreateObject(pAppDomain, &pAddIn)) {
		pRuntimeHost->Stop();
		pRuntimeHost->Release();
		return 0;
	}
	
	pHostAccess = new CHostAccess;

	bstrHostName = SysAllocString(L"asd");
	pAddIn->Initialize(pHostAccess, bstrHostName);
	SysFreeString(bstrHostName);
	
	pAddIn->Destroy();
	
	pRuntimeHost->UnloadDomain(pAppDomain);
	pAppDomain->Release();
	pAddIn->Release();
	pHostAccess->Release();
	pRuntimeHost->Stop();
	pRuntimeHost->Release();

	return 0;
}

BOOL CreateObject(_AppDomain *pAppDomain, IAddIn **ppPlugIn)
{
	HRESULT       hr;
	BSTR          bstrAssemblyFile;
	BSTR          bstrTypeName;
	VARIANT       var;
	_ObjectHandle *pObjectHandle;
	
	bstrAssemblyFile = SysAllocString(L"ClassLibrary1.dll");
	bstrTypeName = SysAllocString(L"Class1");
	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(__uuidof(IAddIn), (void **)ppPlugIn);
	VariantClear(&var);

	return SUCCEEDED(hr);
}


// CHostAccess


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

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

	AddRef();
	
	return S_OK;
}

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

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

	return m_cRef;
}

STDMETHODIMP CHostAccess::ShowText(BSTR bstrText)
{
	MessageBoxW(NULL, bstrText, L"OK", MB_OK);
	
	return S_OK;
}

アドインのオブジェクトを作成するには、_AppDomain::CreateInstanceFromを呼び出さなければなりませんが、 今回はこの_AppDomainをICorRuntimeHost::GetDefaultDomainで取得するのではなく、 ICorRuntimeHost::CreateDomainで作成しています。 独自のAppDomainを作成しているのは、不要になったアセンブリをいつでもアンロードできるようにするためです。 _AppDomain::CreateInstanceFromの第1引数に指定している"ClassLibrary1.dll"が、アドインのDLLになります。 フルパスを指定していない場合は、カレントディレクトリを基準に検索されます。

Destroyでアドインに破棄処理を行わせたら、AppDomainをアンロードしても問題ありません。 これにより、AppDomainにロードされているアセンブリがアンロードされることになります。 ただ、正確には、UnloadDomainの時点でアンロードが行われるのではなく、 UnloadDomainの後のReleaseでアンロードが行われるようです。

IAppDomainSetupについて

独自のAppDomainを作成してそこにアセンブリをロードすることは、 AppDomainを通じてアセンブリをアンロードできる利点があります。 この他に独自のAppDomainを作成する理由としては、 個別のセキュリティ(証拠)を設定する場合や、構成情報を設定する場合が挙げられます。 構成情報を設定するには、IAppDomainSetupを使用します。

IUnknown        *pUnknown;
IAppDomainSetup *pAppDomainSetup;

pRuntimeHost->CreateDomainSetup(&pUnknown);
pUnknown->QueryInterface(IID_PPV_ARGS(&pAppDomainSetup));

pAppDomainSetup->put_ApplicationBase(bstrPath);

pRuntimeHost->CreateDomainEx(L"ad2", pAppDomainSetup, NULL, &pUnknown);
pUnknown->QueryInterface(__uuidof(_AppDomain), (void **)&pAppDomain);

ICorRuntimeHost::CreateDomainSetupによって取得できるIUnknownからは、IAppDomainSetupを照会できます。 これのput_ApplicationBaseを呼び出せば、アセンブリの検索に使用されるベースディレクトリを変更できます。 _AppDomain::get_BaseDirectoryを呼び出せば、実際にベースディレクトリが変更されていることが分かります。 IAppDomainSetupを設定する場合は、CreateDomainではなくCreateDomainExを呼び出します。

CreateDomainExの第3引数には、証拠を識別するIUnknownを指定します。 これはICorRuntimeHost::CreateEvidenceで取得でき、IIdentityを照会できるとされています。 しかし、実際にはIIdentityを照会できず、_Evidenceの照会ならば成功するようです。 ただし、このインターフェースは具体的なメソッドを持っておらず、 メソッドを呼び出しはIDispatchを行うことになるでしょう。



戻る