ページ

2012-12-03

ILGeneratorの「向こう側」でTypeやMethodInfoを得るために"ldtoken"命令と~FromHandleメソッドを使う

最初のころは黒魔術だったのにだんだん慣れてきちゃった。とはいえ、書いておかないと忘れかねないので書いておく。今回のテーマはタイトルの通りなので、タイトルを読んでわかる人には不要の話。

概要

ILGeneratorを使ってコードを書いているような状況では、ILGeneratorの「向こう側」(生成したILコードが動作している文脈のこと。ILを使ってコードを生成していると、文脈を区別したくなるので、勝手にこう呼んでいる)でTypeやMethodInfoなどのリフレクション用のオブジェクトが欲しくなるときがある。もちろん、Nameを使ってGetMethod等々を「向こう側」で呼ぶということも可能だが、「こっち側」(ILコードを生成している側の文脈)でType等々が識別できている状況では、もう少しいい方法がある。それは以下の通り。

  1. "ldtoken"命令と「メタデータ トークン」を使って、評価スタックにRuntimeHandleをpushする。
  2. 各種~FromHandleメソッドを使って、TypeやMethodInfoなどを得る。

実践 - Typeの場合

Typeの場合こんな感じ。
var typeObj = il.DeclareLocal(typeof(Type));   // ローカル変数の準備
il.Emit(OpCodes.Ldtoken, typeof(SampleClass)); // -- (1)
il.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"), null); // -- (2)
il.Emit(OpCodes.Stloc, typeObj); // ローカル変数へのストア

(1) メタデータトークンからRuntimeTypeHandleを得る

まず、「メタデータ トークン」を使ってldtoken命令を実行し、RuntimeHandleを得る。
書式 / アセンブリ形式 / 説明
D0 < T > / ldtoken token / メタデータ トークンをそのランタイム表現に変換します。
(from OpCodes.Ldtoken フィールド)

しかし、メタデータ トークンとは何か?
メタデータは、ランタイム型 (クラス、値型、およびインターフェイス)、グローバル関数、グローバル変数などの抽象化の宣言情報です。
(from メタデータ トークン)
ということで、詳細は上記ページに書いてあるが、型やメソッドなんかのプリミティブな表現くらいに思っておけば大丈夫だと思う。

幸いなことに、ILGenerator.Emit(OpCodes.Ldtoken, xxx)なら、TypeやMethodInfoを渡せば裏でうまいことやってくれるので、あまり意識しなくていい。

このメタデータトークンを使って"ldtoken"を使うと、「RuntimeHandle」が得られる。またややこしいものがでてきたが、
内部メタデータ トークンを使用する型を表します。
(RuntimeTypeHandle 構造体)
ということで、メタデータトークンの構造体表現くらいだと思っておけばいいと思う。

(2) RuntimeTypeHandleからTypeオブジェクトを得る

RuntimeTypeHandleからTypeオブジェクトを得るには、Type.GetTypeFromHandle(RuntimeTypeHandle)を使う。

これでTypeが得られたので、あとは好きに使えばOK.
たとえば、Nameを表示したければこんな感じ。
// 試しに名前を出してみる
il.Emit(OpCodes.Ldloc, typeObj);
il.EmitCall(OpCodes.Callvirt, typeof(Type).GetProperty("Name").GetGetMethod(), null);
il.EmitCall(OpCodes.Call,
  typeof(System.Console).GetMethod("WriteLine", new Type[] { typeof(String) }),
 null);

このコードで生成したコードがこんな感じ。
ldtoken [HelloMetadata]HelloMetadata.SampleClass
call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
stloc.0
 
ldloc.0
callvirt instance string [mscorlib]System.Reflection.MemberInfo::get_Name()
call void [mscorlib]System.Console::WriteLine(string)

実践 - MethodInfoの場合

MethodInfoの場合はこんな感じ。
var methodInfoObj = il.DeclareLocal(typeof(MethodInfo));
il.Emit(OpCodes.Ldtoken, typeof(SampleClass).GetMethod("SampleMethod"));
il.EmitCall(OpCodes.Call, typeof(MethodBase).GetMethod("GetMethodFromHandle", new Type[] { typeof(RuntimeMethodHandle) } ), null);
il.Emit(OpCodes.Stloc, methodInfoObj);

概ねTypeの場合と同じだが、Methodの場合は二つ注意点がある。
  • GetMethodFromHandle()は、MethodInfo/ConstructorInfoの親であるMethodBaseに存在する。
  • GetMethodFromHandle()は、Typeが指定できるオーバーロードがあるので、メソッド名だけではGetMethod()できない。

名前を表示したければ、同じようにこんなかんじ。
// 試しに名前を出してみる
il.Emit(OpCodes.Ldloc, methodInfoObj);
il.EmitCall(OpCodes.Callvirt, typeof(MethodInfo).GetProperty("Name").GetGetMethod(), null);
il.EmitCall(OpCodes.Call,
   typeof(System.Console).GetMethod("WriteLine", new Type[] { typeof(String) }),
   null);

これで生成されたコードがこんな感じ。
ldtoken method void [HelloMetadata]HelloMetadata.SampleClass::SampleMethod()
call class [mscorlib]System.Reflection.MethodBase [mscorlib]System.Reflection.MethodBase::GetMethodFromHandle(valuetype [mscorlib]System.RuntimeMethodHandle)
stloc.1

ldloc.1
callvirt instance string [mscorlib]System.Reflection.MemberInfo::get_Name()
call void [mscorlib]System.Console::WriteLine(string)

その他

FieldInfoでは試してないけど、OpCodes.Ldlocの説明を見る限り、概ね同じ方法でいけると思う。
また、RuntimeMethodHandleについては、IL生成をしていない場面でもメソッドを識別するのに便利な場面があるかもしれない。

0 件のコメント:

コメントを投稿