Создать копию метода из IL

Я пытаюсь создать копию метода во время выполнения, используя отражение.

У меня есть следующий код.

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

Последняя строка выдает исключение с сообщением:

Cреда CLR обнаружила недопустимую программу.

Есть ли другой способ сделать это? Я бы предпочел иметь возможность получить дерево синтаксического анализа метода вместо прямого использования 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:

Ответ Роба помогает решить вышеуказанную проблему. Однако при использовании чуть более сложного метода Fib() (например, закомментированного метода Фибоначчи) программа вылетает со следующим сообщением.

Индекс не найден. (Исключение из 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;
}

Я могу создать динамический метод, используя подход, предложенный в ответе на соответствующий вопрос: Ссылка на коллекцию из метода, созданного на языке IL. Однако при попытке сделать то же самое здесь это не удается.


person Igor Ševo    schedule 21.01.2016    source источник
comment
Вы не можете скопировать тело метода, подобное этому, когда инструкция в нем каким-то образом использует токен метаданных.   -  person thehennyy    schedule 22.01.2016
comment
@thehennyy Можете ли вы предложить исправление?   -  person Igor Ševo    schedule 22.01.2016
comment
@thehennyy При изучении IL, взятого из GetILAsByteArray, не было никаких ссылок на токен.   -  person Rob    schedule 22.01.2016
comment
Можете ли вы расширить свой код теми вещами, которые вы точно пробуете, чтобы я мог взглянуть на него?   -  person thehennyy    schedule 22.01.2016
comment
Проблема в том, что в условиях отладки компилятор вводит локальную переменную. Этот локальный объект должен быть объявлен с помощью метода ILGenerator.DeclareLocal, иначе он не будет присутствовать и приведет к исключению InvalidProgramException.   -  person thehennyy    schedule 22.01.2016
comment
@thehennyy Я попробовал ваше предложение, но оно тоже приводит к ошибке. Я предполагаю, что тип переменной, которую я указываю, неверен.   -  person Igor Ševo    schedule 22.01.2016
comment
@Rob Почему ты удалил ответ? Это было очень полезно.   -  person Igor Ševo    schedule 22.01.2016
comment
@IgorŠevo Как отметил @BrianReichie, мой вывод был неверным (в том смысле, что операция ветвления использует смещение, а не явный адрес для перехода). С этими открытыми знаниями отладка IL совершенно верна, поэтому ответ о том, что IL был искажен, неверен, и я не хочу вводить в заблуждение будущих читателей. Если хотите, я могу поместить код в отрывок pastebin в комментариях, но я не думаю, что он подходит в качестве ответа.   -  person Rob    schedule 22.01.2016
comment
@Rob Да, но код решил несколько проблем. Может быть, вы могли бы связать это.   -  person Igor Ševo    schedule 22.01.2016
comment
@IgorŠevo Вот, пожалуйста :) pastebin.com/9hJRp7q0   -  person Rob    schedule 22.01.2016
comment
В качестве общего замечания, я предлагаю вам указать AssemblyBuilderAccess.RunAndSave, чтобы вы могли сохранить созданную сборку на диск. Затем вы можете использовать Peverify.exe чтобы убедиться, что все в порядке. Ваша функция Fib не будет работать, так как она содержит токен метаданных в инструкции вызова.   -  person thehennyy    schedule 22.01.2016
comment
Как говорит @thehennyy, ваш код не будет работать, потому что он содержит токены метаданных. Вам нужно будет запросить сборку, чтобы узнать, на что ссылается каждый токен метаданных, и каким-то образом заменить эти ссылки. Например, вот как выглядит один из рекурсивных вызовов в байтах IL: IL_0010: /* 28 | (06)000002 */ call int32 ConsoleApplication1.Program::Fib(int32). Вам нужно найти 06000002, спросить сборку, что это такое, и заменить его своим собственным значением маркера метаданных в контексте вашего нового метода.   -  person elchido    schedule 22.01.2016
comment
@elchido Я смог сделать это с помощью рекурсивных вызовов метода. Однако у меня возникают проблемы с вызовами API (например, вызовы списков, массивов, словарей и т. д.). Мне тоже нужно заменить их, но я не уверен, как этого добиться.   -  person Igor Ševo    schedule 22.01.2016
comment
@IgorŠevo Я никогда этого не делал, но думаю, что это то же самое: взять токен метаданных, узнать, на что он ссылается в исходной сборке, получить его MemberInfo в своей программе, а токен метаданных из MemberInfo.   -  person elchido    schedule 22.01.2016
comment
@elchido Я думаю, что пробовал это среди многих других вещей. Я попробую еще раз.   -  person Igor Ševo    schedule 22.01.2016
comment
@elchido Я пробовал то, что вы предложили, но это не помогло. Возможно, я делаю это неправильно. Я обновил вопрос тем, что пробовал.   -  person Igor Ševo    schedule 25.01.2016
comment
@thehennyy Я попытался реализовать ответ на мой другой вопрос здесь, но, похоже, он не работает в этом контексте.   -  person Igor Ševo    schedule 25.01.2016
comment
@IgorŠevo ILGenerator.Emit() никогда не принимает токен метаданных в качестве аргумента, потому что эти токены должны генерироваться самой динамической сборкой, токен метаданных действителен только в рамках своего модуля. Вместо этого вы всегда будете передавать экземпляр xxxInfo или xxxBuilder. Чтобы было немного понятнее, в большинстве случаев класс xxxBuilder является подклассом соответствующего ему класса xxxInfo, поэтому его можно использовать. Имейте это в виду, когда ваш испускаемый код становится более сложным и включает в себя другие динамически сгенерированные члены.   -  person thehennyy    schedule 25.01.2016
comment
@IgorŠevo Если токен метода предназначен для рекурсивного метода, который вы хотите заменить, выполните вызов, используя ваш MethodBuilder объект mtbl. В противном случае получите MethodInfo для метода и вызовите его.   -  person elchido    schedule 25.01.2016


Ответы (2)


Мне удалось реализовать реконструкцию на основе очень полезного обсуждения в комментариях. Он не рассматривает все возможные сценарии, но очень хорошо иллюстрирует решение.

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 { }
    }
}
person Igor Ševo    schedule 25.01.2016

Проблема с полезным решением от Игоря заключается в том, что оно использует 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, а затем найдите уже скопированную информацию о методе в новом типе.

Нетривиальный.

То же самое потребуется для полей, но проще, поскольку мы можем сначала сделать поля, а затем методы, которые на них ссылаются.

person David Regan    schedule 04.12.2018