Замена инструкций в MethodBody метода - PullRequest
8 голосов
/ 10 мая 2010

(Прежде всего, это очень длинный пост, но не волнуйтесь: я уже все это реализовал, я просто спрашиваю ваше мнение или возможные альтернативы.)

У меня проблемы с реализацией следующего; Буду признателен за помощь:

  1. Я получаю Type в качестве параметра.
  2. Я определяю подкласс, используя отражение. Обратите внимание, что я не собираюсь изменять исходный тип, но создаю новый.
  3. Я создаю свойство для поля исходного класса, например:

    public class OriginalClass {
        private int x;
    }
    
    
    public class Subclass : OriginalClass {
        private int x;
    
        public int X {
            get { return x; }
            set { x = value; }
        }
    
    }
    
  4. Для каждого метода суперкласса я создаю аналогичный метод в подклассе. Тело метода должно быть таким же, за исключением того, что я заменяю инструкции ldfld x на callvirt this.get_X, то есть вместо непосредственного чтения из поля я вызываю метод доступа get.

У меня проблемы с шагом 4. Я знаю, что вы не должны манипулировать таким кодом, но мне действительно нужно.

Вот что я пробовал:

Попытка # 1: Использовать Mono.Cecil. Это позволило бы мне разобрать тело метода в удобочитаемое Instructions и легко заменить инструкции. Однако оригинальный тип отсутствует в файле .dll, поэтому я не могу найти способ загрузить его с помощью Mono.Cecil. Записать тип в .dll, затем загрузить его, затем изменить его и записать новый тип на диск (который, как мне кажется, является способом создания типа с помощью Mono.Cecil), а затем загрузить его, как огромную накладные расходы.

Попытка № 2: Использовать Mono.Reflection. Это также позволило бы мне разобрать тело в Instructions, но тогда у меня нет поддержки для замены инструкций. Я реализовал очень уродливое и неэффективное решение с использованием Mono.Reflection, но оно пока не поддерживает методы, содержащие операторы try-catch (хотя, думаю, я смогу это реализовать), и я обеспокоен тем, что могут быть другие сценарии в который не будет работать, так как я использую ILGenerator несколько необычным способом. Кроме того, это очень некрасиво;). Вот что я сделал:

private void TransformMethod(MethodInfo methodInfo) {

    // Create a method with the same signature.
    ParameterInfo[] paramList = methodInfo.GetParameters();
    Type[] args = new Type[paramList.Length];
    for (int i = 0; i < args.Length; i++) {
        args[i] = paramList[i].ParameterType;
    }
    MethodBuilder methodBuilder = typeBuilder.DefineMethod(
        methodInfo.Name, methodInfo.Attributes, methodInfo.ReturnType, args);
    ILGenerator ilGen = methodBuilder.GetILGenerator();

    // Declare the same local variables as in the original method.
    IList<LocalVariableInfo> locals = methodInfo.GetMethodBody().LocalVariables;
    foreach (LocalVariableInfo local in locals) {
        ilGen.DeclareLocal(local.LocalType);
    }

    // Get readable instructions.
    IList<Instruction> instructions = methodInfo.GetInstructions();

    // I first need to define labels for every instruction in case I
    // later find a jump to that instruction. Once the instruction has
    // been emitted I cannot label it, so I'll need to do it in advance.
    // Since I'm doing a first pass on the method's body anyway, I could
    // instead just create labels where they are truly needed, but for
    // now I'm using this quick fix.
    Dictionary<int, Label> labels = new Dictionary<int, Label>();
    foreach (Instruction instr in instructions) {
        labels[instr.Offset] = ilGen.DefineLabel();
    }

    foreach (Instruction instr in instructions) {

        // Mark this instruction with a label, in case there's a branch
        // instruction that jumps here.
        ilGen.MarkLabel(labels[instr.Offset]);

        // If this is the instruction that I want to replace (ldfld x)...
        if (instr.OpCode == OpCodes.Ldfld) {
            // ...get the get accessor for the accessed field (get_X())
            // (I have the accessors in a dictionary; this isn't relevant),
            MethodInfo safeReadAccessor = dataMembersSafeAccessors[((FieldInfo) instr.Operand).Name][0];
            // ...instead of emitting the original instruction (ldfld x),
            // emit a call to the get accessor,
            ilGen.Emit(OpCodes.Callvirt, safeReadAccessor);

        // Else (it's any other instruction), reemit the instruction, unaltered.
        } else {
            Reemit(instr, ilGen, labels);
        }

    }

}

И вот идет ужасный, ужасный Reemit метод:

