EternalWindows
ホスト API / CLRのロード

アプリケーション開発者は、常にある種の選択を問われているといえます。 たとえば、Windowsアプリケーションを開発することを考えた場合、 C言語でWindows APIを直接呼び出す方法もあれば、 C++言語でMFCというライブラリを使用する方法もあり、 アプリケーションの種類(GUIかどうかなど)に応じて使い分けることができます。 ただし、こうした使い分けというのはあくまで内部的なことであり、 作成されるexeファイル自体にはそこまで違いはありません。 簡単に言えばどちらのexeファイルにも、特定のCPU向けにコンパイルされたネイティブコード(アンマネージコード)が格納されているわけです。 これとは対照的に、Windowsアプリケーションの開発に.NET Frameworkを使用した場合は、 作成されたexeファイルにネイティブコードは格納されません。 格納されているのはマネージコードであり、これはそのままではCPUが実行できるコードではありません。 それではどうやってアプリケーションが動作しているのかというと、 CLR(Common Language Runtime)が実行時にマネージコードをネイティブコードに変換しています。 マネージコードはCLR(及び基本クラスライブラリ)が存在しなければ実行できませんから、 マネージアプリケーションの実行には、.NET Framework(CLRなどを同梱)がインストールされていることが不可欠ということになります。

CLRの正体は、mscorwks.dll(.NET 4ではclr.dll)です。 マネージアプリケーションが起動されると、プロセスのアドレス空間にこのDLLがロードされ、 マネージコードがJITコンパイル(ネイティブコードに変換)されます。 ただし、正確にはJITコンパイルを行っているのはmscorjit.dll(.NET 4ではclrjit.dll)であり、 CLRが行っているのは主にメモリの管理です。 マネージコードで確保されるメモリはマネージヒープという領域に格納されますが、 ここに格納されたメモリはCLRによって適切なタイミングで開放されるため、 アプリケーションが明示的に開放処理を行う必要がなくなります。 このような、メモリの開放のし忘れを意識しなくてもよいというのは、 マネージアプリケーションの大きな特徴であるといえるでしょう。

アプリケーションを開発するにあたって、マネージとアンマネージどちらを選択するかは自由ですが、 アンマネージで開発を行う場合でも、マネージについての知識が必要になることがあります。 たとえば、プロセスを列挙するツールを開発するとなった場合、アンマネージアプリケーションに関しては使用しているAPIを列挙できるけれども、 マネージアプリケーションに関しては使用しているクラスを列挙できないとなっては面白くありませんから、 情報を取得する方法が欲しいものです。 幸いにもWindows SDKには、アンマネージ APIと呼ばれるAPIを定義しているヘッダーファイルが存在するため、 これをインクルードすることでマネージアプリケーションを参照できるようになるでしょう。 アンマネージ APIは、次のようにグループ分けできます。

APIの種類 説明
ホスト API CLRをホストする。CLRをホストするとは、アンマネージアプリケーションからマネージコードを管理したり、機能を使用したりするということである。
メタデータ API マネージモジュール(.NETのexeやdll)が定義しているクラスやメソッドを取得する。
プロファイル API マネージアプリケーションのパフォーマンスやメモリの使用状況を確認する。DLLをプロファイル対象のアプリケーションにロードすることになる。
デバッグ API マネージアプリケーションのデバッグを行う。
シンボル ストア診断 API pdbファイルから情報を取得する。デバッグ APIと共に使用されることが多いと思われる。
ALink API アセンブリの作成に使用すると思われる。alink.hの存在が確認できないため、詳細は不明。

CLRをホストすることで可能になる操作はいくつもあります。 たとえば、.NETの基本クラスライブラリを使用したり、外部開発者が作成した.NET製のアドイン(マネージDLL)を使用したりできます。 また、CLRの既定の動作を拡張し、より高いパフォーマンスを引き出す目的でも使用できます。 ホスト APIを使用している既存のアプリケーションとしては、 Internet ExplorerやMicrosoft SQL Serverが有名です。

