从IL创建一个方法的副本

8
我将使用反射来在运行时创建一个方法的副本。
以下是我的代码。
public static R CopyMethod<T, R>(Func<T, R> f, T t)
{
    AppDomain currentDom = Thread.GetDomain();
    AssemblyName asm = new AssemblyName();
    asm.Name = "DynamicAssembly";
    AssemblyBuilder abl = currentDom.DefineDynamicAssembly(asm, AssemblyBuilderAccess.Run);
    ModuleBuilder mbl = abl.DefineDynamicModule("Module");
    TypeBuilder tbl = mbl.DefineType("Type");
    var info = f.GetMethodInfo();
    MethodBuilder mtbl = tbl.DefineMethod(info.Name, info.Attributes, info.CallingConvention, info.ReturnType, info.GetParameters().Select(x => x.ParameterType).ToArray());

    byte[] il = f.Method.GetMethodBody().GetILAsByteArray();

    mtbl.CreateMethodBody(il, il.Length);
    Type type = tbl.CreateType();
    Func<T, R> method = type.GetMethod(info.Name).CreateDelegate(typeof(Func<T, R>)) as Func<T, R>;
    return method(t);
}

最后一行抛出了一个带有以下消息的异常:

Common Language Runtime 检测到一个无效程序。

还有其他方法吗?我更希望能够获取方法的解析树,而不是直接使用IL。 编辑1: 我正在测试以下函数。
public static int Fib(int n)
{
    /*if (n < 2)
        return 1;
    return Fib(n - 1) + Fib(n - 2);*/
    return n;
}

以下是测试行。

int x = Copy.CopyMethod(Copy.Fib, 10);

编辑2::

Rob的答案有助于解决上述问题。然而,当使用稍微复杂一些的Fib()方法(例如被注释掉的Fibonacci方法)时,程序会崩溃并显示以下消息。

找不到索引。(HRESULT异常: 0x80131124)

编辑3::

我尝试了评论中提出的几个建议,但在动态程序集中无法找到元数据标记。

public static R CopyMethod<T, R>(Func<T, R> f, T t)
{
    AppDomain currentDom = Thread.GetDomain();
    AssemblyName asm = new AssemblyName("DynamicAssembly");
    AssemblyBuilder abl = currentDom.DefineDynamicAssembly(asm, AssemblyBuilderAccess.Run);
    ModuleBuilder mbl = abl.DefineDynamicModule("Module");
    TypeBuilder tbl = mbl.DefineType("Type");
    MethodInfo info = f.GetMethodInfo();
    MethodBuilder mtbl = tbl.DefineMethod(info.Name, info.Attributes, info.CallingConvention, info.ReturnType, info.GetParameters().Select(x => x.ParameterType).ToArray());
    MethodBody mb = f.Method.GetMethodBody();
    byte[] il = mb.GetILAsByteArray();

    OpCode[] opCodes = GetOpCodes(il);
    Globals.LoadOpCodes();
    MethodBodyReader mbr = new MethodBodyReader(info);
    string code = mbr.GetBodyCode();
    Console.WriteLine(code);

    ILGenerator ilg = mtbl.GetILGenerator();
    ilg.DeclareLocal(typeof(int[]));
    ilg.DeclareLocal(typeof(int));
    for (int i = 0; i < opCodes.Length; ++i)
    {
        if (opCodes[i].OperandType == OperandType.InlineType)
        {
            int token;
            Type tp = info.Module.ResolveType(token = BitConverter.ToInt32(il, i + 1), info.DeclaringType.GetGenericArguments(), info.GetGenericArguments());
            ilg.Emit(opCodes[i], tp.MetadataToken);
            i += 4;
            continue;
        }
        if (opCodes[i].FlowControl == FlowControl.Call)
        {
            int token;
            MethodBase mi = info.Module.ResolveMethod(token = BitConverter.ToInt32(il, i + 1));
            ilg.Emit(opCodes[i], mi.MetadataToken);
            i += 4;
            continue;
        }
        ilg.Emit(opCodes[i]);
    }

    Type type = tbl.CreateType();
    Func<T, R> method = type.GetMethod(info.Name).CreateDelegate(typeof(Func<T, R>)) as Func<T, R>;
    return method(t);
}

以下方法也无法解决问题。
var sigHelp = SignatureHelper.GetLocalVarSigHelper(mtbl.Module);
mtbl.SetMethodBody(il, mb.MaxStackSize, sigHelp.GetSignature(), null, new int[] { 3 });

我可以通过以下方式更改元数据标记来修复递归函数调用(我意识到这种方法并不适用于所有情况,但我正在尝试以某种方式使其正常工作)。

if (opCodes[i].FlowControl == FlowControl.Call)
{
    ilg.Emit(opCodes[i], mtbl);
    i += 4;
}

我可以使用在相关问题的回答中建议的方法构建一个动态方法:Reference a collection from IL constructed method。然而,在尝试在这里进行相同操作时,它失败了。


