EternalWindows
メタデータ API / 型の検索

.NETで作成されたモジュール(exeやdll)には、 マネージコードの他にメタデータと呼ばれる領域が格納されています。 メタデータには、ソースファイルで定義していた型や、型に含まれているメソッドやフィールドの情報が格納され、 モジュールが正常に動作する大きな基盤になっています。 たとえば、CLRはJITコンパイルを通じてマネージコードをネイティブコードに変換しますが、 このときにはメタデータの情報を頼りに、メソッドが実装されている位置などを特定しています。 また、メタデータにアクセスすればフィールドの型も分かりますから、 あるフィールドに何からの型を代入する際、 その型が本当にフィールドの型と一致するかを調べるようなことも可能です。 この点は、.NETが掲げているタイプセーフと呼ばれる部分に相当します。

メタデータの中身を実際に確認する場合は、ildsamというアプリケーションを使用するのが便利です。 これは、%ProgramFiles%\Microsoft SDKs\Windows\v6.0\Bin以下に存在すると思われます。 試しに、次のようなコードを持ったマネージモジュールをildsamをオープンしてみます。

using System;

public class Class1
{
    int a;

    public void Method1()
    {
    }
}

モジュールをオープンするには、ildsamの「File」メニューから「Open」を選択します。 これにより、ウインドウの内容が次のようになります。

見て分かるように、コードで定義されていた型が列挙されています。 また、型の下には型の中で定義されたメソッドやフィールドも列挙されます。 ildsamはメタデータAPIを通じてメタデータにアクセスしているため、 アプリケーションからでもこのAPIを呼び出せば、メタデータにアクセスできます。

メタデータを読み取るか生成するかに関わらず、最初に必要となるインターフェースはIMetaDataDispenserです。 これは、CoCreateInstanceにCLSID_CorMetaDataDispenserを指定することで作成できます。 メタデータの読み取りを行いたいアプリケーションは、 IMetaDataDispenser::OpenScopeでIMetaDataImportを取得します。

HRESULT IMetaDataDispenser::OpenScope (
  LPCWSTR  szScope, 
  DWORD    dwOpenFlags, 
  REFIID   riid, 
  IUnknown **ppIUnk
);

szScopeは、オープンするファイル名を指定します。 dwOpenFlagsは、オープンする際のモードを指定します。 読み取りの場合は、ofReadを指定します。 riidは、取得するインターフェースのIIDを指定します。 IMetaDataImportの場合は、IID_IMetaDataImportになります。 ppIUnkは、インターフェースを受け取る変数のアドレスを指定します。

IMetaDataImportを取得したらメタデータの情報にアクセスできますが、 具体的にはどのようなことが可能なのでしょうか。 たとえば、メタデータには型についての定義が存在するわけですから、 特定の型がメタデータに存在するかを調べるようなことが可能です。 これには、FindTypeDefByNameを呼び出します。

HRESULT IMetaDataImport::FindTypeDefByName (
  LPCWSTR   szTypeDef,
  mdToken   tkEnclosingClass,
  mdTypeDef *ptd
);

szTypeDefは、検索したい型を表す文字列を指定します。 tkEnclosingClassは、通常はNULLで構いません。 ptdは、見つかった型を受け取る変数のアドレスを指定します。

メタデータに含まれる型は、mdTypeDefで表すことができます。 これは一般的に、TypeDefトークンと呼ばれています。 トークンというのは、メタデータの情報を表す単位だと思えばよいでしょう。 たとえば、メタデータは型に含まれるメソッドやフィールドも定義しており、 それぞれMethodトークン(mdMethodDef)とFieldトークン(mdFieldDef)で表されます。 ちなみに、トークンの実体は数値であり、トークン毎に一定の基準値が置かれています。 この基準値はCorTokenType列挙型として定義されており、 たとえばTypeDefトークンはmdtTypeDef(0x02000000)を基準にカウントした値になっています。

トークンには種類がありますから、トークンの情報を取得するメソッドにも種類があります。 TypeDefトークンの情報を取得したい場合は、IMetaDataImport::GetTypeDefPropsを呼び出します。

HRESULT IMetaDataImport::GetTypeDefProps (
  mdTypeDef td,
  LPWSTR    szTypeDef,
  ULONG     cchTypeDef,
  ULONG     *pchTypeDef,
  DWORD     *pdwTypeDefFlags,
  mdToken   *ptkExtends
);

