Создание метода динамически и его выполнение - PullRequest
27 голосов
/ 06 октября 2011

Справочная информация:

Я хочу определить несколько static методов в C # и сгенерировать код IL в виде байтового массива из одного из этих методов, выбранных во время выполнения (на клиенте)и отправить байтовый массив по сети на другой компьютер (сервер), где он должен быть выполнен после повторной генерации кода IL из байтового массива.

Моя попытка: ( POC )

public static class Experiment
{
    public static int Multiply(int a, int b)
    {
        Console.WriteLine("Arguments ({0}, {1})", a, b);
        return a * b;
    }
}

А затем я получаю код IL тела метода, как:

BindingFlags flags = BindingFlags.Public | BindingFlags.Static;
MethodInfo meth = typeof(Experiment).GetMethod("Multiply", flags);
byte[] il = meth.GetMethodBody().GetILAsByteArray();

До сих пор я ничего не создавал динамически.Но у меня есть IL-код в виде байтового массива, и я хочу создать сборку, затем модуль в ней, затем тип, затем метод - все динамически.При создании тела метода динамически создаваемого метода я использую код IL, полученный с помощью отражения в приведенном выше коде.

Код генерации кода выглядит следующим образом:

AppDomain domain = AppDomain.CurrentDomain;
AssemblyName aname = new AssemblyName("MyDLL");
AssemblyBuilder assemBuilder = domain.DefineDynamicAssembly(
                                               aname, 
                                               AssemblyBuilderAccess.Run);

ModuleBuilder modBuilder = assemBuilder.DefineDynamicModule("MainModule");

TypeBuilder tb = modBuilder.DefineType("MyType", 
                            TypeAttributes.Public | TypeAttributes.Class);

MethodBuilder mb = tb.DefineMethod("MyMethod", 
     MethodAttributes.Static | MethodAttributes.Public, 
     CallingConventions.Standard,
     typeof(int),                          // Return type
     new[] { typeof(int), typeof(int) });  // Parameter types

mb.DefineParameter(1, ParameterAttributes.None, "value1");  // Assign name 
mb.DefineParameter(2, ParameterAttributes.None, "value2");  // Assign name 

//using the IL code to generate the method body
mb.CreateMethodBody(il, il.Count()); 

Type realType = tb.CreateType();

var meth = realType.GetMethod("MyMethod");
try
{
    object result = meth.Invoke(null, new object[] { 10, 9878 });
    Console.WriteLine(result);  //should print 98780 (i.e 10 * 9878)
}
catch (Exception e)
{
    Console.WriteLine(e.ToString());
}

Но вместо того, чтобы печатать 98780 в окне вывода, выдается исключение, говорящее:

System.Reflection.TargetInvocationException: Исключение было сгенерировано целью вызова.---> System.TypeLoadException: не удалось загрузить тип 'Invalid_Token.0x0100001E' из сборки 'MyDLL, версия = 0.0.0.0, Culture = нейтральный, PublicKeyToken = null'.
в MyType.MyMethod (значение Int32, значение Int32, значение 2) [...]

Пожалуйста, помогите мне выяснить причину ошибки и как ее исправить.

Ответы [ 6 ]

18 голосов
/ 19 ноября 2011

Запустите ildasm.exe на произвольной сборке.Используйте View + Show значения токена и посмотрите на некоторый разобранный код.Вы увидите, что IL содержит ссылки на другие методы и переменные через число.Число является индексом в таблицах метаданных для сборки.

Возможно, теперь вы видите проблему: вы не можете переместить кусок IL из одной сборки в другую, если эта целевая сборка не имеет одинаковых метаданных .Или до тех пор, пока вы не замените значения токена метаданных в IL значениями, соответствующими метаданным целевой сборки.Это крайне непрактично, конечно, вы по сути дела дублируете сборку.Можно также сделать копию сборки.Или, если уж на то пошло, можно также использовать существующий IL в исходной сборке.

Вам нужно немного обдумать это, довольно неясно, чего вы на самом деле пытаетесь достичь.Пространства имен System.CodeDom и Reflection.Emit доступны для динамической генерации кода.

11 голосов
/ 19 ноября 2011

Если я возьму следующий метод:

public static int Multiply(int a, int b)
{
    return a * b;
}

скомпилируйте его в режиме Release и используйте в своем коде, все работает нормально. И если я проверю массив il, он содержит 4 байта, которые в точности соответствуют четырем инструкциям из ответа Джона (ldarg.0, ldarg.1, mul, ret).

Если я скомпилирую его в режиме отладки, код будет 9 байтов. А в Reflector это выглядит так:

.method public hidebysig static int32 Multiply(int32 a, int32 b) cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 CS$1$0000)
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: ldarg.1 
    L_0003: mul 
    L_0004: stloc.0 
    L_0005: br.s L_0007
    L_0007: ldloc.0 
    L_0008: ret 
}

Проблемной частью является локальная переменная. Если вы просто копируете байты инструкции, вы отправляете инструкцию, которая использует локальную переменную, но никогда не объявляете ее.

