Unboxing Nullable cuando Emitir código para un método deja la pila de evaluación en un estado inesperado (para mí)

c# dapper dynamicmethod reflection.emit unboxing

Pregunta

Descripción general (perdóneme por ser tan detallado, pero preferiría que sea demasiado). Estoy intentando editar la fuente de Dapper para nuestra solución de tal forma que cuando se lea cualquier DateTime o Nullable de la base de datos , su propiedad DateTime.Kind siempre se establece en DateTimeKind.Utc.

En nuestro sistema, todos los DateTimes provenientes de la interfaz están garantizados en horario UTC, y la base de datos (Sql Server Azure) los almacena como tipo DateTime en UTC (no estamos usando DateTimeOffsets, siempre nos aseguramos de que DateTime es UTC antes de almacenarlo en el DB.)

He estado leyendo todo sobre cómo generar código para DynamicMethods mediante el uso de ILGenerator.Emit (...), y siento que tengo una comprensión decente de cómo funciona con la pila de evaluación, los locales, etc. En mis esfuerzos por resolver esto, problema, he escrito pequeñas muestras de código para ayudarme a alcanzar el objetivo final. Escribí un DynamicMethod para tomar un DateTime como argumento, llamar a DateTime.SpecifyKind, devolver el valor. ¿Entonces lo mismo con DateTime? escriba, utilizando su propiedad Nullable.Value para obtener el DateTime para el método SpecifyKind.

Aquí es donde entra mi problema: en apuesto, el DateTime (o DateTime? En realidad no lo sé, pero cuando lo trato como si fuera o no estoy obteniendo lo que esperaba) está en recuadro. Entonces, cuando trato de usar OpCodes.Unbox u OpCodes.Unbox_Any, entonces trato el resultado como DateTime o DateTime ?, obtengo una VerificationException: la operación podría desestabilizar el tiempo de ejecución.

Obviamente me falta algo importante sobre el boxeo, pero te daré mis ejemplos de código y tal vez puedas ayudarme a hacerlo funcionar.

Esto funciona:

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

Obtengo lo que espero aquí. ¡Hurra! Pero aún no ha terminado, porque tenemos unboxing para tratar con ...

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

Esto simplemente no se ejecutará. Recibo VerificationException, y luego lloro en la esquina por un tiempo hasta que estoy listo para volver a intentarlo.

He intentado esperar un DateTime en lugar de un DateTime? (después de la casilla de verificación, asume DateTime en la pila de evaluación, en lugar de DateTime?) pero eso también falla.

¿Alguien puede decirme qué me estoy perdiendo?

Respuesta aceptada

En caso de duda, escriba una biblioteca mínima de C # que haga lo mismo y vea a qué compila:

Tu intento parece ser equivalente a

using System;

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

y esto compila a:

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

Ti primera diferencia con tu versión es que ldarg se utiliza en lugar de ldarga . Desea que unbox.any verifique el valor pasado, no un puntero al valor pasado. ( ldarga también funciona en mis pruebas, pero ldarg tiene más sentido de todos modos).

La segunda y más relevante diferencia con su versión es que después de unbox.any , el valor se almacena y luego se carga una referencia a esa ubicación. Esto es debido a que la implícita this parámetro de los métodos de instancia de los tipos de valor tiene el tipo ref T , en lugar de la T que está acostumbrado a los métodos de instancia de los tipos de referencia. Si stloc.0 ese stloc.0 / ldloca.s 0 , su código pasa su prueba en mi sistema.

Sin embargo, ¿como leer incondicionalmente la propiedad Value después de DateTime? a DateTime? , también podría lanzar directamente a DateTime y evitar el problema por completo. La única diferencia sería qué excepción recibe cuando se pasa un valor del tipo incorrecto.

Si en cambio quieres algo así como

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

entonces usaría algo como

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


Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow