Как генерировать OpCodes.Constrained с OpCodes.Callvirt, учитывая, что у меня есть необходимый MethodInfo и экземпляр Type под рукой - PullRequest
5 голосов
/ 30 июля 2011

У меня есть рекурсивная функция emit : Map<string,LocalBuilder> -> exp -> unit, где il : ILGenerator является глобальной функцией, а exp - это дискриминантный союз, представляющий язык синтаксического анализа с проверкой типа, с регистром InstanceCall of exp * MethodInfo * exp list * Type и Type - свойством * 1006.* представляющий тип выражения.

В следующем фрагменте я пытаюсь генерировать коды операций IL для вызова экземпляра, где instance.Type может быть или не быть ValueType.Поэтому я понимаю, что могу использовать OpCodes.Constrained, чтобы гибко и эффективно выполнять виртуальные вызовы для типов ссылки, значения и перечисления.Я новичок в Reflection.Emit и машинных языках в целом, поэтому понимание связанной документации для OpCodes.Constrained не слишком хорошо для меня.

Вот моя попытка, но она приводит к VerificationException, «Операцияможет дестабилизировать среду выполнения. ":

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

Глядя на документы, я думаю, что ключом может быть" Управляемый указатель ptr помещается в стек. Тип ptr должен быть управляемым указателем (&) to thisType. Обратите внимание, что это отличается от случая с нефиксированной инструкцией callvirt, которая ожидает ссылку на thisType. "

Обновление

Спасибо @Tomasи @desco, теперь я понимаю, когда использовать OpCodes.Constrained (instance.Type - это ValueType, но methodInfo.DeclaringType - это ссылочный тип).

Но оказывается, мне пока не нужно рассматривать этот случай, и моей настоящей проблемой был аргумент экземпляра в стеке: мне потребовалось всего 6 часов, чтобы понять, что ему нужен адрес вместо значения(просмотр исходного кода DLR дал мне подсказки, а затем использование ilasm.exe в простой программе на C # дало понять).

Вот моя окончательная рабочая версия:

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

Ответы [ 2 ]

3 голосов
/ 30 июля 2011

В основном я согласен с Томасом: если вы знаете точный тип во время компиляции, то вы можете самостоятельно вывести правильную инструкцию вызова.Ограниченный префикс обычно используется для универсального кода

Но в документации также говорится:

Ограниченный код операции позволяет компиляторам IL выполнять вызов виртуальной функции единообразным способом, независимо от того, является ли ptrявляется типом значения или ссылочным типом.Хотя он предназначен для случая, когда thisType является переменной универсального типа, ограниченный префикс также работает для неуниверсальных типов и может снизить сложность генерации виртуальных вызовов в языках, которые скрывают различие между типами значений и ссылочными типами....

Использование ограниченного префикса также позволяет избежать потенциальных проблем управления версиями с типами значений.Если ограниченный префикс не используется, должен выдаваться другой IL в зависимости от того, переопределяет ли тип значения метод System.Object.Например, если тип значения V переопределяет метод Object.ToString (), выдается инструкция вызова V.ToString ();если это не так, генерируются инструкция box и инструкция callvirt Object.ToString ().Проблема управления версиями может возникнуть в первом случае, если переопределение будет позже удалено, и во втором случае, если переопределение будет добавлено позднее.

Небольшая демонстрация (позор, у меня нет F # на моем нетбуке):

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

Вывод:

55
!!!!!!
12
1 голос
/ 30 июля 2011

Мне кажется, что источником проблемы является та часть документации, которую вы цитировали в конце вопроса.Я не совсем уверен, для чего предназначен префикс OpCodes.Constrained (я не понимаю документацию лучше, чем вы), но я попытался посмотреть, как он используется Microsoft: -).

Вотфрагмент из исходного кода Dynamic Language Runtime , который испускает вызов метода:

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

Я думаю, вы, вероятно, захотите следовать их поведению - похоже, префикс constrainedиспользуется только для виртуальных вызовов по типам значений.Моя интерпретация заключается в том, что для типов значений вы знаете, что является фактическим типом, поэтому вам не нужен фактический (неограниченный) виртуальный вызов.

...