tdは、情報を取得したいTypeDefトークンを指定します。 szTypeDefは、トークンの名前を受け取るバッファを指定します。 cchTypeDefは、バッファのサイズを指定します。 pchTypeDefは、バッファに格納されたサイズを受け取る変数のアドレスを指定します。 pdwTypeDefFlagsは、属性値を受け取る変数のアドレスを指定します。 TypeDefトークンの属性は、CorTypeAttr列挙型として定義されています。 ptkExtendsは、tdの基本型を受け取る変数のアドレスを指定します。

今回のプログラムは、特定の型がメタデータに定義されているかを調べます。 メタデータ APIを使用するため、cor.hをインクルードします。

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

void GetTypeData(IMetaDataImport *pMetaDataImport, mdTypeDef typeDef, LPWSTR lpszBuf);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	IMetaDataDispenser *pMetaDataDispenser;
	IMetaDataImport    *pMetaDataImport;
	mdTypeDef          typeDef;
	WCHAR              szBuf[2048];
	HRESULT            hr;

	CoInitialize(NULL);
	
	hr = CoCreateInstance(CLSID_CorMetaDataDispenser, 0, CLSCTX_INPROC_SERVER, IID_IMetaDataDispenser, (void **)&pMetaDataDispenser);
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}
	
	hr = pMetaDataDispenser->OpenScope(L"C:\\ClassLibrary1.dll", ofRead, IID_IMetaDataImport, (IUnknown **)&pMetaDataImport);
	if (FAILED(hr)) {
		pMetaDataDispenser->Release();
		CoUninitialize();
		return 0;
	}

	hr = pMetaDataImport->FindTypeDefByName(L"Class1", NULL, &typeDef);
	if (FAILED(hr)) {
		MessageBox(NULL, TEXT("指定した型が存在しません。"), NULL, MB_ICONWARNING);
		pMetaDataImport->Release();
		pMetaDataDispenser->Release();
		CoUninitialize();
		return 0;
	}

	GetTypeData(pMetaDataImport, typeDef, szBuf);

	MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);

	pMetaDataImport->Release();
	pMetaDataDispenser->Release();
	CoUninitialize();
	
	return 0;
}

void GetTypeData(IMetaDataImport *pMetaDataImport, mdTypeDef typeDef, LPWSTR lpszBuf)
{
	WCHAR szName[256];
	DWORD dwAttr;

	pMetaDataImport->GetTypeDefProps(typeDef, szName, 256, NULL, &dwAttr, NULL);
	if (IsTdInterface(dwAttr))
		lstrcatW(lpszBuf, L"Interface ");
	if (IsTdPublic(dwAttr))
		lstrcatW(lpszBuf, L"Public ");
	if (IsTdAbstract(dwAttr))
		lstrcatW(lpszBuf, L"Abstract ");
	if (IsTdSealed(dwAttr))
		lstrcatW(lpszBuf, L"Sealed ");

	lstrcatW(lpszBuf, szName);
}

CoCreateInstanceでIMetaDataDispenserを取得したら、OpenScopeでIMetaDataImportを取得します。 これが成功したら、第1引数のアセンブリのメタデータにアクセスできます。 FindTypeDefByNameには"Class1"という文字列を指定しているため、 アセンブリには"Class1"という型が定義されている必要があります。 これが成功したら型を表すTypeDefトークンを取得できるため、 GetTypeDataという自作関数でトークンの情報を取得します。

void GetTypeData(IMetaDataImport *pMetaDataImport, mdTypeDef typeDef, LPWSTR lpszBuf)
{
	WCHAR szName[256];
	DWORD dwAttr;

	pMetaDataImport->GetTypeDefProps(typeDef, szName, 256, NULL, &dwAttr, NULL);
	if (IsTdInterface(dwAttr))
		lstrcatW(lpszBuf, L"Interface ");
	if (IsTdPublic(dwAttr))
		lstrcatW(lpszBuf, L"Public ");
	if (IsTdAbstract(dwAttr))
		lstrcatW(lpszBuf, L"Abstract ");
	if (IsTdSealed(dwAttr))
		lstrcatW(lpszBuf, L"Sealed ");

	lstrcatW(lpszBuf, szName);
}

GetTypeDefPropsの第4引数と第6引数はNULLで問題ありません。 第5引数もNULLを指定できますが、型の属性を取得するために指定しています。 ここで返る値はCorTypeAttr列挙型であり、たとえばdwAttr & tdInterfaceの結果が真になるならば、 その型はinterfaceということになります。 しかし、こうした記述はIsTdInterfaceのような形で定義されているので、それを使用することにしています。

