EternalWindows
ホスト API / AppDomainとアセンブリ

Windowsで何らかのexeファイルを起動した場合、そのexeファイルのコードを実行するためのプロセスが作成され、 プロセスのアドレス空間にexeファイルがマッピングされます。 このようなプロセスという単位を使用しているのは、あるexeファイルの実行が他のexeファイルの実行に影響を与えないようにするためです。 C/C++のようなネイティブコードは、ポインタを通じて任意のアドレスにアクセスすることが許されているため、 複数のexeファイルが1つのアドレス空間を共有してしまっては、片方のデータをもう片方が破壊してしまう可能性があるのです。 しかし、CLRの管理下で動作するマネージコードは、コンパイル段階においてタイプセーフ(型に対して正しくアクセスすること)が原則保障されるため、 複数のexeファイルが1つのアドレス空間を共有しても、問題は起きにくいことになります。 このようなことからCLRでは、マネージアプリケーションを単一のプロセスではなく、 既存プロセスの中で実行させることができるようになっています。

bstrAssemblyFile = SysAllocString(L"C:\\manage.exe");
pAppDomain->ExecuteAssembly(bstrAssemblyFile, NULL, NULL);

ExecuteAssemblyは、exe形式のアセンブリを実行する場合に呼び出します。 メソッドが成功した場合は、exeのMainメソッドが呼ばれることになりますが、 その際にタスクマネージャなどでプロセスを列挙しても、 ExecuteAssemblyに指定した名前は含まれないはずです。 また、ホストが動作している間は、exeファイルを削除できないはずです。 こうしたことから、exeファイルのマネージコードがホストの中で動作していることは間違いないといえるでしょう。 ただ、ここで少し気になるのは、このコードがどういった環境下で実行されるのかという点です。 たとえば、コードはどのようなセキュリティコンテキスト(権限)で実行されているのでしょうか。 また、コードで何らかのアセンブリを検索しようとした場合は、どのようなディレクトリが対象になるのでしょうか。 つまり、ホストの中でexeを実行させたいのであれば、そのexeのための環境を用意してあげなければならないということなのですが、 そうした環境を定義するのがAppDomainです。 AppDomainの中には複数のアセンブリをロードでき、アセンブリは自分がロードされているAppDomainの環境下でコードを実行します。

ホストがICorRuntimeHost::Startを呼び出した場合、既定のAppDomainが作成されます。 このAppDomainを取得するのがICorRuntimeHost::GetDefaultDomainであり、 先のコードではこのAppDomainのExecuteAssemblyを呼び出していたため、 アセンブリは既定のAppDomainにロードされたことになります。 既定のAppDomainの環境はホストの環境と同一であり、 ロードされたアセンブリはホストと同じ環境で動作することになりますが、 場合によってはこれが好ましくないこともあります。 たとえばIEでは、Webサイトからダウンロードしたアセンブリを別のドメインにロードし、 低いセキュリティを与えることで問題を発生しにくくしています。

AppDomainにアセンブリをロードした場合、そのAppDomainがアンロードされるまでアセンブリはアンロードされません。 つまり、アセンブリは単一でアンロードすることができず、常にAppDomainと共にアンロードされます。 このようになっているのは、アセンブリのアンロードによってAppDomain内で実行しているスレッドがエラーを起こさないようするためです。 既定のAppDomainはホストが終了するまでアンロードされないため、 任意のタイミングでアセンブリをアンロードしたい場合は、独自のAppDomainを作成するようにします。 そして、アセンブリが不要になった時点でAppDomainをアンロードします。

今回のプログラムは、AppDomainにアセンブリがロードされていることを証明します。 まず、マネージアプリケーションのコードを示します。

using System;
using System.Windows.Forms;

static class Program
{
    static void Main()
    {
        MessageBox.Show("Class1");
    }
}

System.Windows.Formsの宣言から分かるように、これはWindowsアプリケーションのコードです。 このような場合はフォームを表示することが多いですが、今回はメッセージボックスを表示するだけにしています。 続いて、ホストのコードを示します。

#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")

