Инструкции по созданию DynamicMethod из Action‹T›

Я играю с DynamicMethod и стремлюсь сделать следующее:

У меня есть действие, из которого я получаю код IL в виде байтов, используя GetILAsByteArray(). Из этих байтов я хотел бы создать динамический метод и выполнить его. Вот пример того, что я пытаюсь сделать:

class Program
{
    static void Main(string[] args)
    {
        //Create action and execute
        Action<string> myAction = s =>
        {
            Console.WriteLine("Hello " + s);
        };
        myAction("World");
        //Get IL bytes
        byte[] ilBytes = myAction.GetMethodInfo().GetMethodBody().GetILAsByteArray();
        DynamicMethod dynamicCallback = new DynamicMethod("myAction", typeof(void), new Type[] { typeof(string) });
        DynamicILInfo dynamicIlInfo = dynamicCallback.GetDynamicILInfo();
        dynamicIlInfo.SetCode(ilBytes, 100);
        dynamicCallback.Invoke(null, new object[] { "World" });
    }
}

При вызове dynamicCallback.Invoke(null, new object[] { "World" }) мы получаем «Исключение:« System.BadImageFormatException »в mscorlib.dll».

Одна вещь, о которой я понятия не имею, это то, что я должен использовать в качестве второго аргумента для SetCode(), что следует использовать как «maxStackSize»? Как я могу установить то же значение, что и для начального действия? Но я полагаю, что это не причина для исключения.

Как я могу правильно создать динамический метод из байтов IL?


Решение

Здесь я хотел бы обобщить полное решение, предоставленное Дуди Келети:

static void Main(string[] args)
{
    Action<string> myAction = s =>
    {
        Console.WriteLine("Hello " + s);
    };
    MethodInfo method = myAction.GetMethodInfo();
    object target = myAction.Target;

    DynamicMethod dm = new DynamicMethod(
        method.Name,
        method.ReturnType,
        new[] {method.DeclaringType}.
            Concat(method.GetParameters().
                Select(pi => pi.ParameterType)).ToArray(),
        method.DeclaringType,
        skipVisibility: true);

    DynamicILInfo ilInfo = dm.GetDynamicILInfo();
    var body = method.GetMethodBody();
    SignatureHelper sig = SignatureHelper.GetLocalVarSigHelper();
    foreach (LocalVariableInfo lvi in body.LocalVariables)
    {
       sig.AddArgument(lvi.LocalType, lvi.IsPinned);
    }
    ilInfo.SetLocalSignature(sig.GetSignature());
    byte[] code = body.GetILAsByteArray();
    ILReader reader = new ILReader(method);
    DynamicMethodHelper.ILInfoGetTokenVisitor visitor = new DynamicMethodHelper.ILInfoGetTokenVisitor(ilInfo, code);
    reader.Accept(visitor);
    ilInfo.SetCode(code, body.MaxStackSize);

    dm.Invoke(target, new object[] { target, "World" });

    Console.ReadLine(); //Just to see the result
}

Примечание. DynamicMethodHelper — это класс, разработанный Хайбо Луо и описанный в сообщение в блоге, но его также можно загрузить напрямую здесь.


person Sjoerd222888    schedule 20.10.2016    source источник
comment
Я думаю, что вы не можете получить значение maxStackSize с помощью отражения. Но на самом деле проблема здесь не в этом. Проблема в том, что вызов Console.WriteLine закодирован как токен метаданных (вероятно, MethodRef), а токены метаданных действительны только в области модуля, объявляющего их. Взгляните на функции DynamicILInfo.GetTokenFor, они будут импортировать другие элементы метаданных и создавать токены, действительные для DynamicMethod.   -  person thehennyy    schedule 20.10.2016
comment
@thehennyy Я безуспешно пытался, смотрите мое редактирование.   -  person Sjoerd222888    schedule 20.10.2016
comment
Вы должны заменить старый токен в массиве байтов IL новым, который возвращает вам метод GetTokenFor.   -  person thehennyy    schedule 20.10.2016
comment
Думал уже не имеет смысла что кодил, не сразу понял. Спасибо. Есть ли общий способ сделать это, не зная, что было вызвано в действии? Например. есть ли способ сделать это динамически в конце?   -  person Sjoerd222888    schedule 20.10.2016
comment
Да, вы можете проанализировать массив байтов тела метода, а затем разрешить все токены с помощью методов Module.Resolvexxx.   -  person thehennyy    schedule 20.10.2016
comment
Почему вы пытаетесь это сделать? Как пояснил thehennyy, простой подход не сработает из-за токенов метаданных. Может быть, есть какой-то другой способ добиться того, что вы пытаетесь сделать.   -  person svick    schedule 20.10.2016
comment
Вопрос опубликован из любопытства. Что можно сделать с этим, так это своего рода сериализацию методов, что звучит как интересная вещь, с которой можно поиграться. Но я знаю, что это может быть неразумно делать в реальных приложениях.   -  person Sjoerd222888    schedule 21.10.2016


Ответы (1)


