Подтвердить что ты не робот

Создание метода динамически и выполнение его

Фон:

Я хочу определить несколько методов static в С# и сгенерировать 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, Version = 0.0.0.0, Culture = neutral, PublicKeyToken = null'.
    в MyType.MyMethod(Int32 value1, Int32 value2)    [...]

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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

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

Ответ 3

Я думаю, что проблема заключается в том, чтобы использовать 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

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

Ответ 5

Существует один трудный способ заставить метод "copy" работать и займет некоторое время.

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

Ответ 6

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

Вы можете использовать деревья выражений - с ними приятно работать и иметь синтаксический сахар 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();
        }
    }
}