Как выдать OpCodes.Constrained с OpCodes.Callvirt, если у меня есть необходимая MethodInfo и тип экземпляра

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

В следующем фрагменте я пытаюсь создать коды операций 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 должен быть управляемым указателем (&) на thisType. Обратите внимание, что это отличается от случая без префикса callvirt, которая ожидает ссылку этого типа».

Обновить

Спасибо @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)
        ...

person Stephen Swensen    schedule 30.07.2011    source источник
comment
См. также: stackoverflow.com/questions/18182756/   -  person Ian Kemp    schedule 30.12.2018
comment
Но оказалось, что мне пока не нужно рассматривать этот случай, и моей настоящей проблемой был аргумент экземпляра в стеке: мне потребовалось всего 6 часов, чтобы узнать, что ему нужен адрес вместо значения, я очень смеюсь прямо сейчас, потому что каждый человек, который начинает писать IL, сталкивается именно с этой проблемой и долго ищет, что не так. Многие из них также публикуют свои вопросы в SO... как и я!   -  person aaronburro    schedule 01.02.2021


Ответы (2)


Я думаю, что часть документации, которую вы процитировали в конце вопроса, является источником проблемы. Я не совсем уверен, для чего нужен префикс 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 используется только для виртуальных вызовов типов значений. Моя интерпретация заключается в том, что для типов значений вы знаете, что такое фактический тип, поэтому вам не нужен фактический (неограниченный) виртуальный вызов.

person Tomas Petricek    schedule 30.07.2011

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

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

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

Использование ограниченного префикса также позволяет избежать потенциальных проблем с версиями для типов значений. Если ограниченный префикс не используется, должен быть создан другой IL в зависимости от того, переопределяет ли тип значения метод System.Object. Например, если тип значения V переопределяет метод Object.ToString(), выдается инструкция call 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
person desco    schedule 30.07.2011