マネージモジュールかの判定

マネージモジュールには、既に述べたようにマネージコードとメタデータが格納されていますが、 これ以外にCLRヘッダと呼ばれる情報も格納されています。 アプリケーションがこのヘッダにアクセスすることは基本的にありませんが、 ヘッダが存在することはモジュールがマネージであることを意味するため、 そうした判定に利用することができます。 次に例を示します。

#include <windows.h>

BOOL IsManageModule(LPTSTR lpszModule, LPBOOL lpbManage);
DWORD GetAddress(PIMAGE_NT_HEADERS pNtHeaders, DWORD dwRva);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	BOOL bManage;

	if (!IsManageModule(TEXT("C:\\ClassLibrary1.dll"), &bManage)) {
		MessageBox(NULL, TEXT("モジュールのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	if (bManage)
		MessageBox(NULL, TEXT("モジュールはマネージです。"), TEXT("OK"), MB_OK);
	else
		MessageBox(NULL, TEXT("モジュールはアンマネージです。"), TEXT("OK"), MB_OK);

	return 0;
}

BOOL IsManageModule(LPTSTR lpszModule, LPBOOL lpbManage)
{
	HANDLE              hFile;
	HANDLE              hFileMapping;
	LPVOID              lpBaseAddress;
	PIMAGE_DOS_HEADER   pDosHeader;
	PIMAGE_NT_HEADERS   pNtHeaders;
	PIMAGE_COR20_HEADER pCorHeader;
	DWORD               dwRva;

	hFile = CreateFile(lpszModule, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return FALSE;

	hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
	lpBaseAddress = MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0);

	pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress;
	pNtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)pDosHeader + pDosHeader->e_lfanew);
	dwRva = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].VirtualAddress;
	if (dwRva == 0) {
		*lpbManage = FALSE;
		UnmapViewOfFile(lpBaseAddress);
		CloseHandle(hFileMapping);
		CloseHandle(hFile);
		return TRUE;
	}

	pCorHeader = (PIMAGE_COR20_HEADER)((LPBYTE)lpBaseAddress + GetAddress(pNtHeaders, dwRva));
	*lpbManage = pCorHeader->cb == sizeof(IMAGE_COR20_HEADER);

	UnmapViewOfFile(lpBaseAddress);
	CloseHandle(hFileMapping);
	CloseHandle(hFile);
	
	return TRUE;
}

DWORD GetAddress(PIMAGE_NT_HEADERS pNtHeaders, DWORD dwRva)
{
	WORD                  i;
	DWORD                 dwSize;
	PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeaders);

	for (i= 0; i < pNtHeaders->FileHeader.NumberOfSections; i++) {
		dwSize = pSectionHeader->Misc.VirtualSize;
		if (dwSize == 0)
			dwSize = pSectionHeader->SizeOfRawData;

		if (dwRva >= pSectionHeader->VirtualAddress && dwRva < pSectionHeader->VirtualAddress + dwSize) {
			dwRva -= pSectionHeader->VirtualAddress - pSectionHeader->PointerToRawData;
			break;
		}
	}

	return dwRva;
}

モジュールを読み込んで先頭アドレスにアクセスするために、 CreateFileからMapViewOfFileまでの関数を呼び出しています。 マネージ、アンマネージを問わずモジュールの先頭はIMAGE_DOS_HEADER構造体で識別でき、 e_lfanewメンバの値を足すことでIMAGE_NT_HEADERS構造体を参照できます。 この構造体のOptionalHeader.DataDirectoryには特定のデータ領域までのオフセットが格納されており、 IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTORを指定すれば、CLRヘッダまでのオフセットが分かります。 もし、これが0である場合はヘッダが存在しないことになるため、モジュールはマネージではないことになります。 ヘッダの位置は、先頭アドレスとオフセットを足すことで分かりますが、 上記のようにモジュールをLoadLibraryでロードしていない場合は、 オフセットの値を少し調整する必要があります。 この処理はGetAddressで行われていますが、この内容は少し複雑であるためここでは説明を割愛します。 ヘッダはIMAGE_COR20_HEADER構造体で識別でき、cbメンバが構造体のサイズと一致する場合は、 ヘッダを正しく存在していると考えてよいでしょう。



戻る