2010-05-10 12 views
8

(Trước hết, đây là một bài đăng dài, nhưng đừng lo lắng: Tôi đã triển khai tất cả, tôi chỉ hỏi ý kiến ​​của bạn hoặc các giải pháp thay thế có thể.)Thay thế các hướng dẫn trong MethodBody

Tôi đang gặp sự cố khi triển khai những điều sau; Tôi đánh giá cao một số trợ giúp:

  1. Tôi nhận được thông số Type.
  2. Tôi xác định một lớp con bằng cách sử dụng sự phản chiếu. Lưu ý rằng tôi không có ý định sửa đổi kiểu gốc, nhưng tạo một kiểu mới.
  3. tôi tạo ra một tài sản cho mỗi lĩnh vực của lớp ban đầu, như vậy:

    public class OriginalClass { 
        private int x; 
    } 
    
    
    public class Subclass : OriginalClass { 
        private int x; 
    
        public int X { 
         get { return x; } 
         set { x = value; } 
        } 
    
    } 
    
  4. Đối với mỗi phương pháp của lớp cha, tôi tạo ra một phương pháp tương tự trong lớp con. Cơ thể của phương thức phải giống nhau, ngoại trừ việc tôi thay thế các hướng dẫn ldfld x bằng callvirt this.get_X, tức là thay vì đọc trực tiếp từ trường, tôi gọi trình truy cập nhận.

Tôi gặp sự cố với bước 4. Tôi biết bạn không được phép thao tác mã như thế này, nhưng tôi thực sự cần.

Đây là những gì tôi đã cố gắng:

Cố gắng # 1: Sử dụng Mono.Cecil. Điều này sẽ cho phép tôi phân tích cú pháp nội dung của phương thức thành một số người có thể đọc được Instructions và dễ dàng thay thế các hướng dẫn. Tuy nhiên, kiểu gốc không có trong tệp .dll, vì vậy tôi không thể tìm cách tải nó bằng Mono.Cecil. Viết loại cho một dll, sau đó tải nó, sau đó sửa đổi nó và viết loại mới vào đĩa (mà tôi nghĩ là cách bạn tạo ra một loại với Mono.Cecil), và sau đó tải nó có vẻ như một lớn trên cao .

Cố gắng # 2: Sử dụng Mono.Reflection. Điều này cũng sẽ cho phép tôi phân tích cú pháp nội dung thành Instructions, nhưng sau đó tôi không có hỗ trợ để thay thế hướng dẫn. Tôi đã triển khai một giải pháp rất xấu và không hiệu quả khi sử dụng Mono.Reflection, nhưng nó chưa hỗ trợ các phương thức chứa các câu lệnh try-catch (mặc dù tôi đoán tôi có thể thực hiện điều này) và tôi lo ngại rằng có thể có các kịch bản khác trong nó sẽ không hoạt động, vì tôi đang sử dụng ILGenerator theo cách hơi khác thường. Ngoài ra, nó rất xấu xí;). Đây là những gì tôi đã thực hiện:

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); 
     } 

    } 

} 

Và ở đây có khủng khiếp, khủng khiếp Reemit phương pháp:

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. 
} 

hướng dẫn Chi nhánh là một trường hợp đặc biệt vì instr.OperandSByte, nhưng Emit hy vọng một toán hạng kiểu Label . Do đó sự cần thiết cho Dictionary labels.

Như bạn có thể thấy, điều này khá khủng khiếp. Hơn nữa, nó không hoạt động trong mọi trường hợp, ví dụ với các phương thức có chứa các câu lệnh try-catch, vì tôi đã không phát chúng bằng các phương thức BeginExceptionBlock, BeginCatchBlock, v.v. của ILGenerator. Điều này đang trở nên phức tạp. Tôi đoán tôi có thể làm điều đó: MethodBody có danh sách ExceptionHandlingClause cần chứa thông tin cần thiết để thực hiện việc này. Nhưng tôi không thích giải pháp này anyway, vì vậy tôi sẽ tiết kiệm này như là một giải pháp cuối cùng khu nghỉ mát.

Cố gắng # 3: Tới trần trở lại và chỉ cần sao chép các mảng byte được trả về bởi MethodBody.GetILAsByteArray(), kể từ khi tôi chỉ muốn thay thế một chỉ dẫn duy nhất cho một chỉ dẫn duy nhất có cùng kích thước mà tạo ra kết quả chính xác như nhau: nó tải cùng một loại đối tượng trên ngăn xếp, v.v. Vì vậy, sẽ không có bất kỳ nhãn chuyển dịch nào và mọi thứ sẽ hoạt động giống hệt nhau. Tôi đã làm điều này, thay thế các byte cụ thể của mảng và sau đó gọi MethodBuilder.CreateMethodBody(byte[], int), nhưng tôi vẫn nhận được lỗi tương tự với các ngoại lệ và tôi vẫn cần khai báo biến cục bộ hoặc tôi sẽ gặp lỗi ... ngay cả khi tôi chỉ đơn giản là sao chép phần thân của phương thức và không thay đổi gì cả. Vì vậy, điều này hiệu quả hơn nhưng tôi vẫn phải chăm sóc ngoại lệ, v.v.

