Search code examples
c#cilreflection.emit

IL Calling a method with params object[] arguments using Reflection.Emit


I'm writing a library that requires a later type build. Library uses platform .Net core 2.0

There is issue with some type that I am generating using Reflection.Emit

public class GeneratedA : A, IA
{
    public void DoInterface(string arg0, bool arg1, int arg2, object arg3, List<float> arg4, params object[] otherArgs)
    {
        DoClass(arg0, arg1, arg2, arg3, arg4, otherArgs);
    }
}

for these types:

public interface IA
{
    void DoInterface(string arg0, bool arg1, int arg2, object arg3, List<float> arg4,  params object[] otherArgs);
}
public class A
{
    public void DoClass(params object[] args)
    {
    }
}

Sample IL code:

class Program
{
    public static class Generator
    {
        public static T Create<T>()
            where T : class
        {
            AssemblyName aName = new AssemblyName("DynamicAssembly");
            AssemblyBuilder ab = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(Guid.NewGuid().ToString()), AssemblyBuilderAccess.Run);
            ModuleBuilder mb = ab.DefineDynamicModule(aName.Name);

            var interfaceType = typeof(T);
            var interfaceMethod = interfaceType.GetMethod("DoInterface");
            var interfaceMethodArgs = interfaceMethod.GetParameters().Select(x => x.ParameterType).ToArray();
            var classType = typeof(A);
            var classMethod = classType.GetMethod("DoClass");
            var returnType = typeof(void);
            var baseType = typeof(object);
            var baseConstructor = baseType.GetConstructor(BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance, null, Type.EmptyTypes, null);


            TypeBuilder tb = mb.DefineType("GeneratedA", TypeAttributes.Public, baseType);


            ConstructorBuilder ctor = tb.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes);
            ILGenerator ctorIL = ctor.GetILGenerator();
            ctorIL.Emit(OpCodes.Ldarg_0);
            ctorIL.Emit(OpCodes.Call, baseConstructor);
            ctorIL.Emit(OpCodes.Nop);
            ctorIL.Emit(OpCodes.Nop);
            ctorIL.Emit(OpCodes.Ret);


            tb.AddInterfaceImplementation(interfaceType);

            MethodBuilder mbIM = tb.DefineMethod(interfaceType.Name + "." + interfaceMethod.Name,
                MethodAttributes.Private | MethodAttributes.HideBySig |
                MethodAttributes.NewSlot | MethodAttributes.Virtual |
                MethodAttributes.Final,
                returnType,
                interfaceMethodArgs);
            ILGenerator genIM = mbIM.GetILGenerator();
            // ToDo
            genIM.Emit(OpCodes.Call, classMethod);
            genIM.Emit(OpCodes.Ret);


            tb.DefineMethodOverride(mbIM, interfaceMethod);

            Type t = tb.CreateType();

            return Activator.CreateInstance(t) as T;
        }

    }

    static void Main(string[] args)
    {
        IA a;
        a = new GeneratedA();
        a.DoInterface("0", true, 2, 3, new List<float>() { 4 }, "5", 6);
        a = Generator.Create<IA>();
        a.DoInterface("0", true, 2, 3, new List<float>() { 4 }, "5", 6);
    }
}

Whenever I try to fill out a comment "ToDo", I get an error "Common Language Runtime detected an invalid program".

I ask to help with a call of method DoClass.

Thanks


Solution

  • To call DoClass method you need to provide arguments, just Call classMethod isn't going to work.

    First argument is of course "this" reference:

    genIM.Emit(OpCodes.Ldarg_0);
    

    Second argument is array of objects. params is compiler feature, if you build code yourself - you have to treat it as if params is not there. What I mean is - while

     DoClass();
    

    is legal when you write it in code - this is compiled as:

    DoClass(new object[0]);
    

    So when you are emitting this call - you should always provide array of objects argument, you cannot omit it.

    To push array of objects to stack:

    // push array length (0, for example) to stack
    genIM.Emit(OpCodes.Ldc_I4_0); 
    // push new array with length given by the above value (0)
    genIM.Emit(OpCodes.Newarr, typeof(object)); 
    

    At this point your code will compile and run fine. This is analog of:

    public class GeneratedA : A, IA
    {
        public void DoInterface(string arg0, bool arg1, int arg2, object arg3, List<float> arg4, params object[] otherArgs)
        {
            DoClass();
        }
    }
    

    If you want to pass all arguments of DoInterface, that requires more work. I'll provide couple examples. To pass first argument (string arg0):

    genIM.Emit(OpCodes.Dup);
    // push index to store next element at (0)
    genIM.Emit(OpCodes.Ldc_I4_0);
    // push first argument (arg0 of DoInterface) to stack
    genIM.Emit(OpCodes.Ldarg_1);
    // store element in array at given index (yourArguments[0] = arg0)
    genIM.Emit(OpCodes.Stelem_Ref);
    

    To pass second argument:

    genIM.Emit(OpCodes.Dup);
    // push index to store next element at (1)
    genIM.Emit(OpCodes.Ldc_I4_1);
    // push arg2
    genIM.Emit(OpCodes.Ldarg_2);
    // box, because boolean is value type, and you store it in object array
    genIM.Emit(OpCodes.Box, typeof(bool));
    // store in array (yourArguments[1] = (object) arg2
    genIM.Emit(OpCodes.Stelem_Ref);
    

    And so on.

    When you will push arguments into your array, don't forget to change its length to reflect number of arguments:

    // push array length - 6
    genIM.Emit(OpCodes.Ldc_I4_6); 
    // push new array with length given by the above value (6)
    genIM.Emit(OpCodes.Newarr, typeof(object)); 
    

    Note also that you might want to change:

    TypeBuilder tb = mb.DefineType("GeneratedA", TypeAttributes.Public, baseType);
    

    to

    TypeBuilder tb = mb.DefineType("GeneratedA", TypeAttributes.Public, classType);
    

    Or just change to baseType = typeof(A), because you want to inherit your generated class from A, not from object.

    Full code to emit call with first 2 args:

    ILGenerator genIM = mbIM.GetILGenerator();            
    genIM.Emit(OpCodes.Ldarg_0);   
    genIM.Emit(OpCodes.Ldc_I4_2);
    genIM.Emit(OpCodes.Newarr, typeof(object));
    
    genIM.Emit(OpCodes.Dup);
    genIM.Emit(OpCodes.Ldc_I4_0);
    genIM.Emit(OpCodes.Ldarg_1);
    genIM.Emit(OpCodes.Stelem_Ref);
    
    genIM.Emit(OpCodes.Dup);
    genIM.Emit(OpCodes.Ldc_I4_1);
    genIM.Emit(OpCodes.Ldarg_2);
    genIM.Emit(OpCodes.Box, typeof(bool));
    genIM.Emit(OpCodes.Stelem_Ref);
    
    genIM.Emit(OpCodes.Call, classMethod);
    genIM.Emit(OpCodes.Ret);