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

1

5

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?

Paxwax answered 11/2, 2014 at 19:29 Comment(2)
Just of curiosity (and not necessarily related to your question): what is actually general purpose of using dynamically emitted code in such case? Is it just performance? Or sth else?Teagan
@KubaWyrostek The code I am attempting to modify (not these snippets above, though they are related) is inside Dapper ORM (it's a very fast, lightweight ORM; I believe Stackoverflow actually uses it behind the scenes). It is using reflection to create dynamic deserializer methods to map the SQL output objects to your specified domain objects with pretty much zero configuration! These deserializers are then cached to be used every time another one of your domain objects needs to be deserialized. So in this case it is satisfying my needs for minimal configuration and is great performance!Paxwax
F
7

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);
Fairlead answered 11/2, 2014 at 20:9 Comment(1)
Thank you for the prompt and detailed response! I just got home from work, so when I get in the office tomorrow I'm going to give this a shot. If it helps me get closer to my solution I'll mark this as the answer. : )Paxwax

© 2022 - 2024 — McMap. All rights reserved.