private void Reemit(Instruction instr, ILGenerator ilGen, Dictionary<int, Label> labels) {

    // If the instruction doesn't have an operand, emit the opcode and return.
    if (instr.Operand == null) {
        ilGen.Emit(instr.OpCode);
        return;
    }

    // Else (it has an operand)...

    // If it's a branch instruction, retrieve the corresponding label (to
    // which we want to jump), emit the instruction and return.
    if (instr.OpCode.FlowControl == FlowControl.Branch) {
        ilGen.Emit(instr.OpCode, labels[Int32.Parse(instr.Operand.ToString())]);
        return;
    }

    // Otherwise, simply emit the instruction. I need to use the right
    // Emit call, so I need to cast the operand to its type.
    Type operandType = instr.Operand.GetType();
    if (typeof(byte).IsAssignableFrom(operandType))
        ilGen.Emit(instr.OpCode, (byte) instr.Operand);
    else if (typeof(double).IsAssignableFrom(operandType))
        ilGen.Emit(instr.OpCode, (double) instr.Operand);
    else if (typeof(float).IsAssignableFrom(operandType))
        ilGen.Emit(instr.OpCode, (float) instr.Operand);
    else if (typeof(int).IsAssignableFrom(operandType))
        ilGen.Emit(instr.OpCode, (int) instr.Operand);
    ... // you get the idea. This is a pretty long method, all like this.
}

Инструкции ветвления являются особым случаем, потому что instr.Operand равен SByte, но Emit ожидает операнд типа Label. Отсюда и необходимость Dictionary labels.

Как видите, это довольно ужасно. Более того, он работает не во всех случаях, например, с методами, содержащими операторы try-catch, поскольку я не генерировал их с использованием методов BeginExceptionBlock, BeginCatchBlock и т. Д. ILGenerator. Это становится сложным. Я думаю, я могу сделать это: MethodBody имеет список ExceptionHandlingClause, который должен содержать необходимую информацию для этого. Но в любом случае мне не нравится это решение, поэтому я сохраню его как последнее средство.

Попытка # 3: Вернитесь назад и просто скопируйте массив байтов, возвращенный MethodBody.GetILAsByteArray(), так как я хочу заменить только одну инструкцию для другой отдельной инструкции того же размера, которая производит точный тот же результат: он загружает один и тот же тип объекта в стек и т. д. Таким образом, метки не будут смещаться, и все должно работать точно так же. Я сделал это, заменив определенные байты массива и затем вызвав MethodBuilder.CreateMethodBody(byte[], int), но я все еще получаю ту же ошибку с исключениями, и мне все еще нужно объявить локальные переменные, или я получу ошибку ... даже когда Я просто копирую тело метода и ничего не меняю. Так что это более эффективно, но я все еще должен позаботиться об исключениях и т.д.

Вздох.

Вот реализация попытки # 3, на случай, если кому-то интересно:

private void TransformMethod(MethodInfo methodInfo, Dictionary<string, MethodInfo[]> dataMembersSafeAccessors, ModuleBuilder moduleBuilder) {

    ParameterInfo[] paramList = methodInfo.GetParameters();
    Type[] args = new Type[paramList.Length];
    for (int i = 0; i < args.Length; i++) {
        args[i] = paramList[i].ParameterType;
    }
    MethodBuilder methodBuilder = typeBuilder.DefineMethod(
        methodInfo.Name, methodInfo.Attributes, methodInfo.ReturnType, args);

    ILGenerator ilGen = methodBuilder.GetILGenerator();

    IList<LocalVariableInfo> locals = methodInfo.GetMethodBody().LocalVariables;
    foreach (LocalVariableInfo local in locals) {
        ilGen.DeclareLocal(local.LocalType);
    }

    byte[] rawInstructions = methodInfo.GetMethodBody().GetILAsByteArray();
    IList<Instruction> instructions = methodInfo.GetInstructions();

    int k = 0;
    foreach (Instruction instr in instructions) {

        if (instr.OpCode == OpCodes.Ldfld) {

            MethodInfo safeReadAccessor = dataMembersSafeAccessors[((FieldInfo) instr.Operand).Name][0];

            // Copy the opcode: Callvirt.
            byte[] bytes = toByteArray(OpCodes.Callvirt.Value);
            for (int m = 0; m < OpCodes.Callvirt.Size; m++) {
                rawInstructions[k++] = bytes[put.Length - 1 - m];
            }

            // Copy the operand: the accessor's metadata token.
            bytes = toByteArray(moduleBuilder.GetMethodToken(safeReadAccessor).Token);
            for (int m = instr.Size - OpCodes.Ldfld.Size - 1; m >= 0; m--) {
                rawInstructions[k++] = bytes[m];
            }

        // Skip this instruction (do not replace it).
        } else {
            k += instr.Size;
        }

    }

    methodBuilder.CreateMethodBody(rawInstructions, rawInstructions.Length);

}


private static byte[] toByteArray(int intValue) {
    byte[] intBytes = BitConverter.GetBytes(intValue);
    if (BitConverter.IsLittleEndian)
        Array.Reverse(intBytes);
    return intBytes;
}



private static byte[] toByteArray(short shortValue) {
    byte[] intBytes = BitConverter.GetBytes(shortValue);
    if (BitConverter.IsLittleEndian)
        Array.Reverse(intBytes);
    return intBytes;
}

(Я знаю, что это не красиво. Извините. Я быстро собрал все вместе, чтобы посмотреть, сработает ли это.)

У меня нет особой надежды, но кто-нибудь может предложить что-нибудь лучше, чем это?

