C#
.NET

ILを使った動的なコード生成入門(高速リフレクション-プロパティアクセス編)

やること

前回の続きとして、PropertyInfoをもとに高速なプロパティアクセスの動的生成を行います。

  • 高速なプロパティアクセス
public interface IAccessor
{
    PropertyInfo Source { get; }

    object GetValue(object target);

    void SetValue(object target, object value);
}

進め方

基本的なやりかたは前回と同様、以下の手順で行います。

  • ベースとなるinterfaceを用意
  • サンプルとなる実装をC#でベタ書きしてビルド
  • ビルド結果をildasmで逆アセンブルし、その内容をもとに動的生成を行うコードを記述

まずは単純そうなところからということで、以下のようなstring型のプロパティを持つDataクラスと、そのアクセスを行うIAccessorをサンプルとして書いてその再現からはじめてみます。

public class Data
{
    public string StringValue { get; set; }
}
public sealed class SampleDataStringValueAccessor : IAccessor
{
    public PropertyInfo Source { get; }

    public SampleDataStringValueAccessor(PropertyInfo source)
    {
        Source = source;
    }

    public object GetValue(object target)
    {
        return ((Data)target).StringValue;
    }

    public void SetValue(object target, object value)
    {
        ((Data)target).StringValue = (string)value;
    }
}

SampleDataStringValueAccessorで行っているのはプロパティへの直接アクセスであり、必要なコストはキャストだけなため、リフレクションを使うよりも高速に動作します。

逆アセンブル

サンプルコードをビルドして、その内容をildasm.exeを使用して確認します。

3.png

今回はこのSampleDataStringValueAccessor相当の内容を動的に作成していきます。

なお、内容から類推できますが、プロパティ及びコンストラクタの定義については扱っている型がConstructorInfoからPropertyInfoになっているだけで、構造は前回のIActivatorの実装と同様です。

よって、その部分は前回の内容を参照する形都市、今回はGetValueメソッド及びSetValueメソッドの実装についてのみ絞って記述します。

GetValueメソッドの定義

例によって、まずはildasmでメソッド定義の内容を確認します。

.method public hidebysig newslot virtual final 
        instance object  GetValue(object target) cil managed
{
  // コード サイズ       12 (0xc)
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  castclass  EmitExample.Data
  IL_0006:  callvirt   instance string EmitExample.Data::get_StringValue()
  IL_000b:  ret
} // end of method SampleDataStringValueAccessor::GetValue

この内容をもとにしたメソッドの定義は前回のIActivator.Createと同様で、以下のようになります。

var method = typeBuilder.DefineMethod(
    nameof(IAccessor.GetValue),
    MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final,
    typeof(object),
    new[] { typeof(object) });
typeBuilder.DefineMethodOverride(method, typeof(IAccessor).GetMethod(nameof(IAccessor.GetValue)));

ILも特に難しい点はなく、キャストしたプロパティの値を取得しているだけです。

var ilGenerator = method.GetILGenerator();

// インデックス1の引数(target)をスタックに積む
ilGenerator.Emit(OpCodes.Ldarg_1);
// pi.DeclaringType(Data型)のキャスト
ilGenerator.Emit(OpCodes.Castclass, pi.DeclaringType);
// スタック上にあるオブジェクトのプロパティ(StringValue)のGetメソッドを呼び出す
ilGenerator.Emit(OpCodes.Callvirt, pi.GetGetMethod());
// 戻り値を返す
ilGenerator.Emit(OpCodes.Ret);

SetValueメソッドの定義

特に難しい点はないので、さくさく進めていきます。

まずはildasmの結果から。

.method public hidebysig newslot virtual final 
        instance void  SetValue(object target,
                                object 'value') cil managed
{
  // コード サイズ       18 (0x12)
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  castclass  EmitExample.Data
  IL_0006:  ldarg.2
  IL_0007:  castclass  [mscorlib]System.String
  IL_000c:  callvirt   instance void EmitExample.Data::set_StringValue(string)
  IL_0011:  ret
} // end of method SampleDataStringValueAccessor::SetValue

