Search code examples
c#.net-corecilreflection.emit

.NET reflection emit - what am I doing wrong in writing this method in MSIL?


I am trying to dynamically create a delegate that returns a list of the values of all properties defined on an object, using reflection emit in C#.

I have started with the examples given here: https://learn.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/exczf7b9(v=vs.100)

Then, I have used LinqPad to view the IL for the code I want to emit. E.g., for this code:

void Main()
{
    new Zap{ Name = "1", Value = 2 }.Test();
}


public class Zap
{
    public string Name { get; set; }
    public int Value { get; set; }
    
    public IEnumerable<object> Test()
    {
        return new List<object> {Name, Value}; // This is what I want to do...
    }
}

I got this IL code for the Test() method:

IL code

But when I run this code, I get a System.InvalidProgramException: 'Common Language Runtime detected an invalid program.' exception:

new Target().DoMagic();

public class Target
{
    delegate List<object> ListProps();
    public string MyProperty1 { get; set; } = "zap";
    public int MyProperty2 { get; set; } = 77;

    public void DoMagic()
    {
        Type[] methodArgs = { };
        DynamicMethod listPropsMethod = new DynamicMethod("ListProps", typeof(List<object>), methodArgs, typeof(Target).Module);

        ConstructorInfo? constructor = typeof(List<object>).GetConstructor(new Type[0]);
        MethodInfo? addMethod = typeof(List<object>).GetMethod("Add");

        ILGenerator il = listPropsMethod.GetILGenerator();
        il.Emit(OpCodes.Newobj, constructor);

        il.Emit(OpCodes.Dup);
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Call, this.GetType().GetProperty(nameof(MyProperty1), BindingFlags.Instance | BindingFlags.Public).GetGetMethod());
        il.Emit(OpCodes.Callvirt, addMethod);

        il.Emit(OpCodes.Dup);
        il.Emit(OpCodes.Ldarg_0, this.GetType().GetProperty(nameof(MyProperty2), BindingFlags.Instance | BindingFlags.Public).GetGetMethod());
        il.Emit(OpCodes.Box);
        il.Emit(OpCodes.Callvirt, addMethod);

        il.Emit(OpCodes.Ret);

        var value = listProps();
        Console.WriteLine("Result : " + JsonSerializer.Serialize(value));
    }
}

I probably miss a lot of details (e.g., I don't see how calling methods would know which instances to use...), but if someone can shed some light on how I can make this work, I would be grateful.

Also, will invoking the generated delegate created with emit be as fast as writing the first code snippet and compiling it? I am troubled by the reflection code I use to get the method/property information, and I wonder if this might cause some performance issues.


Solution

  • I think this should work:

    delegate List<object> ListProps(Target obj);
    

    and:

    Type[] methodArgs = { typeof(Target) };
    DynamicMethod listPropsMethod = new DynamicMethod("ListProps", typeof(List<object>), methodArgs, typeof(Target).Module);
    
    ConstructorInfo? constructor = typeof(List<object>).GetConstructor(new Type[0]);
    MethodInfo? addMethod = typeof(List<object>).GetMethod("Add");
    
    ILGenerator il = listPropsMethod.GetILGenerator();
    il.Emit(OpCodes.Newobj, constructor);
    
    il.Emit(OpCodes.Dup);
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Callvirt, this.GetType().GetProperty(nameof(MyProperty1), BindingFlags.Instance | BindingFlags.Public).GetGetMethod());
    il.Emit(OpCodes.Callvirt, addMethod);
    
    il.Emit(OpCodes.Dup);
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Callvirt, this.GetType().GetProperty(nameof(MyProperty2), BindingFlags.Instance | BindingFlags.Public).GetGetMethod());
    il.Emit(OpCodes.Box, typeof(int));
    il.Emit(OpCodes.Callvirt, addMethod);
    
    il.Emit(OpCodes.Ret);
    
    var listProps = (ListProps)listPropsMethod.CreateDelegate(typeof(ListProps));
    Console.WriteLine("Result : {0}", string.Join(", ", listProps(this)));
    

    changes:

    • need to pass in a target for ldarg.0
    • need to specify a target type with box
    • need to split ldarg.0 and call / callvirt
    • always prefer callvirt on instance methods unless you really know why