2010-05-10 52 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中引入的新方法,似乎支持異常和修正。