メソッドの定義もGetValueと同様で、以下のような感じで。

var method = typeBuilder.DefineMethod(
    nameof(IAccessor.SetValue),
    MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final,
    typeof(void),
    new[] { typeof(object), typeof(object) });
typeBuilder.DefineMethodOverride(method, typeof(IAccessor).GetMethod(nameof(IAccessor.SetValue)));

ILについても、C#で書いたコードのイメージそのままなので難しい点はないと思います。

var ilGenerator = method.GetILGenerator();

// インデックス1の引数(target)をスタックに積んでpi.DeclaringType(Data型)にキャスト
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Castclass, pi.DeclaringType);
// インデックス2の引数(value)をスタックに積んでpi.PropertyType(string型)にキャスト
ilGenerator.Emit(OpCodes.Ldarg_2);
ilGenerator.Emit(OpCodes.Castclass, pi.PropertyType);
// スタック上にあるオブジェクトのプロパティ(StringValue)のSetメソッドを呼び出す
ilGenerator.Emit(OpCodes.Callvirt, pi.GetSetMethod());
// 戻る
ilGenerator.Emit(OpCodes.Ret);

完成?

とりあえず使ってみましょうか。

var pi = typeof(Data).GetProperty(nameof(Data.StringValue));
var accessor = CreateAccessor(pi);

var data = new Data();

accessor.SetValue(data, "abc");
var value = accessor.GetValue(data);

このような感じで、プロパティへのアクセスが確認できると思います。

では、ここでstring型以外の型に使ったらどんな結果になるでしょうか?

まずは以下のような、int型のプロパティを対象にしてみます。

public class Data
{
    public int IntValue { get; set; }
}

現時点でのコードを上記のようなプロパティに対して適用すると、正しい値で処理されないことを確認できると思います。

現時点ではValutTypeに対応していないため、このような結果となっています。

ValueTypeへの対応

ValueTypeに対応したILを生成するため、int型のプロパティを処理するサンプルを作って逆アセンブルしてみます。

サンプルのGetValue及びSetValueは以下のよう実装とし、nullが指定されたら0を設定するようにしています。

public object GetValue(object target)
{
    return ((Data)target).IntValue;
}

public void SetValue(object target, object value)
{
    ((Data)target).IntValue = (int?)value ?? 0;
}

この結果をildasmで確認すると、GetValueとSetValueはそれぞれ以下のようになっていることが確認できます。

