2010-05-10 26 views
8

(首先,这是一篇很长的文章,但不用担心:我已经实现了所有这一切,我只是在问你的意见或可能的替代方案。)替换方法的BodyBody中的指令

我在执行以下操作时遇到问题;我会感谢一些帮助:

  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. 对于超类的每一个方法,创建在子类中的类似方法。除了我用callvirt this.get_X替换指令ldfld 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.OperandSByte,但Emit预计Label类型的操作数。因此需要Dictionary labels

正如你所看到的,这是非常可怕的。更重要的是,它在所有情况下都不起作用,例如包含try-catch语句的方法,因为我没有使用方法BeginExceptionBlock,BeginCatchBlockILGenerator发出它们。这变得越来越复杂。我想我可以做到这一点: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: Aggh ......我刚读这in the msdn documentation

[该CreateMethodBody方法]是目前 不完全支持。 用户无法提供 令牌修复程序和异常处理程序的位置。

我应该在尝试任何事情之前阅读文档。有一天我会学习......

这意味着选项#3不能支持try-catch语句,这对我来说是无用的。我真的必须使用可怕的#2吗? :/ 帮帮我! :P


更新#2:我已经成功地实现尝试#2与异常的支持。这非常丑陋,但它很有用。当我细化代码时,我会在这里发布它。这不是一个优先事项,所以从现在起可能需要几个星期。只要有人对此感兴趣就让你知道。

感谢您的建议。

+0

你说你已经用第二种方法解决了这个问题。你可以在这里发布你的解决方案(例如链接到源代码)。提前致谢! – 2011-07-06 08:43:53

+0

是啊,将不胜感激,你是否真的设法取代旧的方法与新的?或者你是否刚刚创建了一个具有不同行为的新动态方法,并将其包装到代理类中? – 2013-04-18 07:07:29

回答

0

您是否试过PostSharp?我认为它已经通过On Field Access Aspect提供了您需要的所有内容。

+0

是的,但它不支持我需要的那种运行时编织。 (还是)感谢你的建议。 – Alix 2010-05-10 15:06:31

+0

我明白了。在这种情况下,我认为我会考虑使用#3,它似乎对我有最小的要求和最小的错误概率。 – Lucero 2010-05-10 15:16:08

+0

是的,我也喜欢#3最好的,但我想弄清楚如何解决异常的问题,我没有任何运气。在我看来,好像我必须打开和关闭在ILGenerator中调用'BeginExceptionBlock','BeginCatchBlock'等的try-catch语句,但在#3中,我并没有真正使用'ILGenerator'来发出指令......我不知道我该怎么做。 – Alix 2010-05-10 15:29:36

0

也许我理解错了什么,但如果你想扩展,截取一个类的现有实例,你可以看看Castle Dynamic Proxy

+0

我正在看它,但现在我还没有找到任何有关分析方法的身体和取代具体说明。另外,我并不想拦截班级。我想要用户明确地请求类扩展,如果他想。连接类不是问题,我只想替换新方法中的特定指令。 – Alix 2010-05-10 15:32:39

0

您必须首先将基类中的属性定义为虚拟或抽象。 此外,这些字段需要修改为'保护'而不是'私人'。

还是我在这里误解了一些东西?

+0

对不起,但是你是;)。基类中没有任何属性,有私有字段。而且,这些字段不需要保护而不是私人的。而这些都不涉及我的问题:如何重写一种方法,按指令进行指导。无论如何,我已经解决了它(见更新#2)。 – Alix 2010-05-14 08:02:40

0

基本上,您正在复制原始类的程序文本,然后对其进行定期更改。您目前的方法是复制对象代码以获得该类和修补程序。我能理解为什么这看起来很丑陋;你的工作水平非常低。

这似乎很容易做到源到源程序转换。 这对AST源代码而不是源代码本身的精度进行操作。见DMS Software Reengineering Toolkit这样的工具。 DMS拥有完整的C#4.0解析器。

1

我想做一个非常类似的事情。我已经尝试了你的#1方法,并且我同意,这造成了巨大的开销(尽管如此,我还没有测量它)。

有一个DynamicMethod类 - 根据MSDN - “定义并表示一个可以编译,执行和丢弃的动态方法,放弃的方法可用于垃圾收集。”

表现明智,听起来不错。

使用ILReader库,我可以将正常的MethodInfo转换为DynamicMethod。当你看看DyanmicMethodHelper类ILReader库,你可以找到的代码的ConvertFrom方法我们需要:

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的。调用 - 26秒
  • DynamicMethod的与代表 - 9秒

我要去尝试下一件事情就是:

  1. 负载原来的方法与塞西尔
  2. 修改代码塞西尔
  3. 剥离装配中的未修改代码
  4. 将装配保存为MemoryStream而不是文件
  5. 负荷新组件(从内存中)与反射
  6. 调用与反射的方法调用如果一次性调用
  7. 生成DynamicMethod的的代表和存储他们,如果我想调用该方法定期
  8. 试图找到我是否可以从内存中卸载不必要的组件(腾出两个的MemoryStream和运行时组件表示)

这听起来像一个大量的工作,它可能无法正常工作,我们将看到:)

我希望它有帮助,让米知道你在想什么。

+0

真的很多工作。如果您成功存储DynamicMethod的代表以便定期打电话给他们,请告诉我您是如何做到的? – 2014-11-17 05:10:49

0

如何使用SetMethodBody而不是CreateMethodBody(这将是#3的变体)?这是一种在.NET 4.5中引入的新方法,似乎支持异常和修正。