void EnumAssemblies(_AppDomain *pAppDomain);

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

	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();

	bstrAssemblyFile = SysAllocString(L"C:\\manage.exe");
	hr = pAppDomain->ExecuteAssembly(bstrAssemblyFile, NULL, NULL);
	SysFreeString(bstrAssemblyFile);
	if (FAILED(hr)) {
		MessageBox(NULL, TEXT("アセンブリの実行に失敗しました。"), TEXT("OK"), MB_OK);
		pAppDomain->Release();
		pRuntimeHost->Stop();
		pRuntimeHost->Release();
		return 0;
	}
	
	EnumAssemblies(pAppDomain);

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

	return 0;
}

void EnumAssemblies(_AppDomain *pAppDomain)
{
	_Assembly *pAssembly;
	IUnknown  *pElement;
	LONG      lIndexMin, lIndexMax;
	SAFEARRAY *pArray;
	LONG      i;
	BSTR      bstr;

	pAppDomain->GetAssemblies(&pArray);
	if (pArray == NULL)
		return;
			
	SafeArrayGetLBound(pArray, 1, &lIndexMin);
	SafeArrayGetUBound(pArray, 1, &lIndexMax);

	for (i = lIndexMin; i <= lIndexMax; i++) {
		SafeArrayGetElement(pArray, &i, (void *)&pElement);
		pElement->QueryInterface(__uuidof(_Assembly), (void **)&pAssembly);

		pAssembly->get_ToString(&bstr);
		MessageBoxW(NULL, bstr, L"OK", MB_OK);

		SysFreeString(bstr);
		pElement->Release();
		pAssembly->Release();
	}
	
	SafeArrayDestroy(pArray);
}

まず、CorBindToRuntimeExでICorRuntimeHostを取得し、StartでCLRを開始します。 この時点で既定のAppDomainが作成されているため、GetDefaultDomainで取得し、 ExecuteAssemblyでアセンブリをロードかつ実行します。 第2引数はセキュリティの証拠を指定できますが、NULLで問題ありません。 第3引数はエントリポイントの戻り値を受け取る変数のアドレスを指定します。 不要な場合はNULLで問題ありません。 EnumAssembliesという自作関数は、第1引数のAppDomainにロードされたアセンブリを列挙します。

void EnumAssemblies(_AppDomain *pAppDomain)
{
	_Assembly *pAssembly;
	IUnknown  *pElement;
	LONG      lIndexMin, lIndexMax;
	SAFEARRAY *pArray;
	LONG      i;
	BSTR      bstr;

	pAppDomain->GetAssemblies(&pArray);
	if (pArray == NULL)
		return;
			
	SafeArrayGetLBound(pArray, 1, &lIndexMin);
	SafeArrayGetUBound(pArray, 1, &lIndexMax);

	for (i = lIndexMin; i <= lIndexMax; i++) {
		SafeArrayGetElement(pArray, &i, (void *)&pElement);
		pElement->QueryInterface(__uuidof(_Assembly), (void **)&pAssembly);

		pAssembly->get_ToString(&bstr);
		MessageBoxW(NULL, bstr, L"OK", MB_OK);

		SysFreeString(bstr);
		pElement->Release();
		pAssembly->Release();
	}
	
	SafeArrayDestroy(pArray);
}

GetAssembliesを呼び出せば、ロードされているアセンブリを格納した配列を取得できます。 この配列はSAFEARRAY型で表すことができ、SafeArrayGetLBoundで要素の先頭インデックス、 SafeArrayGetUBoundで最終要素のインデックスを取得できます。 よって、これらの範囲内でSafeArrayGetElementを呼び出すことにより、配列に格納された要素を受け取ることができます。 要素をIUnknownで識別できることは、SafeArrayGetVartypeがVT_UNKNOWNを返すことから分かります。 このIUnknownからはアセンブリを表す_Assemblyを取得でき、get_ToStringを呼び出せばアセンブリの名前を取得できます。

列挙されるアセンブリには、ExecuteAssemblyに指定したアセンブリ以外に、 mscorlibが含まれているはずです。 これは、.NETの基本的な機能を実装しているアセンブリで、必ずAppDomainにロードされます。 今回実行したアセンブリはWindowsアプリケーションだったため、 System.Windows.Formsなどのアセンブリも含まれることになります。


戻る