2
问题在于,在调试条件下编译器会引入一个本地变量。这个本地变量必须使用ILGenerator.DeclareLocal方法声明,否则它将不存在,并导致InvalidProgramException异常。 - thehennyy
2
作为一般性的提示,我建议您指定 AssemblyBuilderAccess.RunAndSave,这样您就可以将创建的程序集保存到磁盘上。接下来,您可以使用 Peverify.exe 来验证一切是否正常。您的 Fib 函数无法运行,因为它在调用指令中包含元数据标记。 - thehennyy
1
正如@thehennyy所说,你的代码无法工作,因为它包含元数据标记。你需要查询程序集以找出每个元数据标记所指的内容,并以某种方式替换这些引用。例如,在IL字节中,递归调用的一个示例如下:IL_0010: /* 28 | (06)000002 */ call int32 ConsoleApplication1.Program::Fib(int32)。你需要找到06000002,询问程序集那是什么,然后在新方法的上下文中用自己的元数据标记值替换它。 - elchido
2
@IgorŠevo ILGenerator.Emit()永远不会将元数据标记作为参数,因为这些标记必须由动态程序集本身生成,元数据标记仅在其模块的范围内有效。相反,您将始终传递xxxInfoxxxBuilder实例。为了使事情更清晰一些,在大多数情况下,xxxBuilder类是其对应的xxxInfo类的子类,因此可以使用它。当您发出的代码变得更加复杂并涉及其他动态生成的成员时,请记住这一点。 - thehennyy
1
如果方法标记是要替换的递归方法,则使用您的MethodBuilder对象mtbl发出调用。否则,获取该方法的MethodInfo并使用它发出调用。 - elchido
显示剩余14条评论
2个回答

5

我成功地实现了基于评论区非常有用的讨论的重建。这并不涵盖所有可能的场景,但很好地说明了解决方案。

public static R CopyMethod<T, R>(Func<T, R> f, T t)
{
    AppDomain currentDom = Thread.GetDomain();
    AssemblyName asm = new AssemblyName("DynamicAssembly");
    AssemblyBuilder abl = currentDom.DefineDynamicAssembly(asm, AssemblyBuilderAccess.Run);
    ModuleBuilder mbl = abl.DefineDynamicModule("Module");
    TypeBuilder tbl = mbl.DefineType("Type");
    MethodInfo info = f.GetMethodInfo();
    MethodBuilder mtbl = tbl.DefineMethod(info.Name, info.Attributes, info.CallingConvention, info.ReturnType, info.GetParameters().Select(x => x.ParameterType).ToArray());
    MethodBody mb = f.Method.GetMethodBody();
    byte[] il = mb.GetILAsByteArray();
    ILGenerator ilg = mtbl.GetILGenerator();
    foreach (var local in mb.LocalVariables)
        ilg.DeclareLocal(local.LocalType);
    for (int i = 0; i < opCodes.Length; ++i)
    {
        if (!opCodes[i].code.HasValue)
            continue;
        OpCode opCode = opCodes[i].code.Value;
        if (opCode.OperandType == OperandType.InlineBrTarget)
        {
            ilg.Emit(opCode, BitConverter.ToInt32(il, i + 1));
            i += 4;
            continue;
        }
        if (opCode.OperandType == OperandType.ShortInlineBrTarget)
        {
            ilg.Emit(opCode, il[i + 1]);
            ++i;
            continue;
        }
        if (opCode.OperandType == OperandType.InlineType)
        {
            Type tp = info.Module.ResolveType(BitConverter.ToInt32(il, i + 1), info.DeclaringType.GetGenericArguments(), info.GetGenericArguments());
            ilg.Emit(opCode, tp);
            i += 4;
            continue;
        }
        if (opCode.FlowControl == FlowControl.Call)
        {
            MethodInfo mi = info.Module.ResolveMethod(BitConverter.ToInt32(il, i + 1)) as MethodInfo;
            if (mi == info)
                ilg.Emit(opCode, mtbl);
            else
                ilg.Emit(opCode, mi);
            i += 4;
            continue;
        }
        ilg.Emit(opCode);
    }

    Type type = tbl.CreateType();
    Func<T, R> method = type.GetMethod(info.Name).CreateDelegate(typeof(Func<T, R>)) as Func<T, R>;
    return method(t);
}

static OpCodeContainer[] GetOpCodes(byte[] data)
{
    List<OpCodeContainer> opCodes = new List<OpCodeContainer>();
    foreach (byte opCodeByte in data)
        opCodes.Add(new OpCodeContainer(opCodeByte));
    return opCodes.ToArray();
}

class OpCodeContainer
{
    public OpCode? code;
    byte data;

    public OpCodeContainer(byte opCode)
    {
        data = opCode;
        try
        {
            code = (OpCode)typeof(OpCodes).GetFields().First(t => ((OpCode)(t.GetValue(null))).Value == opCode).GetValue(null);
        }
        catch { }
    }
}

2
Igor提供的有用解决方案存在问题,它在传递给函数的信息上使用了ResolveMethod。这意味着它将会把克隆的实例强制转换为原始类型(虽然这不应该被允许,但我们在IL中!),然后调用原始方法。例如,如果我在原始类TestClass中有两个方法,称为SimpleMethod和MethodCallingSimpleMethod,则复制的类型将会执行以下操作:
internal class Type
{
  public int SimpleMethod([In] int obj0, [In] string obj1)
  {
    return obj0 + obj1.Length;
  }

  public int MethodCallingSimpleMethod([In] string obj0)
  {
    if (string.IsNullOrEmpty(obj0))
      return 0;
    return ((TestClass) this).SimpleMethod(42, obj0);
  }
}

要完整实现这个,我们需要找到方法之间的依赖关系。按正确顺序复制它们,然后使用元标记来解析到原始MethodInfo,再在新类型中查找已复制的方法信息。
不是一件简单的事情。
对于字段,同样需要类似的处理,但比较简单,因为我们可以先处理字段,再处理引用它们的方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接