Sigh.

Đây là việc thực hiện các nỗ lựC# 3, trong trường hợp bất cứ ai quan tâm: (... Tôi biết nó không phải là khá Xin lỗi tôi đặt nó một cách nhanh chóng với nhau để xem nếu nó sẽ làm việc)

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; 
} 

Tôi không có nhiều hy vọng, nhưng bất cứ ai có thể đề xuất bất cứ điều gì tốt hơn điều này?

Xin lỗi về bài đăng cực kỳ dài và cảm ơn.


CẬP NHẬT # 1: Aggh ... Tôi vừa mới đọc in the msdn documentation:

[Phương pháp CreateMethodBody] là hiện không hỗ trợ đầy đủ. Người dùng không thể cung cấp vị trí của sửa lỗi mã thông báo và trình xử lý ngoại lệ.

Tôi thực sự nên đọc tài liệu trước khi thử bất cứ điều gì. Một ngày nào đó tôi sẽ học ...

Điều này có nghĩa là tùy chọn # 3 không thể hỗ trợ các câu lệnh try-catch, điều này khiến tôi vô dụng. Tôi có thực sự phải sử dụng số 2 khủng khiếp không? :/ Cứu giúp! : P


CẬP NHẬT # 2: Tôi đã thực hiện thành công nỗ lựC# 2 với sự hỗ trợ cho trường hợp ngoại lệ. Nó khá xấu xí, nhưng nó hoạt động. Tôi sẽ đăng nó ở đây khi tôi tinh chỉnh mã một chút. Nó không phải là một ưu tiên, vì vậy nó có thể là một vài tuần kể từ bây giờ. Chỉ cần cho bạn biết trong trường hợp ai đó quan tâm đến điều này.

Cảm ơn đề xuất của bạn.

+0

Bạn nói rằng bạn đã giải quyết được vấn đề bằng cách tiếp cận thứ hai. Bạn có thể đăng ở đây giải pháp của bạn (ví dụ: liên kết đến mã nguồn) không. Cảm ơn trước! –

+0

vâng, sẽ đánh giá cao nó, bạn cũng thực sự quản lý để thay thế phương pháp cũ bằng phương pháp mới? Hay bạn vừa tạo ra một phương thức động mới có hành vi khác một chút và gói nó vào lớp proxy? –

Trả lời

0

Bạn đã thử PostSharp chưa? Tôi nghĩ rằng nó đã cung cấp tất cả những gì bạn cần ra khỏi hộp thông qua số On Field Access Aspect.

+0

Có, nhưng nó không hỗ trợ loại thời gian chạy mà tôi cần. Nhờ đề nghị mặc dù. – Alix

+0

Tôi hiểu. Trong trường hợp đó tôi nghĩ rằng tôi sẽ xem xét việc sử dụng # 3, mà dường như có những yêu cầu nhỏ nhất và xác suất lỗi ít nhất đối với tôi. – Lucero

+0

Vâng, tôi cũng thích # 3 là tốt nhất, nhưng tôi đang cố gắng tìm ra cách giải quyết vấn đề với ngoại lệ và tôi không có may mắn. Dường như với tôi như thể tôi phải mở và đóng các câu lệnh try-catch gọi 'BeginExceptionBlock',' BeginCatchBlock', vv trên 'ILGenerator', nhưng trong # 3 tôi không thực sự sử dụng' ILGenerator' để phát ra hướng dẫn ... Tôi không biết làm thế nào tôi có thể làm điều này. – Alix

0

Có lẽ tôi đã hiểu sai điều gì đó, nhưng nếu bạn muốn mở rộng, hãy chặn một phiên bản hiện tại của lớp bạn có thể xem xét Castle Dynamic Proxy.

+0

Tôi đang nhìn vào nó nhưng bây giờ tôi đã không tìm thấy bất cứ điều gì liên quan đến việc phân tích cơ thể của phương pháp và thay thế các hướng dẫn cụ thể. Ngoài ra, tôi không thực sự muốn * đánh chặn * lớp học. Tôi muốn người dùng yêu cầu rõ ràng tiện ích mở rộng lớp nếu anh ấy muốn. Hooking lên các lớp học không phải là một vấn đề, tôi chỉ muốn thay thế những hướng dẫn cụ thể trong các phương pháp mới. – Alix

0

Bạn phải xác định thuộc tính trong lớp cơ sở là ảo hoặc trừu tượng trước. Ngoài ra, các trường sau đó cần phải được sửa đổi để được 'bảo vệ' thay vì 'riêng tư'.

