(Прежде всего, это очень длинный пост, но не волнуйтесь: я уже все это реализовал, я просто спрашиваю ваше мнение или возможные альтернативы.)
У меня проблемы с реализацией следующего; Буду признателен за помощь:
- Я получаю
Type
в качестве параметра.
- Я определяю подкласс, используя отражение. Обратите внимание, что я не собираюсь изменять исходный тип, но создаю новый.
Я создаю свойство для поля исходного класса, например:
public class OriginalClass {
private int x;
}
public class Subclass : OriginalClass {
private int x;
public int X {
get { return x; }
set { x = value; }
}
}
Для каждого метода суперкласса я создаю аналогичный метод в подклассе. Тело метода должно быть таким же, за исключением того, что я заменяю инструкции 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 с поддержкой исключений. Это довольно некрасиво, но это работает. Я опубликую это здесь, когда немного уточнить код. Это не приоритет, так что может быть через пару недель. Просто сообщаю, если кому-то это интересно.
Спасибо за ваши предложения.