当发出方法的代码时,取消装箱Nullable会使评估堆栈处于意外(对我)状态

时间:2014-02-11 19:29:31

标签: c# dapper reflection.emit unboxing dynamicmethod

概述(请原谅我这么详细,但我宁愿这太过分了):我正在尝试以这样的方式编辑Dapper源代码,以便在读取任何DateTime或Nullable时从数据库中,其DateTime.Kind属性始终设置为DateTimeKind.Utc。

在我们的系统中,来自前端的所有DateTime都保证是UTC时间,并且数据库(Sql Server Azure)将它们存储为UTC中的DateTime类型(我们不使用DateTimeOffsets,我们只是一直在制作确保DateTime为UTC,然后再将其存储在数据库中。)

我一直在阅读有关如何使用ILGenerator.Emit(...)为DynamicMethods生成代码的所有内容,并且感觉我对它如何与评估堆栈,本地人等一起工作有一个很好的理解。在我的努力中为了解决这个问题,我编写了一些代码示例来帮助我实现最终目标。我写了一个DynamicMethod来取一个DateTime作为参数,调用DateTime.SpecifyKind,返回值。然后与DateTime相同? type,使用其Nullable.Value属性获取SpecifyKind方法的DateTime。

这就是我的问题所在:在dapper中,DateTime(或DateTime?我实际上并不知道,但是当我把它视为我得不到我期望的那样时)是盒装的。因此,当我尝试使用OpCodes.Unbox或OpCodes.Unbox_Any时,将结果视为DateTime或DateTime?,我得到VerificationException:操作可能会破坏运行时的稳定性。

显然我错过了一些关于拳击的重要内容,但我会给你我的代码示例,也许你可以帮助我让它工作。

这有效:

    [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));
    }

我得到了我的期望。好极了!但它还没有结束,因为我们已经拆箱了......

    [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));
    }

这只是直接上升不会运行。我得到了VerificationException,然后我在角落里哭了一会儿,直到我准备再试一次。

我试过期待DateTime而不是DateTime? (在unbox之后,假设在eval堆栈上使用DateTime而不是DateTime?)但是也失败了。

有人可以告诉我我错过了什么吗?

1 个答案:

答案 0 :(得分:7)

如果有疑问,请编写一个执行相同操作的最小C#库,并查看编译的内容:

您的尝试似乎等同于

using System;

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

并编译为:

$ 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 
...

与您的版本的第一个区别是ldarg代替ldarga。您希望unbox.any检查传递的值,而不是指向传递的值的指针。 (ldarga在我的测试中也有效,但无论如何ldarg更有意义。)

与您的版本的第二个,也是更相关的差异是,在unbox.any之后,存储该值,然后加载对该位置的引用。这是因为值类型的实例方法的隐式this参数具有类型ref T,而不是您习惯用于引用类型的方法的T。如果我确实包含stloc.0 / ldloca.s 0,那么您的代码会在我的系统上通过测试。

但是,当您在转换为Value后无条件阅读DateTime?属性时,您也可以直接转到DateTime并完全避免此问题。唯一的区别是当传入错误类型的值时会得到哪个异常。

如果您想要类似

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

然后我会使用像

这样的东西
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);