替换方法的MethodBody中的指令

Ali*_*lix 8 c# reflection cil reflection.emit mono.cecil

(首先,这是一个非常冗长的帖子,但不要担心:我已经实现了所有这些,我只是问你的意见,或者可能的替代方案.)

我在实施以下方面遇到了麻烦; 我很感激一些帮助:

  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; }
        }
    
    }
    
    Run Code Online (Sandbox Code Playgroud)
  4. 对于超类的每个方法,我在子类中创建一个类似的方法.该方法的身体必须是除了我更换指令相同的ldfld xcallvirt 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);
        }

    }

}
Run Code Online (Sandbox Code Playgroud)

这是一个可怕的,可怕的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.
}
Run Code Online (Sandbox Code Playgroud)

分支指令是一种特殊情况,因为它instr.OperandSByte,但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;
}
Run Code Online (Sandbox Code Playgroud)

(我知道它不漂亮.抱歉.我把它快速放在一起看看它是否会起作用.)

我没有太大的希望,但有人能提出比这更好的建议吗?

对于非常冗长的帖子感到抱歉,谢谢.


更新#1: Aggh ......我刚刚在msdn文档中读到了这个:

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

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

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


更新#2:我已成功实施了尝试#2并支持异常.这很难看,但它确实有效.当我稍微改进代码时,我会在这里发布它.这不是优先事项,因此可能需要几周时间.如果有人对此感兴趣,请告诉您.

谢谢你的建议.

Luc*_*ero 0

您尝试过 PostSharp 吗?我认为它已经通过On Field Access Aspect提供了您开箱即用所需的一切。