【发布时间】:2011-02-17 17:11:21
【问题描述】:
(首先,这是一篇很长的帖子,但别担心:我已经全部实现了,我只是在询问您的意见,或者可能的替代方案。)
我在执行以下操作时遇到了问题;我会很感激一些帮助:
- 我得到一个
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 语句的方法,因为我没有使用 ILGenerator 的方法 BeginExceptionBlock、BeginCatchBlock 等发出它们。这变得越来越复杂。我想我可以做到: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: 啊……我刚刚读到这个in the msdn documentation:
[CreateMethodBody 方法] 是 目前不完全支持。这 用户无法提供位置 令牌修复和异常处理程序。
在尝试任何事情之前,我真的应该阅读文档。总有一天我会学习...
这意味着选项 #3 不支持 try-catch 语句,这对我来说毫无用处。我真的必须使用可怕的#2吗? :/ 帮助! :P
更新 #2: 我已经成功实现了尝试 #2,支持异常。这很丑陋,但它有效。当我稍微改进代码时,我会在这里发布。这不是优先事项,因此可能需要几周后。如果有人对此感兴趣,请通知您。
感谢您的建议。
【问题讨论】:
-
您说您已经通过第二种方法解决了问题。你能在这里发布你的解决方案吗(例如链接到源代码)。提前致谢!
-
是的,将不胜感激,您是否真的设法用新方法替换旧方法?还是您只是创建了具有一些不同行为的新动态方法并将其包装到代理类中?
标签: c# reflection reflection.emit cil mono.cecil