Вы можете сделать это следующим образом:

byte[] code = body.GetILAsByteArray();
ILReader reader = new ILReader(method);
ILInfoGetTokenVisitor visitor = new ILInfoGetTokenVisitor(ilInfo, code);
reader.Accept(visitor);
ilInfo.SetCode(code, body.MaxStackSize);

ILReader — это класс, который сделает всю тяжелую работу за вас. Вы можете скопировать его из здесь.

Пример:

MethodInfo method = ...
DynamicMethod dm = new DynamicMethod(
     method.Name,
     method.ReturnType,
     method.GetParameters.Select(pi => pi.ParameterType).ToArray(),
     method.DeclaringType,
     skipVisibility: true\fasle - depends of your need);

DynamicILInfo ilInfo = dm.GetDynamicILInfo();
var body = method.GetMethodBody();
SignatureHelper sig = SignatureHelper.GetLocalVarSigHelper();
foreach(LocalVariableInfo lvi in body.LocalVariables)
{
    sig.AddArgument(lvi.LocalType, lvi.IsPinned);
}
ilInfo.SetLocalSignature(sig.GetSignature());
byte[] code = body.GetILAsByteArray();
ILReader reader = new ILReader(method);
ILInfoGetTokenVisitor visitor = new ILInfoGetTokenVisitor(ilInfo, code);
reader.Accept(visitor);
ilInfo.SetCode(code, body.MaxStackSize);

Если ваш метод является простым методом (не универсальным и без дескрипторов исключений), он должен работать.

Если ваш метод является универсальным, вам нужно сделать это для передачи типа владельца конструктору DynamicMethod:

var owner = method.DeclaringType.MakeGenericType(
             method.DeclaringType.GetGenericArguments());

Еще одна вещь, если он все еще не работает, а ваш метод является методом экземпляра, передайте тип instacne метода в первой ячейке массива параметров конструктора DynamicMethod.

Обновить

Вы не можете передать null здесь dm.Invoke(**null**, new object[] { "World" });, потому что myAction не является статическим методом.

myAction (Action<string>) на самом деле является методом в новом сгенерированном классе, который содержит этот метод.

Но я проверил, и исключение выдается, даже если я передаю myAction.Target или новый экземпляр этого типа. Исключение (CLR обнаруживает недопустимую программу) говорит вам, что IL не совсем правильный. Я не могу сейчас точно сказать, в чем проблема, но если это важно для вас, я могу проверить это на следующей неделе, когда вернусь к работе.

В любом случае, если вы просто хотите увидеть DynamicIlInfo.SetCode в действии, вы можете использовать свой код как есть, но просто измените информацию о методе:

class Program
{        
    static void Main(string[] args)
    {
        Action<string> myAction = s =>
        {
            Console.WriteLine("Hello " + s);
        };
        MethodInfo method = myAction.GetMethodInfo();

        //Rest of your code
    }
}

к этому:

class Program
{
    static void M(string s)
    {
        Console.WriteLine("Hello " + s);
    }

    static void Main(string[] args)
    {
        MethodInfo method = typeof (Program).GetMethod("M", BindingFlags.Static | BindingFlags.NonPublic);

        //Rest of your code
    }
}

Обновление 2:

Видимо я вчера сильно устал, не понял вашей ошибки.

Как я писал в своем первоначальном ответе,

Еще одна вещь, если он все еще не работает, а ваш метод является методом экземпляра, передайте тип instacne метода в первой ячейке массива параметров конструктора DynamicMethod.

Итак, вам нужно сделать это:

DynamicMethod dm = new DynamicMethod(
     method.Name,
     method.ReturnType,
     new[] {method.DeclaringType}.
        Concat(method.GetParameters().
        Select(pi => pi.ParameterType)).ToArray(),
     method.DeclaringType,
     skipVisibility: true);

И вызовите динамический метод следующим образом:

dm.Invoke(myAction.Target, new object[] { myAction.Target, "World" });

Теперь работает идеально.

person Dudi Keleti    schedule 26.10.2016
comment
Где объявлено ILInfoGetTokenVisitor? - person Sjoerd222888; 26.10.2016
comment
@Sjoerd222888 Здесь. Об этом есть запись в блоге здесь - person Dudi Keleti; 26.10.2016
comment
Я немедленно получаю «System.Reflection.TargetInvocationException» с сообщением: {Неверная двоичная подпись. (Исключение из HRESULT: 0x80131192)} при попытке вызвать динамический метод. Думаю, я упускаю что-то фундаментальное. - person Sjoerd222888; 26.10.2016
comment
Смотрите мое обновление. Я все еще получаю TargetInvocationException. Где я ошибаюсь? - person Sjoerd222888; 27.10.2016
comment
@ Sjoerd222888 Проверьте это сейчас - person Dudi Keleti; 28.10.2016
comment
Большое спасибо за подробные разъяснения. Я только собираюсь научиться работать с DynamicMethod, и ваш ответ мне очень поможет! - person Sjoerd222888; 28.10.2016