マネージコードをwin32プロセス(ホスト)で実行するためには、このプロセスにCLRがロードされなければなりません。 ただし、ホストが最初にロードするDLLはCLRではなく、shim(シム)と呼ばれるmscoree.dllです。 このDLLは、ホストが特定のバージョンのCLRをロードできるための関数を実装しています。 ホストは、次に示すCorBindToRuntimeExを呼び出してCLRをロードすると共に、 CLRを通信するためのインターフェースを取得します。

HRESULT CorBindToRuntimeEx (
  LPWSTR    pwszVersion, 
  LPWSTR    pwszBuildFlavor, 
  DWORD     startupFlags, 
  REFCLSID  rclsid, 
  REFIID    riid, 
  LPVOID*   ppv
);

pwszVersionは、ロードするCLRのバージョンを指定します。 pwszBuildFlavorは、ワークステーションビルドのCLRをロードするか、サーバービルドのCLRをロードするかを表す文字列を指定します。 前者の場合はwksとなり、後者の場合はsvrになります。 シングルプロセッサのコンピューターの場合は、wksを指定するのがよいと思われます。 startupFlagsは、STARTUP_FLAGS列挙型の定数を指定します。 0を指定した場合は、STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAINを指定したものと解釈されます。 rclsidは、CLSID_CLRRuntimeHostまたはCLSID_CorRuntimeHostを指定します。 riidは、rclsidにCLSID_CLRRuntimeHostを指定した場合はIID_ICLRRuntimeHostを指定し、 rclsidにCLSID_CorRuntimeHostを指定した場合はを指定します。 ppvは、オブジェクトを受け取る変数のアドレスを指定します。

CorBindToRuntimeExでは、ICLRRuntimeHostかICorRuntimeHostを取得することになります。 明確な使い分けはよく分かりませんが、 前者は簡単なメソッド呼び出しやCLRの拡張を行う場合に使用し、 後者はAppDomainを操作する場合に使用するのではないかと思われます。 メソッドを簡単に呼び出したい場合は、ICLRRuntimeHost::ExecuteInDefaultAppDomainを呼び出します。

HRESULT ICLRRuntimeHost::ExecuteInDefaultAppDomain (
  LPCWSTR pwzAssemblyPath,
  LPCWSTR pwzTypeName, 
  LPCWSTR pwzMethodName,
  LPCWSTR pwzArgument,
  DWORD *pReturnValue
);

pwzAssemblyPathは、呼び出したいメソッドを格納しているアセンブリへのパスを指定します。 pwzTypeNameは、呼び出したいメソッドを格納するクラスの名前を指定します。 pwzMethodNameは、呼び出したいメソッドを指定します。 pwzArgumentは、メソッドの引数として渡したい文字列を指定します。 メソッドが引数を持たない場合は、NULLで問題ありません。 pReturnValueは、メソッドの戻り値を受け取る変数のアドレスを指定します。

ExecuteInDefaultAppDomainの第4引数はメソッドに渡す引数ですが、 これはLPCWSTR型になっていました。 ということは、メソッドに渡す引数は1つの文字列しか許されないということなのでしょうか。 ExecuteInDefaultAppDomainを呼び出す限りでは、その通りということになります。 メソッドのシグネチャは次のようになっている必要があります。

static int pwzMethodName (String pwzArgument)

上記のようにメソッドの引数は文字列型になっていなければならず、 戻り値の型はintになっていなければなりません。 また、メソッドはstaticで定義している必要があります。 もし、ExecuteInDefaultAppDomainでシグネチャが正しくないメソッドを呼び出した場合は、 COR_E_MISSINGMETHODが返ることになります。

今回のプログラムは、自作のマネージDLLをアンマネージホストから使用します。 まず、C#で記述されたマネージDLLのコードを示します。

using System;

public class Class1
{
    public static int Method1(String arg)
    {
        Random r = new Random();
        
        return r.Next(50);
    }
}

