(首先,這是一篇很長的文章,但不用擔心:我已經實現了所有這一切,我只是在問你的意見或可能的替代方案。)替換方法的BodyBody中的指令
我在執行以下操作時遇到問題;我會感謝一些幫助:
- 我得到一個
Type
作爲參數。 - 我使用反射定義了一個子類。請注意,我不打算修改原始類型,但創建一個新類型。
創建每原始類,的字段的屬性,像這樣:
public class OriginalClass { private int x; } public class Subclass : OriginalClass { private int x; public int X { get { return x; } set { x = value; } } }
對於超類的每一個方法,創建在子類中的類似方法。除了我用
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.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: Aggh ......我剛讀這in the msdn documentation:
[該CreateMethodBody方法]是目前 不完全支持。 用戶無法提供 令牌修復程序和異常處理程序的位置。
我應該在嘗試任何事情之前閱讀文檔。有一天我會學習......
這意味着選項#3不能支持try-catch語句,這對我來說是無用的。我真的必須使用可怕的#2嗎? :/ 幫幫我! :P
更新#2:我已經成功地實現嘗試#2與異常的支持。這非常醜陋,但它很有用。當我細化代碼時,我會在這裏發佈它。這不是一個優先事項,所以從現在起可能需要幾個星期。只要有人對此感興趣就讓你知道。
感謝您的建議。
你說你已經用第二種方法解決了這個問題。你可以在這裏發佈你的解決方案(例如鏈接到源代碼)。提前致謝! – 2011-07-06 08:43:53
是啊,將不勝感激,你是否真的設法取代舊的方法與新的?或者你是否剛剛創建了一個具有不同行爲的新動態方法,並將其包裝到代理類中? – 2013-04-18 07:07:29