5 votes

Comment émettre un OpCodes.Constrained avec un OpCodes.Callvirt en ayant sous la main le MethodInfo et le Type d'instance nécessaires ?

J'ai une fonction récursive emit : Map<string,LocalBuilder> -> exp -> unit donde il : ILGenerator est globale à la fonction et exp est une union discriminante représentant un langage analysé vérifié par type avec casse InstanceCall of exp * MethodInfo * exp list * Type y Type est une propriété sur exp représentant le type de l'expression.

Dans le fragment suivant, j'essaie d'émettre des opcodes IL pour un appel d'instance où instance.Type peut ou non être un ValueType . Je comprends donc que je peux utiliser OpCodes.Constrained pour effectuer de manière souple et efficace des appels virtuels sur les types référence, valeur et enum. Je suis novice dans le domaine de Reflection.Emit et des langages machine en général, c'est pourquoi la compréhension de la documentation liée de OpCodes.Constrained n'est pas forte pour moi.

Voici ma tentative, mais le résultat est une VerificationException , "L'opération pourrait déstabiliser le temps d'exécution." :

let rec emit lenv ast =
    match ast with
    ...
    | InstanceCall(instance,methodInfo,args,_) ->
        instance::args |> List.iter (emit lenv)
        il.Emit(OpCodes.Constrained, instance.Type)
        il.Emit(OpCodes.Callvirt, methodInfo)
    ...

En regardant la documentation, je pense que la clé est peut-être "Un pointeur géré, ptr, est poussé sur la pile. Le type de ptr doit être un pointeur géré (&) vers thisType. Notez que ceci est différent du cas d'une instruction callvirt non fixée, qui attend une référence de thisType."

Mise à jour

Merci @Tomas et @desco, je comprends maintenant quand il faut utiliser OpCodes.Constrained ( instance.Type est un ValueType, mais methodInfo.DeclaringType est un type de référence).