DLLで定義するメソッドは、Windows APIで実装できない機能を持っておくべきといえます。 そうでなければ、最初からホストがその機能を持っていればよいわけですから、 マネージDLLを用意する意味がなくなります。 Randomクラスは乱数を返すことができますが、こうした乱数はWindows APIで扱いにくいため(実際はCryptGenRandomなどで可能)、 今回は乱数を取得するためのメソッドを用意しています。 Nextを呼び出すことによって、第1引数より小さい乱数が得られることになります。 続いて、ホストのコードを示します。

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

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT         hr;
	ICLRRuntimeHost *pRuntimeHost;
	TCHAR           szBuf[256];
	DWORD           dwResult;

	hr = CorBindToRuntimeEx(L"v2.0.50727", L"wks", 0, CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pRuntimeHost));
	if (FAILED(hr)) {
		wsprintf(szBuf, TEXT("CorBindToRuntimeEx : %x"), hr);
		MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
		return 0;
	}
	
	pRuntimeHost->Start();

	hr = pRuntimeHost->ExecuteInDefaultAppDomain(L"ClassLibrary1.dll", L"Class1", L"Method1", NULL, &dwResult);
	if (FAILED(hr)) {
		if (hr == COR_E_NEWER_RUNTIME)
			MessageBox(NULL, TEXT("バージョンが一致しません。"), NULL, MB_ICONWARNING);
		else {
			wsprintf(szBuf, TEXT("ExecuteInDefaultAppDomain : %x"), hr);
			MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
		}
		pRuntimeHost->Stop();
		pRuntimeHost->Release();
		return 0;
	}

	wsprintf(szBuf, TEXT("乱数 %d"), dwResult);
	MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);

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

	return 0;
}

mscoree.hは、CorBindToRuntimeExやICLRRuntimeHostを定義しているため、必ずインクルードするようにします。 corerror.hは必須ではありませんが、COR_E_NEWER_RUNTIMEなどのエラー定数を定義しているため、それを参照したい場合はインクルードします。 CorBindToRuntimeExの第1引数から分かるように、バージョン2のCLRをロードしようとしています。 当然ながらこのためには.NET 2.0がインクルードされている必要があります。 第2引数はwksでよく、第3引数は0で構いません。 第4引数にCLSID_CLRRuntimeHostを指定していたため、 ICLRRuntimeHostを取得することになります。

CorBindToRuntimeExが成功した時点で、特定のバージョンのCLRがホストのアドレス空間にロードされます。 ただし、CLRは既定で初期化されていないため、ICLRRuntimeHost::Startで初期化するようにします。 これが成功すれば、ICLRRuntimeHost::ExecuteInDefaultAppDomainでメソッド呼び出しを行えます。

hr = pRuntimeHost->ExecuteInDefaultAppDomain(L"ClassLibrary1.dll", L"Class1", L"Method1", NULL, &dwResult);
if (FAILED(hr)) {
	if (hr == COR_E_NEWER_RUNTIME)
		MessageBox(NULL, TEXT("バージョンが一致しません。"), NULL, MB_ICONWARNING);
	else {
		wsprintf(szBuf, TEXT("ExecuteInDefaultAppDomain : %x"), hr);
		MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
	}
	pRuntimeHost->Stop();
	pRuntimeHost->Release();
	return 0;
}

ExecuteInDefaultAppDomainを呼び出すとmscorjit.dllがロードされ、Method1がJITコンパイルされることになります。 このメソッドは正しいシグネチャを持っていたため、問題なく呼び出せるはずです。 第1引数はDLLのパスを指定しますが、ファイル名だけを指定した場合はexeと同じディレクトリが検索されます。 カレントディレクトリが対象でないことに注意してください。 第4引数はメソッドに渡したい文字列を指定できますが、今回のメソッドはそれを必要としていないためNULLを指定しています。 COR_E_NEWER_RUNTIMEが返った場合は、CLRがDLLのバージョンより低い可能性があります。 たとえば、.NET 2.0のCLRで.NET 4のDLLをロードしようとした場合に発生します。 DLLのバージョンを確認したい場合は、フルパスを指定してGetFileVersionを呼び出します。

ExecuteInDefaultAppDomainが行っている動作を正確に述べると、第1引数のアセンブリを既定のAppDomainにロードするということになります。 アセンブリとは極めて単純に解釈すればEXEやDLLのことであり、 しかるべきシグネチャのメソッドを持っているのであれば、 EXEでもExecuteInDefaultAppDomainに指定できます。 AppDomainにアセンブリがロードされるという意味については後の節で説明しますが、 この動作がアドレス空間へのマッピングに直結するとは限らないことに注意してください。 たとえば、.NET 2.0ではExecuteInDefaultAppDomainに指定したアセンブリは、メソッドが制御を返してもアドレス空間にマッピングされていますが、 .NET 4ではマッピングされていません。 言い換えれば、アセンブリをGetModuleHandleに指定しても、有効なアドレスは返りません。 この.NETの機能はWin32のこれに相当するという考え方は、.NETの理解を進めるうえで重要ですが、 それが将来のバージョンにおいても共通しているとは限りません。

.NET 4の機能

ここでは、.NET 4から追加されたICLRMetaHostとICLRRuntimeInfoについて説明します。 ICLRRuntimeInfoは、1つのランタイム(CLR)に関する情報を管理しています。 次に、システムに存在するランタイムを列挙する例を示します。

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

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT         hr;
	ICLRMetaHost    *pMetaHost;
	IEnumUnknown    *pEnumUnknown;
	IUnknown        *pUnknown;
	ICLRRuntimeInfo *pRuntimeInfo;
	WCHAR           szBuf[MAX_PATH];
	DWORD           dwSize = MAX_PATH;

	hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
	if (FAILED(hr))
		return 0;

	pMetaHost->EnumerateInstalledRuntimes(&pEnumUnknown);

	while (pEnumUnknown->Next(1, &pUnknown, NULL) == S_OK) {
		pUnknown->QueryInterface(IID_PPV_ARGS(&pRuntimeInfo));
		pRuntimeInfo->GetRuntimeDirectory(szBuf, &dwSize);
		MessageBoxW(NULL, szBuf, L"OK", MB_OK);
		pUnknown->Release();
	}

	pEnumUnknown->Release();
	pMetaHost->Release();

	return 0;
}

CLRCreateInstanceにCLSID_CLRMetaHostを指定すれば、ICLRMetaHostを取得できます。 EnumerateInstalledRuntimesは、現在インストールされているランタイムを列挙するためのIEnumUnknownを返し、 Nextで取得できるIUnknownからはICLRRuntimeInfoを照会できます。 このインターフェースはランタイムの情報を管理していますから、 GetRuntimeDirectoryでランタイムのディレクトリを取得できます。

.NET 4のインターフェースを使用してメソッドを呼び出したい場合は、次のようになります。

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

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT         hr;
	ICLRRuntimeHost *pRuntimeHost;
	TCHAR           szBuf[256];
	DWORD           dwResult;
	ICLRMetaHost    *pMetaHost;
	ICLRRuntimeInfo *pRuntimeInfo;

	hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
	if (FAILED(hr))
		return 0;

	pMetaHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&pRuntimeInfo));
	pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pRuntimeHost));
	pRuntimeInfo->Release();
	pMetaHost->Release();
	
	pRuntimeHost->Start();

	hr = pRuntimeHost->ExecuteInDefaultAppDomain(L"ClassLibrary1.dll", L"Class1", L"Method1", NULL, &dwResult);
	if (FAILED(hr)) {
		if (hr == COR_E_NEWER_RUNTIME)
			MessageBox(NULL, TEXT("バージョンが一致しません。"), NULL, MB_ICONWARNING);
		else {
			wsprintf(szBuf, TEXT("ExecuteInDefaultAppDomain : %x"), hr);
			MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
		}
		pRuntimeHost->Stop();
		pRuntimeHost->Release();
		return 0;
	}

	wsprintf(szBuf, TEXT("乱数 %d"), dwResult);
	MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);

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

    return 0;
}

ICLRMetaHost::GetRuntimeを呼び出せば、第1引数のバージョンのランタイムを表すICLRRuntimeInfoを取得できます。 GetInterfaceにCLSID_CLRRuntimeHostを指定すれば、ICLRRuntimeHostを取得できますが、 この時点でランタイムがロードされることになります。



戻る