Unboxing nullable lorsque le code d'émission d'une méthode laisse la pile d'évaluation dans un état inattendu (à mon sens)

c# dapper dynamicmethod reflection.emit unboxing

Question

Vue d'ensemble (pardonnez-moi d'être si détaillé, mais je préférerais que ce soit trop peu): j'essaye d'éditer la source de Dapper pour notre solution de telle manière que quand n'importe quel DateTime ou Nullable est lu de la base de données , sa propriété DateTime.Kind est toujours définie sur DateTimeKind.Utc.

Dans notre système, tous les DateTimes provenant de l’avant sont garantis à l’heure UTC et la base de données (Sql Server Azure) les stocke en tant que type DateTime en UTC (nous n’utilisons pas DateTimeOffsets, nous nous assurons simplement que DateTime est UTC avant de le stocker dans la base de données.)

J'ai lu tout sur la façon de générer du code pour DynamicMethods en utilisant ILGenerator.Emit (...), et j'ai l'impression d'avoir une bonne compréhension de son fonctionnement avec la pile d'évaluation, les sections locales, etc. problème, j'ai écrit de petits échantillons de code pour m'aider à atteindre l'objectif final. J'ai écrit un DynamicMethod pour prendre un DateTime comme argument, appelez DateTime.SpecifyKind, retournez la valeur. Puis la même chose avec DateTime? tapez, en utilisant sa propriété Nullable.Value pour obtenir le DateTime pour la méthode SpecifyKind.

C'est là que mon problème se pose: dans dapper, le DateTime (ou DateTime? Je ne sais pas vraiment, mais quand je le traite comme si je n'obtenais pas ce que j'attendais) est encadré. Donc, quand j'essaie d'utiliser OpCodes.Unbox ou OpCodes.Unbox_Any, puis traite le résultat comme DateTime ou DateTime?, J'obtiens une exception VerificationException: l'opération peut déstabiliser le runtime.

Évidemment, il me manque quelque chose d'important à propos de la boxe, mais je vais vous donner mes exemples de code et peut-être que vous pourrez m'aider à le faire fonctionner.

Cela marche:

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

Je reçois ce que j'attends ici. Yay! Mais ce n'est pas fini, car nous avons unboxing pour gérer ...

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

Ce simple jeu ne fonctionnera pas. Je reçois une exception de vérification, puis je pleure dans le coin jusqu'à ce que je sois prêt à réessayer.

J'ai essayé d'attendre un DateTime au lieu d'un DateTime? (après unbox, supposons DateTime sur la pile eval, plutôt que DateTime?) mais cela échoue également.

Quelqu'un peut-il s'il vous plaît me dire ce qui me manque?

Réponse acceptée

En cas de doute, écrivez une bibliothèque C # minimale qui fait la même chose et voyez ce qui est compilé pour:

Votre tentative semble être équivalente à

using System;

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

et cela compile pour:

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

La première différence avec votre version est que ldarg est utilisé à la place de ldarga . Vous voulez que unbox.any vérifie la valeur transmise, pas un pointeur sur la valeur passée. ( ldarga fonctionne aussi dans mes tests, mais ldarg plus de sens quand même.)

La seconde différence, plus pertinente, avec votre version est qu'après unbox.any , la valeur est stockée, puis une référence à cet emplacement est chargée. Cela est dû au fait que this paramètre implicite des méthodes d’instance des types de valeur a le type ref T , plutôt que le T vous êtes habitué, par exemple les méthodes de types de référence. Si stloc.0 ce stloc.0 / ldloca.s 0 , votre code passe alors son test, sur mon système.

Toutefois, comme vous lisez inconditionnellement la propriété Value après la conversion vers DateTime? , vous pouvez aussi aller directement à DateTime et éviter complètement le problème. La seule différence serait quelle exception vous obtenez quand une valeur du type incorrect est transmise.

Si vous voulez plutôt quelque chose comme

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

alors je voudrais utiliser quelque chose comme

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



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi