Create a copy of method from IL
Asked Answered
C

2

8

I am trying to create a copy of a method during runtime using reflection.

I have the following code.

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

The last line throws an exception with message:

Common Language Runtime detected an invalid program.

Is there another way of doing this? I would prefer being able to get the parse tree of the method instead of using IL directly.

EDIT 1:

I am testing with the following function.

public static int Fib(int n)
{
    /*if (n < 2)
        return 1;
    return Fib(n - 1) + Fib(n - 2);*/
    return n;
}

Testing with the following line.

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

EDIT 2:

Rob's answer helps address the above issue. However, when using the Fib() method that is slightly more complicated (e.g. the commented Fibonacci method), the program crashes with the following message.

Index not found. (Exception from HRESULT: 0x80131124)

EDIT 3:

I have tried several suggestions from comments, but the metadata token cannot be located within the dynamic assembly.

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

The following also does not work.

var sigHelp = SignatureHelper.GetLocalVarSigHelper(mtbl.Module);
mtbl.SetMethodBody(il, mb.MaxStackSize, sigHelp.GetSignature(), null, new int[] { 3 });

I can fix the recursive function calls by changing the metadata token the following way (I realize that this will not work in all cases, but I am trying to get it to work in some way).

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

I can build a dynamic method using the approach suggested in the answer to the related question: Reference a collection from IL constructed method. However, when trying to do the same here, it fails.

Cantillate answered 21/1, 2016 at 22:34 Comment(19)
You can not copy a methodbody like this when a instruction in it uses a metadata token in some way.Ensphere
@Ensphere Can you suggest a fix?Stinson
@Ensphere When examining the IL taken from GetILAsByteArray, there does not appear to be any reference to the token.Landholder
Can you expand your code with the things you try exactly, so i can have a look at it?Ensphere
The problem is, under debug conditions the compiler introduces a local variable. This local has to be declared with the ILGenerator.DeclareLocal Method or it will not be present and will result in an InvalidProgramException.Ensphere
@Ensphere I have tried your suggestion, but it too results in an error. I assume the type of the variable I specify is wrong.Stinson
@Landholder Why did you delete the answer? It was very useful.Stinson
@IgorŠevo As @BrianReichie pointed out, my conclusion was wrong (in that the branch operation takes an offset rather than an explicit address to jump to). With that knowledge in the open, the debug IL is perfectly valid so the answer that the IL was malformed is wrong, and I don't want to mislead future readers. If you'd like, I can put the code up in a pastebin snippet in the comments, but I don't believe it's suitable as an answerLandholder
@Landholder Yes, but the code resolved several issues. Maybe you could link it.Stinson
@IgorŠevo Here you go :) pastebin.com/9hJRp7q0Landholder
As a general notice, i suggest you to specify AssemblyBuilderAccess.RunAndSave so you can save your created assembly to the disk. Next you can use Peverify.exe to verify that everything is ok. Your Fib function will not run because it contains a metadata token in the call instruction.Ensphere
As @Ensphere says, your code won't work because it contains metadata tokens. You would need to query the assembly to find out what each metadata token refers to, and somehow replace those references. As an example, this is what one of the recursive calls looks like in the IL bytes: IL_0010: /* 28 | (06)000002 */ call int32 ConsoleApplication1.Program::Fib(int32). You need to find the 06000002, ask the assembly what that is, and replace it with your own metadata token value in the context of your new method.Misanthropy
@Misanthropy I have been able to do that with the recursive calls to the method. However, I am having problems with calls to the API (e.g. calls to lists, arrays, dictionaries etc.). I need to substitute those too, but am not sure how to achieve that.Stinson
@IgorŠevo I've never done it, but I'd think it's the same: take the metadata token, find out what it refers to in the original assembly, get its MemberInfo in your program, and the metadata token out of the MemberInfo.Misanthropy
@Misanthropy I think I tried that among many other things. I'll give it a try one more time.Stinson
@Misanthropy I've tried what you suggested, but it fails. Maybe I am doing it incorrectly. I've updated the question with what I tried.Stinson
@Ensphere I tried implementing the answer you had to my other question here, but it seems to not work in this context.Stinson
@IgorŠevo ILGenerator.Emit() never takes a metadata token as argument because these tokens have to be generated by the dynamic assembly itself, a metadata token is only valid in the scope of its module. Instead you will always pass the xxxInfo or xxxBuilder instance. To make things a bit more clear, in the most cases, the xxxBuilder class is a subclass of its corresponding xxxInfo class and so can be used. Keep that in mind when your emitted code gets more complex and involves other dynamic generated members.Ensphere
@IgorŠevo If the method token is for the recursive method you want to replace, emit a call using your MethodBuilder object mtbl. Otherwise get the MethodInfo for the method and emit a call with it.Misanthropy
C
5

I managed to implement the reconstruction based on the very helpful discussion in the comments. It does not address all possible scenarios, but illustrates the solution very well.

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 { }
    }
}
Cantillate answered 25/1, 2016 at 22:8 Comment(0)
W
2

The problem with the helpful solution from Igor is that it uses ResolveMethod on the info passed to the function. This means that it will be casting the cloned instance to the original type (which shouldn't be allowed but we're in IL!) and then calling the original method. e.g. if I have two methods in my original class, TestClass, called SimpleMethod and MethodCallingSimpleMethod then the copied type will do something like this:

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

To implement this fully we'd need to find dependencies between methods. Copy them in the correct order and then use the meta-token to resolve to the original MethodInfo and then look up the already copied method info in the new type.

Non-trivial.

Same sort of thing would be required for fields but simpler as we can do the fields first and then the methods that reference them.

Wu answered 4/12, 2018 at 18:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.