Если бы вы использовали ILGenerator, вы могли бы использовать DeclareLocal(). Кажется, вы сможете установить локальные переменные, используя MethodBuilder.SetMethodBody() в .Net 4.5 (в VS 11 DP у меня работает следующее):

var module = typeof(Experiment).Module;
mb.SetMethodBody(il, methodBody.MaxStackSize,
                 module.ResolveSignature(methodBody.LocalSignatureMetadataToken),
                 null, null);

Но я не нашел способа сделать это в .Net 4, кроме как с помощью отражения задать частное поле MethodBuilder, которое содержит локальные переменные, после вызова CreateMethodBody():

typeof(MethodBuilder)
    .GetField("m_localSignature", BindingFlags.NonPublic | BindingFlags.Instance)
    .SetValue(mb, module.ResolveSignature(methodBody.LocalSignatureMetadataToken));

Относительно исходной ошибки: Типы и методы из других сборок (например, System.Console и System.Console.WriteLine) ссылаются с помощью токенов. И эти токены отличаются от сборки к сборке. Это означает, что код для вызова Console.WriteLine() в одной сборке будет отличаться от кода для вызова того же метода в другой сборке, если вы посмотрите на байты инструкции.

Это означает, что вам действительно нужно понять, что означают байты IL, и заменить все токены, которые ссылаются на типы, методы и т. Д., На токены, действительные в сборке, которую вы строите.

8 голосов
/ 06 октября 2011

Я думаю, что проблема заключается в использовании IL из одного типа / сборки в другом.Если вы замените это:

mb.CreateMethodBody(il, il.Count());

на это:

ILGenerator generator = mb.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Mul);
generator.Emit(OpCodes.Ret);

, тогда он выполнит метод правильно (без Console.WriteLine, но вернет правильное значение).

Если вам действительно нужно иметь возможность отбирать IL-код из существующего метода, вам нужно посмотреть дальше - но если вам просто нужна проверка того, что остальная часть кода работает, это может помочь.

Одна вещь, которая может показаться вам интересной, заключается в том, что ошибка изменяется в исходном коде, если вы берете вызов Console.WriteLine из Experiment.Вместо этого он становится InvalidProgramException.Я понятия не имею, почему ...

4 голосов
/ 25 ноября 2011

Если я хорошо понимаю вашу проблему, и вы просто хотите динамически сгенерировать некоторый код .NET и выполнить его на удаленном клиенте, maeby взгляните на IronPython. Вам просто нужно создать строку со скриптом, а затем просто отправить ее клиенту, и клиент может выполнить ее во время выполнения с доступом ко всем .NET Framework, даже мешать вашему клиентскому приложению во время выполнения.

3 голосов
/ 19 ноября 2011

Существует один сложный способ заставить работать метод «копирования», который займет некоторое время.

Взгляните на ILSpy , это приложение используется для просмотра и анализа существующихкод и с открытым исходным кодом.Вы можете извлечь код из проекта, который используется для анализа кода IL-ASM, и использовать его для копирования метода.

2 голосов
/ 24 ноября 2011

Этот ответ является немного ортогональным - больше о проблеме, чем о задействованной технологии.

Вы могли бы использовать деревья выражений - с ними приятно работать и иметь синтаксический сахар VS.

Для сериализации вам нужна эта библиотека (вам тоже нужно ее скомпилировать): http://expressiontree.codeplex.com/ (Очевидно, это работает и с Silverlight 4).

Ограничение деревьев выражений состоит в том, что они поддерживают только лямбда-выражения - т.е. без блоков.

Это на самом деле не ограничение, поскольку вы все равно можете определять другие лямбда-методы в лямбда-выражениях и передавать их помощникам по функциональному программированию, таким как Linq API.

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

Этот код работает:

using System;
using System.Linq;
using System.Linq.Expressions;
using ExpressionSerialization;
using System.Xml.Linq;
using System.Collections.Generic;
using System.Reflection;

namespace ExpressionSerializationTest
{
    public static class FunctionalExtensions
    {
        public static IEnumerable<int> to(this int start, int end)
        {
            for (; start <= end; start++)
                yield return start;
        }
    }

    class Program
    {
        private static Expression<Func<int, int, int>> sumRange = 
            (x, y) => x.to(y).Sum();

        static void Main(string[] args)
        {
            const string fileName = "sumRange.bin";

            ExpressionSerializer serializer = new ExpressionSerializer(
                new TypeResolver(new[] { Assembly.GetExecutingAssembly() })
            );

            serializer.Serialize(sumRange).Save(fileName);

            Expression<Func<int, int, int>> deserializedSumRange =
                serializer.Deserialize<Func<int, int, int>>(
                    XElement.Load(fileName)
                );

            Func<int, int, int> funcSumRange = 
                deserializedSumRange.Compile();

            Console.WriteLine(
                "Deserialized func returned: {0}", 
                funcSumRange(1, 4)
            );

            Console.ReadKey();
        }
    }
}
...