Извините за очень длинный пост, и спасибо.


ОБНОВЛЕНИЕ № 1: Ага ... Я только что прочитал это в документации MSDN :

[метод CreateMethodBody] в настоящее время не полностью поддерживается.пользователь не может указать местоположение исправления токенов и обработчики исключений.

Я действительно должен прочитать документацию, прежде чем что-то пытаться. Когда-нибудь я научусь ...

Это означает, что опция # 3 не может поддерживать операторы try-catch, что делает его бесполезным для меня. Я действительно должен использовать ужасную # 2? :/ Помогите! : P


ОБНОВЛЕНИЕ № 2: Я успешно реализовал попытку № 2 с поддержкой исключений. Это довольно некрасиво, но это работает. Я опубликую это здесь, когда немного уточнить код. Это не приоритет, так что может быть через пару недель. Просто сообщаю, если кому-то это интересно.

Спасибо за ваши предложения.

Ответы [ 6 ]

1 голос
/ 14 сентября 2012

Я пытаюсь сделать очень похожую вещь. Я уже попробовал ваш подход № 1, и я согласен, что это создает огромные накладные расходы (хотя я точно не измерил его).

Существует класс DynamicMethod , который - согласно MSDN - "Определяет и представляет динамический метод, который можно компилировать, выполнять и отбрасывать. Для сбора мусора доступны отброшенные методы".

Производительность звучит хорошо.

С помощью библиотеки ILReader я мог бы преобразовать обычный MethodInfo в DynamicMethod . Когда вы заглянете в метод ConvertFrom класса DyanmicMethodHelper библиотеки ILReader , вы найдете нужный нам код:

byte[] code = body.GetILAsByteArray();
ILReader reader = new ILReader(method);
ILInfoGetTokenVisitor visitor = new ILInfoGetTokenVisitor(ilInfo, code);
reader.Accept(visitor);

ilInfo.SetCode(code, body.MaxStackSize);

Теоретически это позволит нам изменить код существующего метода и запустить его как динамический метод.

Моя единственная проблема сейчас состоит в том, что Mono.Cecil не позволяет нам сохранять байт-код метода (по крайней мере, я не мог найти способ сделать это). Когда вы загружаете исходный код Mono.Cecil, у него есть класс CodeWriter для выполнения этой задачи, но он не является общедоступным.

Другая проблема, с которой я столкнулся при таком подходе, состоит в том, что преобразование MethodInfo -> DynamicMethod работает только со статическими методами с ILReader . Но это можно обойти.

Производительность вызова зависит от метода, который я использовал. Я получил следующие результаты после вызова короткого метода 10'000'000 раз:

  • Reflection.Invoke - 14 секунд
  • DynamicMethod.Invoke - 26 секунд
  • DynamicMethod с делегатами - 9 секунд

Следующая вещь, которую я собираюсь попробовать:

  1. загрузить оригинальный метод с помощью Cecil
  2. изменить код в Сесиле
  3. убрать неизмененный код из сборки
  4. сохранить сборку как MemoryStream вместо File
  5. загрузить новую сборку (из памяти) с помощью Reflection
  6. вызывать метод с отражением invoke, если это одноразовый вызов
  7. генерировать делегаты DynamicMethod и сохранять их, если я хочу регулярно вызывать этот метод
  8. попытайтесь выяснить, могу ли я выгрузить ненужные сборки из памяти (освободите как MemoryStream, так и представление сборок во время выполнения)

Звучит как большая работа, и она может не сработать, посмотрим:)

Надеюсь, это поможет, дайте мне знать, что вы думаете.

0 голосов
/ 10 сентября 2014

А как насчет использования SetMethodBody вместо CreateMethodBody (это будет вариант № 3)? Это новый метод, представленный в .NET 4.5, и, кажется, он поддерживает исключения и исправления.

0 голосов
/ 16 мая 2010

В основном вы копируете текст программы исходного класса, а затем регулярно вносите в него изменения. Ваш текущий метод состоит в том, чтобы скопировать код object для класса и залатать его. Я могу понять, почему это кажется уродливым; вы работаете на крайне низком уровне.

Кажется, что это было бы легко сделать с преобразованиями программы от источника к источнику. Это работает с AST для исходного кода, а не с самим исходным кодом для точности. См. DMS Software Reengineering Toolkit для такого инструмента. DMS имеет полный синтаксический анализатор C # 4.0.

0 голосов
/ 13 мая 2010

Сначала вам нужно определить свойства в базовом классе как виртуальные или абстрактные. Кроме того, поля необходимо изменить, чтобы они были «защищены», а не «приватными».

Или я что-то здесь неправильно понимаю?

0 голосов
/ 10 мая 2010

Может быть, я что-то понял неправильно, но если вы хотите расширить, перехватить существующий экземпляр класса, вы можете взглянуть на Castle Dynamic Proxy .

0 голосов
/ 10 мая 2010

Вы пробовали PostSharp? Я думаю, что он уже предоставляет все необходимое из коробки через On Field Access Aspect .

...