Unboxing Nullable when Emitting code for a method leaves evaluation stack in an unexpected (to me) state

c# dapper dynamicmethod reflection.emit unboxing

Question

Overview (forgive me for being so detailed, but I'd rather it be too much than too little): I'm attempting to edit the Dapper source for our solution in such a way that when any DateTime or Nullable is read from the database, its DateTime.Kind property is always set to DateTimeKind.Utc.

In our system, all DateTimes coming from the front end are guaranteed to be in UTC time, and the database (Sql Server Azure) is storing them as DateTime type in UTC (We are not using DateTimeOffsets, we are just always making sure the DateTime is UTC before storing it in the DB.)

I have been reading all about how to generate code for DynamicMethods by using ILGenerator.Emit(...), and feel like I have a decent understanding of how it works with the evaluation stack, locals, etc. In my efforts to solve this issue, I have written small samples of code to help get me to the end goal. I wrote a DynamicMethod to take a DateTime as an argument, call DateTime.SpecifyKind, return the value. Then the same with DateTime? type, using its Nullable.Value property to get the DateTime for the SpecifyKind method.

This is where my problem comes in: In dapper, the DateTime (or DateTime? I don't actually know, but when I treat it as though it is either I am not getting what I expect) is boxed. So when I try to use OpCodes.Unbox or OpCodes.Unbox_Any, then treat the result as either DateTime or DateTime?, I get a VerificationException: Operation could destabilize the runtime.

Obviously I'm missing something important about boxing, but I'll give you my code samples and maybe you can help me get it working.

This works:

    [Test]
    public void Reflection_Emit_Test3()
    {
        //Setup
        var dm = new DynamicMethod("SetUtc", typeof(DateTime?), new Type[] {typeof(DateTime?)});

        var nullableType = typeof(DateTime?);

        var il = dm.GetILGenerator();

        il.Emit(OpCodes.Ldarga_S, 0); // [DateTime?]
        il.Emit(OpCodes.Call, nullableType.GetProperty("Value").GetGetMethod()); // [DateTime]
        il.Emit(OpCodes.Ldc_I4, (int)DateTimeKind.Utc); // [DateTime][Utc]
        il.Emit(OpCodes.Call, typeof(DateTime).GetMethod("SpecifyKind")); //[DateTime]
        il.Emit(OpCodes.Newobj, nullableType.GetConstructor(new[] {typeof (DateTime)})); //[DateTime?]
        il.Emit(OpCodes.Ret);

        var meth = (Func<DateTime?, DateTime?>)dm.CreateDelegate(typeof(Func<DateTime?, DateTime?>));

        DateTime? now = DateTime.Now;

        Assert.That(now.Value.Kind, Is.Not.EqualTo(DateTimeKind.Utc));

        //Act

        var nowUtc = meth(now);

        //Verify

        Assert.That(nowUtc.Value.Kind, Is.EqualTo(DateTimeKind.Utc));
    }

I get what I expect here. Yay! But it's not over yet, because we have unboxing to deal with...

    [Test]
    public void Reflection_Emit_Test4()
    {
        //Setup
        var dm = new DynamicMethod("SetUtc", typeof(DateTime?), new Type[] { typeof(object) });

        var nullableType = typeof(DateTime?);

        var il = dm.GetILGenerator();
        il.DeclareLocal(typeof (DateTime?));

        il.Emit(OpCodes.Ldarga_S, 0); // [object]
        il.Emit(OpCodes.Unbox_Any, typeof(DateTime?)); // [DateTime?]
        il.Emit(OpCodes.Call, nullableType.GetProperty("Value").GetGetMethod()); // [DateTime]
        il.Emit(OpCodes.Ldc_I4, (int)DateTimeKind.Utc); // [DateTime][Utc]
        il.Emit(OpCodes.Call, typeof(DateTime).GetMethod("SpecifyKind")); //[DateTime]
        il.Emit(OpCodes.Newobj, nullableType.GetConstructor(new[] { typeof(DateTime) })); //[DateTime?]
        il.Emit(OpCodes.Ret);

        var meth = (Func<object, DateTime?>)dm.CreateDelegate(typeof(Func<object, DateTime?>));

        object now = new DateTime?(DateTime.Now);

        Assert.That(((DateTime?) now).Value.Kind, Is.Not.EqualTo(DateTimeKind.Utc));

        //Act

        var nowUtc = meth(now);

        //Verify

        Assert.That(nowUtc.Value.Kind, Is.EqualTo(DateTimeKind.Utc));
    }

This just straight up won't run. I get the VerificationException, and then I cry in the corner for a while until I'm ready to try again.

I have tried expecting a DateTime instead of a DateTime? (after unbox, assume DateTime on eval stack, rather than DateTime?) but that fails as well.

Can someone please tell me what I'm missing?

Accepted Answer

When in doubt, write a minimal C# library that does the same thing, and see what that compiles to:

Your attempt seems to be equivalent to

using System;

static class Program {
    public static DateTime? SetUtc(object value) {
        return new DateTime?(DateTime.SpecifyKind(((DateTime?)value).Value, DateTimeKind.Utc));
    }
};

and this compiles to:

$ mcs test.cs -target:library -optimize+ && monodis test.dll
...
        IL_0000:  ldarg.0 
        IL_0001:  unbox.any valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.DateTime>
        IL_0006:  stloc.0 
        IL_0007:  ldloca.s 0
        IL_0009:  call instance !0 valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.DateTime>::get_Value()
        IL_000e:  ldc.i4.1 
        IL_000f:  call valuetype [mscorlib]System.DateTime valuetype [mscorlib]System.DateTime::SpecifyKind(valuetype [mscorlib]System.DateTime, valuetype [mscorlib]System.DateTimeKind)
        IL_0014:  newobj instance void valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.DateTime>::'.ctor'(!0)
        IL_0019:  ret 
...

Thee first difference with your version is that ldarg is used instead of ldarga. You want unbox.any to check the passed value, not a pointer to the passed value. (ldarga does work too in my tests, but ldarg makes more sense anyway.)

The second, and more relevant, difference with your version is that after unbox.any, the value is stored, and then a reference to that location is loaded. This is because the implicit this parameter of instance methods of value types have type ref T, rather than the T you're used to for instance methods of reference types. If I do include that stloc.0/ldloca.s 0, your code then passes its test, on my system.

However, as you unconditionally read the Value property after casting to DateTime?, you might as well cast straight to DateTime and avoid the problem entirely. The only difference would be which exception you get when a value of the wrong type is passed in.

If you instead want something like

public static DateTime? SetUtc(object value) {
    var local = value as DateTime?;
    return local == null ? default(DateTime?) : DateTime.SpecifyKind(local.Value, DateTimeKind.Utc);
}

then I would use something like

var label1 = il.DefineLabel();
var label2 = il.DefineLabel();

il.Emit(OpCodes.Ldarg_S, 0); // object
il.Emit(OpCodes.Isinst, typeof(DateTime)); // boxed DateTime
il.Emit(OpCodes.Dup); // boxed DateTime, boxed DateTime
il.Emit(OpCodes.Brfalse_S, label1); // boxed DateTime
il.Emit(OpCodes.Unbox_Any, typeof(DateTime)); // unboxed DateTime
il.Emit(OpCodes.Ldc_I4_1); // unboxed DateTime, int
il.Emit(OpCodes.Call, typeof(DateTime).GetMethod("SpecifyKind")); // unboxed DateTime
il.Emit(OpCodes.Newobj, typeof(DateTime?).GetConstructor(new[] { typeof(DateTime) })); // unboxed DateTime?
il.Emit(OpCodes.Br_S, label2);

il.MarkLabel(label1); // boxed DateTime (known to be null)
il.Emit(OpCodes.Unbox_Any, typeof(DateTime?)); // unboxed DateTime?

il.MarkLabel(label2); // unboxed DateTime?
il.Emit(OpCodes.Ret);



Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why