Hoặc tôi có hiểu nhầm điều gì đó ở đây không?

+0

Xin lỗi, nhưng có bạn đang có;). Không có bất kỳ thuộc tính nào trong lớp cơ sở, có các trường riêng. Ngoài ra, không cần các trường được bảo vệ thay vì riêng tư. Và không có điều nào liên quan đến câu hỏi của tôi: cách viết lại một phương pháp, hướng dẫn bằng cách hướng dẫn. Dù sao, tôi đã giải quyết nó (xem bản cập nhật # 2). – Alix

0

Về cơ bản bạn đang sao chép văn bản chương trình của lớp gốc và sau đó thực hiện các thay đổi thường xuyên cho lớp đó. Phương pháp hiện tại của bạn là sao chép mã đối tượng cho lớp và bản vá đó. Tôi có thể hiểu tại sao điều đó có vẻ xấu xí; bạn đang làm việc ở mức cực kỳ thấp.

Điều này có vẻ như sẽ dễ dàng thực hiện với các phép chuyển đổi chương trình nguồn thành nguồn. Điều này hoạt động trên AST cho mã nguồn chứ không phải là mã nguồn chính nó cho các precisions. Xem DMS Software Reengineering Toolkit cho công cụ như vậy. DMS có trình phân tích cú pháp C# 4.0 đầy đủ.

1

Tôi đang cố gắng làm một điều rất giống nhau. Tôi đã thử cách tiếp cận số 1 của bạn, và tôi đồng ý, điều đó tạo ra một chi phí rất lớn (tôi chưa đo chính xác).

Có một lớp DynamicMethod là - theo MSDN - "Xác định và đại diện cho một phương pháp động có thể được biên dịch, thực hiện và loại bỏ. Các phương pháp bị loại bỏ có sẵn để thu thập rác".

Hiệu suất khôn ngoan có vẻ tốt.

Với thư viện ILReader Tôi có thể chuyển đổi bình thường MethodInfo thành DynamicMethod. Khi bạn nhìn vào các phương pháp ConvertFrom của lớp DyanmicMethodHelper của ILReader thư viện bạn có thể tìm mã chúng tôi cần:

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

ilInfo.SetCode(code, body.MaxStackSize); 

Về mặt lý thuyết này hãy cho chúng tôi thay đổi mã của một phương pháp hiện có và chạy nó như một phương pháp năng động .

Vấn đề duy nhất của tôi bây giờ là Mono.Cecil không cho phép chúng tôi lưu bytecode của một phương thức (ít nhất tôi không thể tìm ra cách để làm điều đó). Khi bạn tải về mã nguồn Mono.Cecil, nó có một lớp CodeWriter để hoàn thành nhiệm vụ, nhưng nó không được công khai.

Vấn đề khác mà tôi có với phương pháp này là việc chuyển đổi MethodInfo -> DynamicMethod chỉ hoạt động với các phương pháp tĩnh với ILReader. Nhưng điều này có thể được làm việc xung quanh.

Hiệu suất của lời gọi phụ thuộc vào phương pháp tôi đã sử dụng. Tôi đã nhận kết quả sau sau khi gọi phương pháp ngắn 10'000'000 lần:

  • Reflection.Invoke - 14 giây
  • DynamicMethod.Gọi - 26 giây
  • DynamicMethod với các đại biểu - 9 giây

Sau đó tôi sẽ cố gắng là:

  1. tải phương pháp ban đầu với Cecil
  2. sửa đổi mã trong Cecil
  3. xoá bỏ mã chưa sửa đổi khỏi assembly
  4. lưu hội đồng dưới dạng MemoryStream thay vì Tệp
  5. tải lắp ráp mới (từ bộ nhớ) với Reflection
  6. gọi phương pháp với sự phản ánh gọi nếu nó gọi một lần
  7. tạo ra các đại biểu DynamicMethod và lưu trữ chúng nếu tôi muốn gọi là phương pháp thường xuyên
  8. cố gắng để tìm thấy ra nếu tôi có thể dỡ bỏ các hội đồng không cần thiết từ bộ nhớ (giải phóng cả MemoryStream và thời gian chạy đại diện lắp ráp)

có vẻ như rất nhiều công việc và nó có thể không hoạt động, chúng ta sẽ thấy :)

Tôi hy vọng nó sẽ giúp, hãy để m e biết bạn nghĩ gì.

+0

Thực sự rất nhiều công việc. Nếu bạn thành công trong việc lưu trữ các đại biểu của DynamicMethod để gọi cho họ thường xuyên, hãy cho tôi biết bạn đã làm như thế nào? –

0

Điều gì về việc sử dụng SetMethodBody thay vì CreateMethodBody (đây có thể là biến thể của # 3)? Đó là một phương pháp mới được giới thiệu trong .NET 4.5 và dường như hỗ trợ các ngoại lệ và sửa chữa.