.method public hidebysig newslot virtual final 
        instance object  GetValue(object target) cil managed
{
  // コード サイズ       17 (0x11)
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  castclass  EmitExample.Data
  IL_0006:  callvirt   instance int32 EmitExample.Data::get_IntValue()
  IL_000b:  box        [mscorlib]System.Int32
  IL_0010:  ret
} // end of method SampleDataIntValueAccessor::GetValue```
.method public hidebysig newslot virtual final 
        instance void  SetValue(object target,
                                object 'value') cil managed
{
  // コード サイズ       38 (0x26)
  .maxstack  2
  .locals init (valuetype [mscorlib]System.Nullable`1<int32> V_0)
  IL_0000:  ldarg.1
  IL_0001:  castclass  EmitExample.Data
  IL_0006:  ldarg.2
  IL_0007:  unbox.any  valuetype [mscorlib]System.Nullable`1<int32>
  IL_000c:  stloc.0
  IL_000d:  ldloca.s   V_0
  IL_000f:  call       instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
  IL_0014:  brtrue.s   IL_0019
  IL_0016:  ldc.i4.0
  IL_0017:  br.s       IL_0020
  IL_0019:  ldloca.s   V_0
  IL_001b:  call       instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
  IL_0020:  callvirt   instance void EmitExample.Data::set_IntValue(int32)
  IL_0025:  ret
} // end of method SampleDataIntValueAccessor::SetValue

さて、ここでGetValueはstringの時と比べてboxが追加されているだけなので単純ですが、SetValueの方はぱっと見わかりづらいと思います。

そこで、サンプルソースを以下のような形で、nullかどうかで分岐するような記述に変更してみます。

public void SetValue(object target, object value)
{
    if (value == null)
    {
        ((Data)target).IntValue = 0;
    }
    else
    {
        ((Data)target).IntValue = (int)value;
    }
}

そうすると、ildasmの結果は以下のようになりました。

.method public hidebysig newslot virtual final 
        instance void  SetValue(object target,
                                object 'value') cil managed
{
  // コード サイズ       34 (0x22)
  .maxstack  8
  IL_0000:  ldarg.2
  IL_0001:  brtrue.s   IL_0010
  IL_0003:  ldarg.1
  IL_0004:  castclass  EmitExample.Data
  IL_0009:  ldc.i4.0
  IL_000a:  callvirt   instance void EmitExample.Data::set_IntValue(int32)
  IL_000f:  ret
  IL_0010:  ldarg.1
  IL_0011:  castclass  EmitExample.Data
  IL_0016:  ldarg.2
  IL_0017:  unbox.any  [mscorlib]System.Int32
  IL_001c:  callvirt   instance void EmitExample.Data::set_IntValue(int32)
  IL_0021:  ret
} // end of method SampleDataIntValueAccessor2::SetValue

更に、処理毎に区切ってコメントを入れると、以下のような内容だということがわかります。

// valueのnullチェック
IL_0000:  ldarg.2
IL_0001:  brtrue.s   IL_0010

// nullの時
IL_0003:  ldarg.1
IL_0004:  castclass  EmitExample.Data
IL_0009:  ldc.i4.0
IL_000a:  callvirt   instance void EmitExample.Data::set_IntValue(int32)
IL_000f:  ret

// null以外の時
IL_0010:  ldarg.1
IL_0011:  castclass  EmitExample.Data
IL_0016:  ldarg.2
IL_0017:  unbox.any  [mscorlib]System.Int32
IL_001c:  callvirt   instance void EmitExample.Data::set_IntValue(int32)
IL_0021:  ret

このように、サンプルの記述によって生成されるILも変わってくるので、生成されるILのわかりやすさと効率を考慮しながらサンプルを修正していきます。

そして生成されるILが望ましい形となったら、その内容で生成コードを書いていきます。

GetValueのValueTypeへの対応

GetValueについては、元のコードにboxの対応を入れれば良いだけなので、既存のコードへ処理を追加して以下のような形となります。

var ilGenerator = method.GetILGenerator();

ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Castclass, pi.DeclaringType);
ilGenerator.Emit(OpCodes.Callvirt, pi.GetGetMethod());

// ValueTypeへの追加分
if (pi.PropertyType.IsValueType)
{
    // Box化する
    ilGenerator.Emit(OpCodes.Box, pi.PropertyType);
}

ilGenerator.Emit(OpCodes.Ret);

SetValueのValueTypeへの対応

SetValueについてですが、既存のコードへの処理追加を考えた場合、まずValueTypeかどうかで処理を分岐した方がわかりやすくなると考えられます。

そこで、まずは以下の用な形として、コメントの部分にValueTypeの時のコード生成処理を追加していきたいと思います。

var ilGenerator = method.GetILGenerator();

if (pi.PropertyType.IsValueType)
{
    // ここにValueType用のコードを追加
}
else
{
    // 以下はstringの時のコードに同じ
    ilGenerator.Emit(OpCodes.Ldarg_1);
    ilGenerator.Emit(OpCodes.Castclass, pi.DeclaringType);

    ilGenerator.Emit(OpCodes.Ldarg_2);
    ilGenerator.Emit(OpCodes.Castclass, pi.PropertyType);

    ilGenerator.Emit(OpCodes.Callvirt, pi.GetSetMethod());

    ilGenerator.Emit(OpCodes.Ret);
}

