How to generate call to base constructor with VarArgs using ILGenerator

915 views Asked by At

If I decompile the Test2 constructor:

public class Test2 : VarArgTest
{
    public Test2() : base("foo", __arglist("one", 2))
    {

    }
}

public class VarArgTest
{
    public VarArgTest(string test, __arglist)
    {

    }
}

I get this IL:

IL_0000:  ldarg.0
IL_0001:  ldstr      "foo"
IL_0006:  ldstr      "one"
IL_000b:  ldc.i4.2
IL_000c:  call       instance vararg void VarargsTest.VarArgTest::.ctor(string,
                                                                        ...,
                                                                        string,
                                                                        int32)

I'm trying to generate the same IL stream using the ILGenerator but EmitCall only takes a MethodInfo not a ConstructorInfo and the only Emit overload that takes a ConstructorInfo has no support for passing in additional parameter types.

3

There are 3 answers

0
Michael B On BEST ANSWER

Okay, After reading this post

I discovered an incredibly easier method of doing this: You can get a token for the constructor with optional arguments from your method builder. Why this is so unbelievably undocumented is a mystery. A similar version of the program in my previous answer is below that does the same thing but with just using this get methodtoken api. This is much easier!

var ab = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Foo"), AssemblyBuilderAccess.RunAndSave);
var mb = ab.DefineDynamicModule(ab.GetName().Name, ab.GetName().Name + ".dll", true);
var tb = mb.DefineType("Foo", TypeAttributes.BeforeFieldInit | TypeAttributes.AnsiClass | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.Public, typeof(VarArgTest));
var ctor = tb.DefineConstructor(MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, CallingConventions.HasThis, Type.EmptyTypes);
var il = ctor.GetILGenerator();
var token= mb.GetConstructorToken(typeof(VarArgTest).GetConstructors()[0], new[] { typeof(string), typeof(int) });
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldstr, "foo");
il.Emit(OpCodes.Ldstr, "one");
il.Emit(OpCodes.Ldc_I4_2);
il.Emit(OpCodes.Call,token.Token);
il.Emit(OpCodes.Ret);
var v = Activator.CreateInstance(tb.CreateType());
Console.WriteLine((v as VarArgTest).CountOfArgs);
1
kvb On

It looks like this isn't possible; I suspect it's merely an oversight that MethodInfo was used as the input type instead of MethodBase, since it appears to be perfectly valid to have varargs .ctors. You could try filing a bug, but I suspect that this is a low priority scenario to support, given that varargs methods aren't CLS compliant.

0
Michael B On

Sadly it appears @kvb is correct. That said, the method you want to do the hard work "GetMethodToken" does appear to take a methodbase. So if you aren't opposed to expression trees you can make your own version of the method. Here is my best guess (minus argument validation) I haven't fully test it in every scenario, bUt you can now use func as a method to call a vararg method or a vararg ctor.
::

var ilgen = Expression.Parameter(typeof(ILGenerator));
var code = Expression.Parameter(typeof(OpCode));
var method = Expression.Parameter(typeof(MethodBase));
var opttypes = Expression.Parameter(typeof(Type[]));
var stackchange = Expression.Variable(typeof(int));
var tok = Expression.Variable(typeof(int));
var paramTypes= Expression.Variable(typeof(Type[]));
var expr = Expression.Lambda<Action<ILGenerator, OpCode, MethodBase, Type[]>>(Expression.Block(
new[]{stackchange,tok,paramTypes},
Expression.Assign(stackchange, Expression.Constant(0)),
Expression.Assign(tok,
    Expression.Call(ilgen,
        typeof(ILGenerator)
            .GetMethod("GetMethodToken", BindingFlags.NonPublic | BindingFlags.Instance),
        method,
        opttypes,
        Expression.Constant(false)
    )
),
Expression.Call(ilgen, typeof(ILGenerator).GetMethod("EnsureCapacity", BindingFlags.Instance | BindingFlags.NonPublic), Expression.Constant(7)),
Expression.Call(ilgen, typeof(ILGenerator).GetMethod("InternalEmit",BindingFlags.Instance|BindingFlags.NonPublic), code),
Expression.IfThen(
    Expression.AndAlso(
        Expression.Not(
            Expression.Property(method, "IsConstructor")
        ),
        Expression.Equal(
            Expression.Property(
                Expression.Convert(method, typeof(MethodInfo)),
                "ReturnType"
            ),
            Expression.Constant(typeof(void))
        )
    ),
    Expression.PostIncrementAssign(stackchange)
),
Expression.Assign(paramTypes, Expression.Call(method,
    typeof(MethodInfo)
        .GetMethod("GetParameterTypes", BindingFlags.NonPublic | BindingFlags.Instance)
    )
),
Expression.IfThen(
    Expression.AndAlso(
        Expression.AndAlso(
            Expression.TypeIs(method, Type.GetType("System.Reflection.Emit.SymbolMethod")),
            Expression.Property(method, "IsStatic")
        ),
        Expression.Equal(
            code,
            Expression.Constant(OpCodes.Newobj, typeof(OpCode))
        )
    ),
    Expression.PostDecrementAssign(stackchange)
),
Expression.IfThen(Expression.NotEqual(opttypes, Expression.Constant(null)),
    Expression.SubtractAssign(stackchange, Expression.ArrayLength(opttypes))
),
Expression.Call(ilgen, typeof(ILGenerator).GetMethod("UpdateStackSize", BindingFlags.NonPublic | BindingFlags.Instance), code, stackchange),
Expression.Call(ilgen, typeof(ILGenerator).GetMethod("RecordTokenFixup", BindingFlags.NonPublic | BindingFlags.Instance)),
Expression.Call(ilgen, typeof(ILGenerator).GetMethod("PutInteger4", BindingFlags.NonPublic | BindingFlags.Instance),tok)
),
ilgen, code, method, opttypes);
var func = expr.Compile();

From here you can use it on your type like so:

public class VarArgTest
{
    public int CountOfArgs;
    public VarArgTest(string test, __arglist)
    {
        ArgIterator args = new ArgIterator(__arglist);
        CountOfArgs = args.GetRemainingCount();
    }
}

//then in the method to create the class

var ab = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Foo"), AssemblyBuilderAccess.RunAndSave);
var mb = ab.DefineDynamicModule(ab.GetName().Name, ab.GetName().Name + ".dll", true);
var tb = mb.DefineType("Foo", TypeAttributes.BeforeFieldInit | TypeAttributes.AnsiClass | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.Public, typeof(VarArgTest));
var ctor = tb.DefineConstructor(MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,CallingConventions.HasThis,Type.EmptyTypes);
var il =ctor.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldstr, "foo");
il.Emit(OpCodes.Ldstr, "one");
il.Emit(OpCodes.Ldc_I4_2);
func(il, OpCodes.Call, typeof(VarArgTest).GetConstructors()[0], new[] { typeof(string), typeof(int) });
il.Emit(OpCodes.Ret);
var v=Activator.CreateInstance(tb.CreateType());
Console.WriteLine((v as VarArgTest).CountOfArgs);

Prints 2.