やること
ILのこねかたについて、多少は実用的な例を題材に入門してみます。
多少は実用的ということで、よくあるケースとして高速なリフレクション用のインターフェースを実装してみます。
具体的には次のようなインターフェースを用意しておき、その実装クラスのコード生成を行います。
- 高速なオブジェクトの生成
public interface IActivator
{
ConstructorInfo Source { get; }
object Create(params object[] arguments);
}
- 高速なプロパティアクセス
public interface IAccessor
{
PropertyInfo Source { get; }
object GetValue(object target);
void SetValue(object target, object value);
}
とりあえず今回はIActivatorの方について。
対象とする読者
黒魔術師の弟子達よ!!(ILによる動的なコード生成に興味はあるけど、どこからはじめていいかわからないみなさん)
サンプル
はじめかた
ConstructoInfoも元に、高速なオブジェクト生成を行う処理を動的に生成します。
ILを使った動的なコード生成処理を作成する場合、お勧めの方法は完成形のコードをC#で書いてみてそれを逆アセンブルして内容を確認し、その再現を行うという方法です。
例として、以下のような生成対象のDataクラスとそれを生成するIActivatorをサンプルとして書いてみます。
public class Data
{
}
public sealed class SampleDatactivator : IActivator
{
public ConstructorInfo Source { get; }
public SampleMutableDataActivator1(ConstructorInfo source)
{
Source = source;
}
public object Create(params object[] arguments)
{
return new Data();
}
}
SampleDatactivatorはDataクラスをnewして生成しているだけなので、当然リフレクションを使うよりも高速に動作します。
このSampleDatactivator相当の内容を動的に作れれば、高速なリフレクションを実現できると言うことになります。
逆アセンブル
サンプルコードをビルドしてその内容をildasm.exeを使用して確認します。
この内容を参考にしてに、SampleDatactivator相当の型を動的に生成する処理を書いていきます。
前準備
まず、動的に生成する型を定義するためのモジュールを用意します。
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
new AssemblyName("DynamicAssembly"),
AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule(
"DynamicModule");
ここで作成したモジュールに型を定義してその実装のILを生成し、最終的にはリフレクションでその型のインスタンスを生成すること形で動的生成されたIActivatorの実装を取得する、っということを行います。
型の定義
型の定義はModuleBuilder.DefineType()で行いますが、その引数に何を指定すればいいのかという話があります。
クラス名についてはここでは「生成する対象のクラス名_DynamicActivator」の形にするとして、TypeAttributeにはildasmで確認したのと同じ値を設定します。
ildasmでSampleDatactivatorのクラス定義の部分をダブルクリックして開くと、次のような内容が確認できます。
.class public auto ansi sealed beforefieldinit EmitExample.SampleDataActivator
extends [mscorlib]System.Object
implements EmitExample.IActivator
{
} // end of class EmitExample.SampleDataActivator
public~beforefieldinitまでが型の属性であり、また、この型はIActivatorを実装していることがわかります。
ここまでの内容を踏まえて、型の定義部分と、IActivator作成処理全体のコードイメージを記述すると以下のようになります。
public IActivator CreateActivator(ConstructorInfo ci)
{
var typeBuilder = moduleBuilder.DefineType(
$"{ci.DeclaringType.FullName}_DynamicActivator",
TypeAttributes.Public | TypeAttributes.AutoLayout | TypeAttributes.AnsiClass | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit);
typeBuilder.AddInterfaceImplementation(typeof(IActivator));
// (ここでプロパティ、コンストラクタ、メソッドを定義)
var typeInfo = typeBuilder.CreateTypeInfo();
return (IActivator)Activator.CreateInstance(typeInfo.AsType(), ci);
}
なお、メンバを実装していないこの時点でコードを実行すると、IActivatorに必要な実装がされていないためCreateTypeInfo()でTypeLoadExceptionとなります。
プロパティの定義
次に、IActivator.Sourceプロパティの定義を行います。
ildasmで確認すると、Sourceプロパティに関する内容として以下の項目が定義されていることがわかります。
項目 | 内容 |
---|---|
<Source>k__BackingField | 値が保存されるフィールド(バッキングフィールド) |
get_Source | プロパティのget処理のメソッド |
Source | プロパティの定義 |
SampleDatactivatorではSourceを自動実装プロパティで記述しています。
そのためバッキングフィールドが作成されていますが、ここではわかりやすくするために、自動実装プロパティを使わない形でサンプルを書いて、その内容をidlasmで確認してみます。
public sealed class SampleDataActivator2 : IActivator
{
private readonly ConstructorInfo source;
public ConstructorInfo Source
{
get { return source; }
}
public SampleDataActivator2(ConstructorInfo source)
{
this.source = source;
}
public object Create(params object[] arguments)
{
return new Data();
}
}
バッキングフィールドの代わりにsourceフィールドが定義されていることが確認できました。
Sourceプロパティの定義では、これらのフィールドの定義、プロパティの定義、プロパティのgetメソッドの定義という3つの定義を行います。
フィールドの定義
型の定義で確認した時の同じように、フィールドの定義を開くと次のような内容を確認できます。
.field private initonly class [mscorlib]System.Reflection.ConstructorInfo source
この情報を元にフィールドの定義を記述すると次のようになります。
var sourceField = typeBuilder.DefineField(
"source",
typeof(ConstructorInfo),
FieldAttributes.Private | FieldAttributes.InitOnly);
これでConstructorInfo型のsourceフィールドが定義できました。
プロパティの定義
フィールドと同様に、プロパティの定義も確認して実装します。
.property instance class [mscorlib]System.Reflection.ConstructorInfo
Source()
{
.get instance class [mscorlib]System.Reflection.ConstructorInfo EmitExample.SampleDataActivator2::get_Source()
} // end of property SampleDataActivator2::Source
var property = typeBuilder.DefineProperty(
"Source",
PropertyAttributes.None,
typeof(ConstructorInfo),
null);
プロパティはその定義だけでは駄目で、そのget/setメソッドの定義を行ってプロパティに設定してあげる必要があります。
getメソッドの定義
get_Sourceの定義を開くと以下のような内容が確認できます。
.method public hidebysig newslot specialname virtual final
instance class [mscorlib]System.Reflection.ConstructorInfo
get_Source() cil managed
{
// コード サイズ 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldfld class [mscorlib]System.Reflection.ConstructorInfo EmitExample.SampleDataActivator2::source
IL_0006: ret
} // end of method SampleDataActivator2::get_Source
ここではじめてILが出てきましたが、まずはget_Sourceメソッドの定義とプロパティへの設定を行ってしまいましょう。
var method = typeBuilder.DefineMethod(
"get_Source",
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual | MethodAttributes.Final,
typeof(ConstructorInfo),
Type.EmptyTypes);
property.SetGetMethod(method);
そしてやっとILの生成の話になりますが、ここでの処理は単純で、this.sourceを返しているだけです。
っというわけで、そのままコード化してしまいます。
var ilGenerator = method.GetILGenerator();
// インデックス0の引数(this)をスタックに積む
ilGenerator.Emit(OpCodes.Ldarg_0);
// スタック上にあるオブジェクト内のフィールド(source)を取得
ilGenerator.Emit(OpCodes.Ldfld, sourceField);
// 戻り値を返す
ilGenerator.Emit(OpCodes.Ret);
以上で、Sourceプロパティの取得に関する処理はできました。
コンストラクタの定義
Sourceプロパティはgetオンリーなプロパティなので、コンストラクタでその取得元となるsourceフィールドを初期化してあげる必要があります。
これまでと同様に、ildasmでコンストラクタの定義を開くと以下の内容を確認できます。
.method public hidebysig specialname rtspecialname
instance void .ctor(class [mscorlib]System.Reflection.ConstructorInfo source) cil managed
{
// コード サイズ 14 (0xe)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ldarg.0
IL_0007: ldarg.1
IL_0008: stfld class [mscorlib]System.Reflection.ConstructorInfo EmitExample.SampleDataActivator2::source
IL_000d: ret
} // end of method SampleDataActivator2::.ctor
そしてこの内容を元にコンストラクタを定義します。
var ctor = typeBuilder.DefineConstructor(
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
CallingConventions.Standard,
new[] { typeof(ConstructorInfo) });
次にIL部分ですが、やっていることはベースクラスであるobjectクラスの呼び出しと、引数へのメンバ変数への保存です。
ILの生成コードへ変換すると以下の形になります。
var ilGenerator = ctor.GetILGenerator();
// objectのコンストラクタを呼ぶ
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
// 引数のsourceの値をフィールドのsourceに格納する
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Stfld, sourceField);
// 戻る
ilGenerator.Emit(OpCodes.Ret);
Createメソッドの定義
そしてIActivatorのメインとなるCreateメソッドの実装についてです。
まず、ildasmで内容を確認してみます。
.method public hidebysig newslot virtual final
instance object Create(object[] arguments) cil managed
{
.param [1]
.custom instance void [mscorlib]System.ParamArrayAttribute::.ctor() = ( 01 00 00 00 )
// コード サイズ 6 (0x6)
.maxstack 8
IL_0000: newobj instance void EmitExample.Data::.ctor()
IL_0005: ret
} // end of method SampleDataActivator2::Create
とりあえず、これまでと同様にメソッドの定義だけをしてしまいましょう。
var method = typeBuilder.DefineMethod(
nameof(IActivator.Create),
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final,
typeof(object),
new[] { typeof(object[]) });
typeBuilder.DefineMethodOverride(method, typeof(IActivator).GetMethod(nameof(IActivator.Create)));
DefineMethodOverrideでは、このメソッドはIActivator.Createの実装であることを定義しています。
そしてILの生成コードは以下になります。
var ilGenerator = method.GetILGenerator();
// インスタンス生成
ilGenerator.Emit(OpCodes.Newobj, ci);
// 返す
ilGenerator.Emit(OpCodes.Ret);
完成?
やったー、一通りの定義ができたぞ(?)、っというわけで、とりあえず使ってみましょうか。
var ctor = typeof(Data).GetConstructor(Type.EmptyTypes);
var activator = CreateActivator(ctor);
var obj = activator.Create();
はい、確かにactivator.Create()でDataのインスタンスを取得することが確認できると思います。
ただし、Createの引数が無し?、っということで気がつきますが、ここまでの実装だけだとデフォルトコンストラクタにしか対応していません。
っというわけで、コンストラクタの引数にも対応するため、新たに以下のようなサンプルを追加してビルドし、逆アセンブルして結果を見ていくことにします。
public class Data2
{
public int IntValue { get; }
public string StringValue { get; }
public Data2(int intValue, string stringValue)
{
IntValue = intValue;
StringValue = stringValue;
}
}
public sealed class SampleData2Activator : IActivator
{
public ConstructorInfo Source { get; }
public SampleData2Activator(ConstructorInfo source)
{
Source = source;
}
public object Create(params object[] arguments)
{
return new Data2((int)arguments[0], (string)arguments[1]);
}
}
Createメソッドの定義(完全版)
SampleData2Activatorの逆アセンブル結果については、CreateメソッドのIL以外はこれまでと同じなので、ildasmからIL部分だけを抜粋してみます。
IL_0000: ldarg.1
IL_0001: ldc.i4.0
IL_0002: ldelem.ref
IL_0003: unbox.any [mscorlib]System.Int32
IL_0008: ldarg.1
IL_0009: ldc.i4.1
IL_000a: ldelem.ref
IL_000b: castclass [mscorlib]System.String
IL_0010: newobj instance void EmitExample.Data2::.ctor(int32, string)
IL_0015: ret
これだとわかりづらい?、っということで、処理を区切って内容を補足してみます。
// 第1引数intをスタック
IL_0000: ldarg.1
IL_0001: ldc.i4.0
IL_0002: ldelem.ref
IL_0003: unbox.any [mscorlib]System.Int32
// 第2引数stringをスタック
IL_0008: ldarg.1
IL_0009: ldc.i4.1
IL_000a: ldelem.ref
IL_000b: castclass [mscorlib]System.String
// インスタンスの生成
IL_0010: newobj instance void EmitExample.Data2::.ctor(int32, string)
IL_0015: ret
こからどこまでが何の処理かということについては、ILの知識が十分になくても、引数の数や型を変更してその逆アセンブル結果を比較していくと、なんとなくあたりがつくようになってくるのではないかと思います。
更に引数の処理部分を見ていくと、以下の部分は繰り返しになっていることがわかります。
- ldarg.1 : 第1引数(arguments)をスタック
- ldc.i4 : 整数値(ここで配列のインデックス)をスタック
- ldelem.ref : 配列のn番目の値をスタック
unbox.anyとcastclassについては、ValueTypeならUnboxが必要で、ValueType以外は(object型以外は)コンストラクタ引数の型へのキャストが必要だということがわかってくると思います。
これをIL生成コードにしていくわけですが、以下の点を考慮してコードを書きたいところです。
- 繰り返し処理
- ldc.i4.xは整数値0~8については専用のオペコードがあり、それ以外では127までの値に使える短縮形のldc.i4.sとldc.i4がある点
- 引数の型による変換処理の分岐部分
ILに関する処理については、他でも同様の処理が必要になることが想定されるため、以下のようなILGeneratorの拡張メソッドを用意して記述を行いやすくしたいと思います。
ldc.i4用拡張メソッド
ldc.i4の使い分けについては、設定する値によって使用するオペコードを使い分ける拡張メソッドを用意します。
public static void EmitLdcI4(this ILGenerator il, int i)
{
switch (i)
{
case 0:
il.Emit(OpCodes.Ldc_I4_0);
break;
case 1:
il.Emit(OpCodes.Ldc_I4_1);
break;
case 2:
il.Emit(OpCodes.Ldc_I4_2);
break;
case 3:
il.Emit(OpCodes.Ldc_I4_3);
break;
case 4:
il.Emit(OpCodes.Ldc_I4_4);
break;
case 5:
il.Emit(OpCodes.Ldc_I4_5);
break;
case 6:
il.Emit(OpCodes.Ldc_I4_6);
break;
case 7:
il.Emit(OpCodes.Ldc_I4_7);
break;
case 8:
il.Emit(OpCodes.Ldc_I4_8);
break;
default:
if ((i >= -128) && (i <= 127))
{
il.Emit(OpCodes.Ldc_I4_S, (sbyte)i);
}
else
{
il.Emit(OpCodes.Ldc_I4, i);
}
break;
}
}
型変換用拡張メソッド
型の変化については、下記のような拡張メソッドを用意することで分岐部分の記述を容易にします。
public static void EmitTypeConversion(this ILGenerator il, Type type)
{
if (type.IsValueType)
{
il.Emit(OpCodes.Unbox_Any, type);
}
else if (type != typeof(object))
{
il.Emit(OpCodes.Castclass, type);
}
}
CreateメソッドのIL完成版
拡張メソッドを使用し、コンストラクタの引数に対応したCreateメソッドのILは以下のようになります。
var ilGenerator = method.GetILGenerator();
// argumentsの処理
for (var i = 0; i < ci.GetParameters().Length; i++)
{
// argumentsの
ilGenerator.Emit(OpCodes.Ldarg_1);
// i番目を
ilGenerator.EmitLdcI4(i);
// スタックして
ilGenerator.Emit(OpCodes.Ldelem_Ref);
// 型変換
ilGenerator.EmitTypeConversion(ci.GetParameters()[i].ParameterType);
}
// インスタンス生成
ilGenerator.Emit(OpCodes.Newobj, ci);
// 返す
ilGenerator.Emit(OpCodes.Ret);
本当に完成したか確認
以下のようなコードが動作することを確認できると思います。
var ctor = typeof(Data2).GetConstructor(new[] { typeof(int), typeof(string) });
var activator = CreateActivator(ctor);
var obj = (Data2)activator.Create(1, "abc");
Debug.Assert(obj.IntValue == 1);
Debug.Assert(obj.StringValue == "abc");
うまくいかないと思ったら
動的生成した型のインスタンスは生成できるが、処理を呼び出してみると動作がおかしかったり、例外が発生したりすることがあるかもしれません。
どこかILが間違っているのかもしれないと思った時には、型を定義したモジュールをファイルに保存して、その内容をildasmでサンプルで作ったコードのILとの差異を確認すると問題の把握が容易になると思います。
定義したモジュールをファイルに保存したい場合、モジュールの定義は以下のように変更します。
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
new AssemblyName("DynamicAssembly"),
AssemblyBuilderAccess.RunAndSave);
var moduleBuilder = assemblyBuilder.DefineDynamicModule(
"DynamicModule",
"test.dll");
また、型の定義が完了後に以下のようにしてファイルに保存します。
assemblyBuilder.Save("test.dll");
次回
うさコメ
っということで、こんな風に進めていけば、みんなILの先生になれることがわかったのではないかと思います(`・ω・´)
なお、全体のソースについてはサンプルのEmitFactory.csを参考にしてくださいな。
動的コード生成、それも一種の麻疹( ˘ω˘ )
そしてコンパイルタイムコード生成もいいもんですよ(・∀・)