ValueTypeの時のコードについては、ILの内容を確認すると下記のように最初にnullかどうかの分岐がある内容となっています。

// valueのnullチェック
IL_0000:  ldarg.2
IL_0001:  brtrue.s   IL_0010

// nullの時
IL_0003:  ldarg.1
...
IL_000f:  ret

// null以外の時
IL_0010:  ldarg.1
...
IL_0021:  ret

まずはこの構造を再現するため、ValueType用のアウトラインを以下のように作り、nullの時とnull以外の時の処理を埋めていく形にしたいと思います。

// ValueType用のコード
var hasValue = ilGenerator.DefineLabel();

ilGenerator.Emit(OpCodes.Ldarg_2);
ilGenerator.Emit(OpCodes.Brtrue_S, hasValue);

// nullの時
// ...
ilGenerator.Emit(OpCodes.Ret);

// null以外の時
ilGenerator.MarkLabel(hasValue);
// ...
ilGenerator.Emit(OpCodes.Ret);

null以外の時

まずは簡単なnull以外の時のコードについて、サンプルのILを抜粋して再度確認してみます。

// null以外の時
IL_0010:  ldarg.1
IL_0011:  castclass  EmitExample.Data
IL_0016:  ldarg.2
IL_0017:  unbox.any  [mscorlib]System.Int32
IL_001c:  callvirt   instance void EmitExample.Data::set_IntValue(int32)
IL_0021:  ret

この部分だけ見ると内容はstringの処理と比べてunbox.anyが増えているくらいなので、以下のようにIL生成コードにすることは難しくないと思います。

// 分岐で飛んでくるラベルの定義
ilGenerator.MarkLabel(hasValue);
// インデックス1の引数(target)をスタックに積んでpi.DeclaringType(Data型)にキャスト
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Castclass, pi.DeclaringType);
// インデックス2の引数(value)をスタックに積んでUnbox
ilGenerator.Emit(OpCodes.Ldarg_2);
ilGenerator.Emit(OpCodes.Unbox_Any, pi.PropertyType);
// スタック上にあるオブジェクトのプロパティ(IntValue)のSetメソッドを呼び出す
ilGenerator.Emit(OpCodes.Callvirt, pi.GetSetMethod());
// 戻る
ilGenerator.Emit(OpCodes.Ret);

nullの時

次にnullの時の処理ですが、null以外の時と比べて考慮が必要な点があります。

まずはサンプルのILを抜粋して再度確認してみます。

// インデックス1の引数(target)をスタックに積んでpi.DeclaringType(Data型)にキャスト
IL_0003:  ldarg.1
IL_0004:  castclass  EmitExample.Data

// 0(intの初期値)をスタック
IL_0009:  ldc.i4.0

// スタック上にあるオブジェクトのプロパティ(IntValue)のSetメソッドを呼び出す
IL_000a:  callvirt   instance void EmitExample.Data::set_IntValue(int32)

// 戻る
IL_000f:  ret

考慮が必要なのは、上記の初期値のスタックの部分で、型によって設定する初期値の値が異なるため、その対応が必要となります。

初期値について、まずはプリミティブ型への対応のみを考えて、それ以外のValueTypeへの対応は次章以降に記述します。

プリミティブ型への対応については、下記のようなテーブルを用意して処理を行いたいと思います。

