3

I have the following class:

private sealed class Person
{
    public string Name { get; }
    public int Age { get; }

    public Person(string name)
    {
        Name = name;
    }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

And the following method to dynamically create an instance of a ConstructorInfo:

public static Func<object[], T> GetBuilder<T>(ConstructorInfo constructor)
{
    var type = constructor.ReflectedType;    
    var ctorParams = constructor.GetParameters();

    var dynamicMethod = new DynamicMethod("Create_" + constructor.Name, type, new[] { typeof(object[]) }, type, true);
    var ilGen = dynamicMethod.GetILGenerator();

    /*
     * Cast each argument of the input object array to the appropriate type
     * The order of objects should match the order set by the Ctor
     * It is also assumed the length of object array args is same length as Ctor args. 
     * Exceptions for the delegate that mean the above weren't satisfied: 
     * InvalidCastException, IndexOutOfRangeException
     */
    for (var i = 0; i < ctorParams.Length; i++)
    {
        // Push Object array
        ilGen.Emit(OpCodes.Ldarg_0);

        // Push the index to access
        ilGen.Emit(OpCodes.Ldc_I4, i);

        // Push the element at the previously loaded index
        ilGen.Emit(OpCodes.Ldelem_Ref);

        // Cast the object to the appropriate Ctor Parameter Type
        var paramType = ctorParams[i].ParameterType;
        ilGen.Emit(paramType.IsValueType ? OpCodes.Box : OpCodes.Castclass, paramType);
    }

    // Call the Ctor, all values on the stack are passed to the Ctor
    ilGen.Emit(OpCodes.Newobj, constructor);
    // Return the new object
    ilGen.Emit(OpCodes.Ret);

    // Create delegate from our IL, cast and return
    return (Func<object[], T>)dynamicMethod.CreateDelegate(typeof(Func<object[], T>));
}

I then use the method to create two instances of this class one for each of the constructors:

var ctorOne = typeof(Person).GetConstructors(BindingFlags.Public | BindingFlags.Instance)[0];
var instanceBuilderOne = GetBuilder<Person>(publicCtor[0]);
var instanceOne = instanceBuilderOne(new object[] { "Foo"});
instanceOne.Name.Dump(); // is "Foo"

var ctorTwo = typeof(Person).GetConstructors(BindingFlags.Public | BindingFlags.Instance)[1];
var instanceBuilderTwo = GetBuilder<Person>(publicCtor[1]);
var instanceTwo = instanceBuilderTwo(new object[] { "Bar", 1});
instanceTwo.Name.Dump(); // is "Bar"
instanceTwo.Age.Dump(); // is 43603896

However for the instanceTwo instead of getting 1 I am getting 43603896.

Hitting the breakpoint in the related constructor does indeed show 43603896 being passed to the instance but I cannot figure out why!?

MaYaN
  • 6,683
  • 12
  • 57
  • 109
  • are `publicCtor[0]` and `publicCtor[1]` typos? – Darren Wainwright Dec 16 '16 at 20:53
  • No, one is the 1st constructor which gets one param of type `string`, the other is the 2nd which gets one param of type `string` and another of type `int`. – MaYaN Dec 16 '16 at 20:54
  • 1
    Shouldn't you use `Unbox` instead of `Box`? The value was already boxed when pushed into `object[]`. Now you want to get it back as `int`. What you're getting looks like a random number which might represent a *location* of the value you're boxing. – MarcinJuraszek Dec 16 '16 at 20:54
  • @MarcinJuraszek, no that's not it, changing it didn't make a difference. The value is indeed random though. – MaYaN Dec 16 '16 at 20:55
  • This question really serves to remind me how little I still know about C# ... Can `GetBuilder()` differentiate 1 as an `int` versus some other type? Does this still happen if you change 1 to something like 42? – levelonehuman Dec 16 '16 at 21:00
  • 1
    @levelonehuman, The issue is independent of the value, however, if I change the constructor param from `int` to `string` it successfully gets the 2nd param but somehow it seems to me the **ValueType** is being messed up during `boxing`/`unboxing`. – MaYaN Dec 16 '16 at 21:02
  • 1
    Maybe [this question](http://stackoverflow.com/questions/5750585/value-type-conversion-in-dynamically-generated-il) can help. – levelonehuman Dec 16 '16 at 21:05
  • 2
    You have to replace your OpCodes.Box with OpCodes.Unbox_Any. Actually you can replace your ValueType check with "ilGen.Emit(OpCodes.Unbox_Any, paramType);", because it will work for reference types too. – Evk Dec 16 '16 at 21:06
  • 1
    @Evk That was it! What's the difference though between `Unbox` and `Unbox_Any`? Feel free to add it as an answer and I will mark it as accepted. – MaYaN Dec 16 '16 at 21:08

1 Answers1

4

First, OpCodes.Box is obviously wrong here, because you want to unbox int from object, not box it.

Now what OpCodes.Unbox does is it unboxes value and pushes reference to unboxed value to the stack. That reference is what you see instead of "1". If you want to use OpCodes.Unbox, correct way is this:

if (paramType.IsValueType) {
     ilGen.Emit(OpCodes.Unbox, paramType);
     ilGen.Emit(OpCodes.Ldobj, paramType);
}
else {
      ilGen.Emit(OpCodes.Castclass, paramType);
}

But easier to just use OpCodes.Unbox_Any which will basically do the same, but in one line:

ilGen.Emit(OpCodes.Unbox_Any, paramType);
Evk
  • 98,527
  • 8
  • 141
  • 191