Mais il s'avère que je n'ai pas besoin de considérer ce cas pour l'instant, et mon vrai problème était l'argument d'instance sur la pile : il ne m'a fallu que 6 heures pour apprendre qu'il a besoin d'une adresse au lieu de la valeur (regarder le code source de DLR m'a donné des indices, et ensuite utiliser ilasm.exe sur un simple programme C# a rendu les choses claires).

Voici ma version finale de travail :

let rec emit lenv ast =
    match ast with
    | Int32(x,_) -> 
        il.Emit(OpCodes.Ldc_I4, x)
    ...
    | InstanceCall(instance,methodInfo,args,_) ->
        emit lenv instance
        //if value type, pop, put in field, then load the field address
        if instance.Type.IsValueType then
            let loc = il.DeclareLocal(instance.Type)
            il.Emit(OpCodes.Stloc, loc)
            il.Emit(OpCodes.Ldloca, loc)

        for arg in args do emit lenv arg

        if instance.Type.IsValueType then
            il.Emit(OpCodes.Call, methodInfo)
        else
            il.Emit(OpCodes.Callvirt, methodInfo)
        ...

3voto

desco Points 12018

En fait, je suis d'accord avec Tomas : si vous connaissez le type exact au moment de la compilation, vous pouvez émettre vous-même l'instruction d'appel correcte. Le préfixe contraint est généralement utilisé pour le code générique.

Mais la documentation dit aussi :

L'opcode contraint permet aux compilateurs IL d'effectuer un appel à une fonction virtuelle de manière uniforme, indépendamment du fait que ptr soit un type de valeur ou un type de référence. Bien qu'il soit prévu pour le cas où thisType est une variable de type générique, le préfixe contraint fonctionne également pour les types non génériques et peut réduire la complexité de la génération d'appels virtuels dans les langages qui cachent la distinction entre les types de valeur et les types de référence. ...

L'utilisation du préfixe contraint permet également d'éviter les problèmes potentiels de versioning avec les types de valeurs. Si le préfixe contraint n'est pas utilisé, différents IL doivent être émis selon qu'un type de valeur surpasse ou non une méthode de System.Object. Par exemple, si un type de valeur V surpasse la méthode Object.ToString(), une instruction call V.ToString() est émise ; sinon, une instruction box et une instruction callvirt Object.ToString() sont émises. Un problème de versionnage peut survenir dans le premier cas si la surcharge est supprimée ultérieurement, et dans le second cas si une surcharge est ajoutée ultérieurement.

Petite démonstration (honte à moi, je n'ai pas F# sur mon netbook) :

using System;
using System.Reflection;
using System.Reflection.Emit;

public struct EvilMutableStruct
{
    int i;
    public override string ToString()
    {
            i++;
            return i.ToString();
    }
}

class Program
{
    public static void Main()
    {
            var intToString = Make<int>();
            var stringToString = Make<string>();
            var structToString = Make<EvilMutableStruct>();
            Console.WriteLine(intToString(5));
            Console.WriteLine(stringToString("!!!"));   
            Console.WriteLine(structToString (new EvilMutableStruct())); 
    }

    static MethodInfo ToStringMethod = new Func<string>(new object().ToString).Method;
    static MethodInfo ConcatMethod = new Func<string, string, string>(String.Concat).Method;

    // x => x.ToString() + x.ToString()
    private static Func<T, string> Make<T>()
    {
            var dynamicMethod = new DynamicMethod("ToString", typeof(string), new[] {typeof(T)});
            var il = dynamicMethod.GetILGenerator();

            il.Emit(OpCodes.Ldarga_S, 0);
            il.Emit(OpCodes.Constrained, typeof(T));
            il.Emit(OpCodes.Callvirt, ToStringMethod);

            il.Emit(OpCodes.Ldarga_S, 0);
            il.Emit(OpCodes.Constrained, typeof(T));
            il.Emit(OpCodes.Callvirt, ToStringMethod);

            il.Emit(OpCodes.Call, ConcatMethod);

            il.Emit(OpCodes.Ret);
            return (Func<T, string>)dynamicMethod.CreateDelegate(typeof(Func<T, string>));
     }
}

Sortie :

55
!!!!!!
12

1voto

Tomas Petricek Points 118959

Je pense que la partie de la documentation que vous avez citée à la fin de la question est la source du problème. Je ne suis pas tout à fait sûr de ce que le OpCodes.Constrained (je ne comprends pas mieux que vous la documentation), mais j'ai essayé de voir comment il est utilisé par Microsoft :-).

Voici un extrait de code source de Dynamic Language Runtime qui émet un appel de méthode :

// Emit arguments
List<WriteBack> wb = EmitArguments(mi, args);

// Emit the actual call
OpCode callOp = UseVirtual(mi) ? OpCodes.Callvirt : OpCodes.Call;
if (callOp == OpCodes.Callvirt && objectType.IsValueType) {
    // This automatically boxes value types if necessary.
    _ilg.Emit(OpCodes.Constrained, objectType);
}
// The method call can be a tail call if [...]
if ((flags & CompilationFlags.EmitAsTailCallMask) == CompilationFlags.EmitAsTail && 
    !MethodHasByRefParameter(mi)) {
    _ilg.Emit(OpCodes.Tailcall);
}
if (mi.CallingConvention == CallingConventions.VarArgs) {
    _ilg.EmitCall(callOp, mi, args.Map(a => a.Type));
} else {
    _ilg.Emit(callOp, mi);
}

// Emit writebacks for properties passed as "ref" arguments
EmitWriteBack(wb);

Je pense que vous voudrez probablement suivre leur comportement - il semble que la constrained Le préfixe est uniquement utilisé pour les appels virtuels sur les types de valeurs. Mon interprétation est que pour les types de valeurs, vous savez quel est le type réel, donc vous n'avez pas besoin d'un appel virtuel réel (non contraint).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X