private static readonly Dictionary<Type, Action<ILGenerator>> LdcDictionary = new Dictionary<Type, Action<ILGenerator>>
{
    { typeof(bool), il => il.Emit(OpCodes.Ldc_I4_0) },
    { typeof(byte), il => il.Emit(OpCodes.Ldc_I4_0) },
    { typeof(char), il => il.Emit(OpCodes.Ldc_I4_0) },
    { typeof(short), il => il.Emit(OpCodes.Ldc_I4_0) },
    { typeof(int), il => il.Emit(OpCodes.Ldc_I4_0) },
    { typeof(sbyte), il => il.Emit(OpCodes.Ldc_I4_0) },
    { typeof(ushort), il => il.Emit(OpCodes.Ldc_I4_0) },
    { typeof(uint), il => il.Emit(OpCodes.Ldc_I4_0) },
    { typeof(long), il => il.Emit(OpCodes.Ldc_I8, 0L) },
    { typeof(ulong), il => il.Emit(OpCodes.Ldc_I8, 0L) },
    { typeof(float), il => il.Emit(OpCodes.Ldc_R4, 0f) },
    { typeof(double), il => il.Emit(OpCodes.Ldc_R8, 0d) },
    { typeof(IntPtr), il => il.Emit(OpCodes.Ldc_I4_0) },
    { typeof(UIntPtr), il => il.Emit(OpCodes.Ldc_I4_0) },
};

このテーブルを使うと、ValueTypeにnullを設定されたときの処理は以下のように記述できることになります。

// インデックス1の引数(target)をスタックに積んでpi.DeclaringType(Data型)にキャスト
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Castclass, pi.DeclaringType);

// プリミティブ型なら対応する初期値をスタック
if (LdcDictionary.TryGetValue(pi.PropertyType, out var action))
{
    action(ilGenerator);
}
else
{
    // プリミティブ以外の処理(次章以降で解説)
}

// スタック上にあるオブジェクトのプロパティのSetメソッドを呼び出す
ilGenerator.Emit(OpCodes.Callvirt, pi.GetSetMethod());
// 戻る
ilGenerator.Emit(OpCodes.Ret);

structへの対応

プリミティブ以外への対応を行うため、以下のようなサンプルを作ってその結果を逆アセンブルしてみます。

なお、GetValueについては生成されるILは既存の処理と同じになるため省略しています。

public struct MyStruct
{
    public int X { get; set; }

    public int Y { get; set; }
}
public class StructPropertyData
{
    public MyStruct StructValue { get; set; }
}
public void SetValue(object target, object value)
{
    if (value == null)
    {
        ((StructPropertyData)target).StructValue = default(MyStruct);
    }
    else
    {
        ((StructPropertyData)target).StructValue = (MyStruct)value;
    }
}

SetValueの逆アセンブル結果から初期値による設定部分を抜粋すると、以下のようなILになっていることがわかります。

.locals init (valuetype EmitExample.MyStruct V_0)

IL_0003:  ldarg.1
IL_0004:  castclass  EmitExample.StructPropertyData

// MyStructの初期値をスタック
IL_0009:  ldloca.s   V_0
IL_000b:  initobj    EmitExample.MyStruct
IL_0011:  ldloc.0

IL_0012:  callvirt   instance void EmitExample.StructPropertyData::set_StructValue(valuetype EmitExample.MyStruct)
IL_0017:  ret

ローカス変数にValueTypeを初期化して、それを設定する処理となっています。

// ローカル変数の宣言
var local = ilGenerator.DeclareLocal(pi.PropertyType);
// ローカル変数のアドレスをスタック
ilGenerator.Emit(OpCodes.Ldloca_S, local);
// 型の初期化
ilGenerator.Emit(OpCodes.Initobj, pi.PropertyType);
// ローカル変数をスタック
ilGenerator.Emit(OpCodes.Ldloc_0);

これで、プリミティブ以外の構造体への対応もできました。

Enumへの対応

さて、これまでの処理でValueTypeの初期値に関する処理は完成かというと、Enumへの対応が正しくありません。

まず、Enumを扱うサンプルを作って、その逆アセンブル結果を見てみます。

public enum MyEnum
{
    Zero, One, Two
}
public class EnumPropertyData
{
    public MyEnum EnumValue { get; set; }
}
public void SetValue(object target, object value)
{
    if (value == null)
    {
        ((EnumPropertyData)target).EnumValue = default(MyEnum);
    }
    else
    {
        ((EnumPropertyData)target).EnumValue = (MyEnum)value;
    }
}

SetValueの逆アセンブル結果から初期値による設定部分を抜粋すると、以下のようなILになっていることがわかります。

IL_0003:  ldarg.1
IL_0004:  castclass  EmitExample.EnumPropertyData
IL_0009:  ldc.i4.0
IL_000a:  callvirt   instance void EmitExample.EnumPropertyData::set_EnumValue(valuetype EmitExample.MyEnum)
IL_000f:  ret

初期値はldc.i4.0で設定され、これはMyEnumの基となっているint型の初期値の設定処理に同じです。

Enum型の基となっている型はGetEnumUnderlyingType()で取得できるので、これまでのコードを以下の形に変更することでEnumへの対応もできることになります。

// Enumならその基となる型で、初期値用の処理をテーブルから取得
var type = pi.PropertyType.IsEnum ? pi.PropertyType.GetEnumUnderlyingType() : pi.PropertyType;
if (LdcDictionary.TryGetValue(type, out var action))
{
    action(ilGenerator);
}
else
{
    // structの処理
    var local = ilGenerator.DeclareLocal(pi.PropertyType);
    ilGenerator.Emit(OpCodes.Ldloca_S, local);
    ilGenerator.Emit(OpCodes.Initobj, pi.PropertyType);
    ilGenerator.Emit(OpCodes.Ldloc_0);
}

読み取り専用/書き込み専用のプロパティ

他に考慮漏れの処理がないかというと、プロパティが読み取り専用/書き込み専用の場合の考慮が漏れています。

プロパティが読み書きの処理に対応していない場合には、NotSupportedExceptionをスルーする形にしたいと思います。

この処理のILについては、以下のような形で生成します。

var ilGenerator = method.GetILGenerator();

// 書き込みに対応していない場合
if (!pi.CanWrite)
{
    // NotSupportedExceptionを作って
    ilGenerator.Emit(OpCodes.Newobj, typeof(NotSupportedException).GetConstructor(Type.EmptyTypes));
    // throwする
    ilGenerator.Emit(OpCodes.Throw);
    return;
}

// ...既存のIL生成コードの記述

完成形のコード

一通りの考慮ができたと思うので、GetValue及びSetValueのIL生成部分について完成形を確認しておきます。

  • GetValue
var ilGenerator = method.GetILGenerator();

if (!pi.CanRead)
{
    ilGenerator.Emit(OpCodes.Newobj, typeof(NotSupportedException).GetConstructor(Type.EmptyTypes));
    ilGenerator.Emit(OpCodes.Throw);
    return;
}

ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Castclass, pi.DeclaringType);
ilGenerator.Emit(OpCodes.Callvirt, pi.GetGetMethod());
if (pi.PropertyType.IsValueType)
{
    ilGenerator.Emit(OpCodes.Box, pi.PropertyType);
}

ilGenerator.Emit(OpCodes.Ret);
  • SetValue
var ilGenerator = method.GetILGenerator();

if (!pi.CanWrite)
{
    ilGenerator.Emit(OpCodes.Newobj, typeof(NotSupportedException).GetConstructor(Type.EmptyTypes));
    ilGenerator.Emit(OpCodes.Throw);
    return;
}

if (pi.PropertyType.IsValueType)
{
    var hasValue = ilGenerator.DefineLabel();

    ilGenerator.Emit(OpCodes.Ldarg_2);
    ilGenerator.Emit(OpCodes.Brtrue_S, hasValue);

    // null
    ilGenerator.Emit(OpCodes.Ldarg_1);
    ilGenerator.Emit(OpCodes.Castclass, pi.DeclaringType);

    var type = pi.PropertyType.IsEnum ? pi.PropertyType.GetEnumUnderlyingType() : pi.PropertyType;
    if (LdcDictionary.TryGetValue(type, out var action))
    {
        action(ilGenerator);
    }
    else
    {
        var local = ilGenerator.DeclareLocal(pi.PropertyType);
        ilGenerator.Emit(OpCodes.Ldloca_S, local);
        ilGenerator.Emit(OpCodes.Initobj, pi.PropertyType);
        ilGenerator.Emit(OpCodes.Ldloc_0);
    }

    ilGenerator.Emit(OpCodes.Callvirt, pi.GetSetMethod());

    ilGenerator.Emit(OpCodes.Ret);

    // not null
    ilGenerator.MarkLabel(hasValue);

    ilGenerator.Emit(OpCodes.Ldarg_1);
    ilGenerator.Emit(OpCodes.Castclass, pi.DeclaringType);

    ilGenerator.Emit(OpCodes.Ldarg_2);
    ilGenerator.Emit(OpCodes.Unbox_Any, pi.PropertyType);

    ilGenerator.Emit(OpCodes.Callvirt, pi.GetSetMethod());

    ilGenerator.Emit(OpCodes.Ret);
}
else
{
    ilGenerator.Emit(OpCodes.Ldarg_1);
    ilGenerator.Emit(OpCodes.Castclass, pi.DeclaringType);

    ilGenerator.Emit(OpCodes.Ldarg_2);
    ilGenerator.Emit(OpCodes.Castclass, pi.PropertyType);

    ilGenerator.Emit(OpCodes.Callvirt, pi.GetSetMethod());

    ilGenerator.Emit(OpCodes.Ret);
}

本当に完成したか確認

var pi = typeof(Data).GetProperty(nameof(Data.StringValue));
var accessor = EmitFactory.Default.CreateAccessor(pi);

var data = new Data();

accessor.SetValue(data, "abc");
Debug.Assert((string)accessor.GetValue(data) == "abc");

accessor.SetValue(data, null);
Debug.Assert((string)accessor.GetValue(data) == null);
var pi = typeof(Data).GetProperty(nameof(Data.IntValue));
var accessor = EmitFactory.Default.CreateAccessor(pi);

var data = new Data();

accessor.SetValue(data, 1);
Debug.Assert((int)accessor.GetValue(data) == 1);

accessor.SetValue(data, null);
Debug.Assert((int)accessor.GetValue(data) == 0);
var pi = typeof(EnumPropertyData).GetProperty(nameof(EnumPropertyData.EnumValue));
var accessor = EmitFactory.Default.CreateAccessor(pi);

var data = new EnumPropertyData();

accessor.SetValue(data, MyEnum.One);
Debug.Assert((MyEnum)accessor.GetValue(data) == MyEnum.One);

accessor.SetValue(data, null);
Debug.Assert((MyEnum)accessor.GetValue(data) == default(MyEnum));
var pi = typeof(StructPropertyData).GetProperty(nameof(StructPropertyData.StructValue));
var accessor = EmitFactory.Default.CreateAccessor(pi);

var data = new StructPropertyData();

accessor.SetValue(data, new MyStruct { X = 1, Y = 2 });
var structValue = (MyStruct)accessor.GetValue(data);
Debug.Assert(structValue.X == 1);
Debug.Assert(structValue.Y == 2);

accessor.SetValue(data, null);
structValue = (MyStruct)accessor.GetValue(data);
Debug.Assert(structValue.X == 0);
Debug.Assert(structValue.Y == 0);

まとめ

前回と今回の内容で、ILのこねかたについてのイメージが出来たのではないかと思います。

逆アセンブル結果を再現する方法であれば、正解があるわけですから動的生成コードの実装も容易となります。

また、interfaceを用意してその実装クラスを動的に作成するという方法については、動的生成がサポートされない環境において、コンパイルタイムのコード生成に切り替えるといった事も容易となるため、お勧めの方法だと考えます。

なお、前回と今回の内容に関して、ソース全体についてはEmitExampleのコードを参考にしてください。

うさコメ

ILがこねられるようになれば、もうなにも怖くない(`・ω・´)

魔力は満ち、運命の扉は今開かれた。いざ約束の地へ!(これでみなさんもILの先生です。Let's enjoy IL life)