Crear método dinámicamente y ejecutarlo


Antecedentes:

Quiero definir algunos métodos static en C# , y generar código IL como matriz de bytes, a partir de uno de estos métodos, seleccionado en tiempo de ejecución (en el cliente), y enviar la matriz de bytes a través de la red a otra máquina (servidor) donde se debe ejecutar después de volver a generar el código IL desde la matriz de bytes.

Mi Intento: (POC)

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

Y luego obtengo el código IL del cuerpo del método, como:

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

Hasta ahora no he creado nada dinámica. Pero tengo código IL como matriz de bytes, y quiero crear un ensamblaje, luego un módulo en él, luego un tipo, luego un método - todo dinámicamente. Al crear el cuerpo del método del método creado dinámicamente, utilizo el código IL que obtuve usando reflexión en el código anterior.

El código de generación de código es el siguiente:

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

Pero en lugar de imprimir 98780 en la ventana de salida, arroja una excepción diciendo,

Sistema.Reflexión.Excepción de la promoción del objetivo: La excepción ha sido lanzada por el objetivo de una invocación. ---> Sistema.TypeLoadException: No se pudo cargar el tipo 'Invalid_Token.0x0100001E 'from assembly' MyDLL, Version = 0.0.0.0, Culture = neutral, PublicKeyToken = null'.
     a mi tipo.MyMethod(Int32 valor1, Int32 valor2) [...]

Por favor, ayúdame a averiguar la causa del error, y cómo solucionarlo.

Author: Nawaz, 2011-10-06

6 answers

Ejecutar ildasm.exe sobre una asamblea arbitraria. Use Ver + Mostrar valores de token y observe un código desensamblado. Verás que el IL contiene referencias a otros métodos y variables a través de un número. El número es un índice en las tablas de metadatos de un ensamblado.

Tal vez ahora vea el problema, no puede trasplantar un trozo de IL de un ensamblado a otro a menos que ese ensamblado de destino tenga los mismos metadatos. O a menos que reemplace los valores de token de metadatos en el IL con valores que coinciden con los metadatos del ensamblado de destino. Esto es tremendamente poco práctico, por supuesto, que esencialmente terminan duplicando la asamblea. También podría hacer una copia de la asamblea. O para el caso, también podría utilizar el IL existente en el ensamblaje original.

Tienes que pensarlo un poco, no está muy claro lo que realmente intentas lograr. sistema.CodeDom y Reflexión.Los espacios de nombres Emit están disponibles para generar código dinámicamente.

 18
Author: Hans Passant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-19 14:06:22

Si tomo el siguiente método:

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

Compílalo en modo Release y úsalo en tu código, todo funciona bien. Y si inspecciono la matriz il, contiene 4 bytes que corresponden exactamente a las cuatro instrucciones de la respuesta de Jon (ldarg.0, ldarg.1, mul, ret).

Si compilo en modo debug, el código es de 9 bytes. Y en Reflector, se ve así:

.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 
}

La parte problemática es la variable local. Si solo copia los bytes de instrucción, emit instruction that uses a local variable, but you never declare it.

Si estuviera usando ILGenerator, podría usar DeclareLocal(). Parece que usted será capaz de establecer variables locales utilizando MethodBuilder.SetMethodBody() en. Net 4.5 (lo siguiente funciona para mí en VS 11 DP):

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

Pero no he encontrado una manera de hacerlo en. Net 4, excepto mediante el uso de reflexión para establecer un campo privado de MethodBuilder, que contiene las variables locales, después de llamar a CreateMethodBody():

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

Con respecto al error original: Tipos y los métodos de otros ensamblajes (como System.Console y System.Console.WriteLine) son referenciados usando tokens. Y esas fichas son diferentes de una asamblea a otra. Eso significa que el código para llamar Console.WriteLine() en un ensamblado será diferente del código para llamar al mismo método en otro ensamblado, si nos fijamos en los bytes de instrucción.

Lo que eso significa es que tendría que entender realmente lo que significan los bytes IL y reemplazar todos los tokens que hacen referencia a tipos, métodos, etc. a los que son válidos en el asamblea que estás construyendo.

 10
Author: svick,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-19 15:32:19

Creo que el problema tiene que ver con el uso de IL de un tipo/ensamblado en otro. Si reemplaza esto:

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

Con esto:

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

Entonces ejecutará el método correctamente (no Console.WriteLine, pero devuelve el valor correcto).

Si realmente necesita ser capaz de sorber IL de un método existente, tendrá que buscar más, pero si solo necesita validar que el resto del código estaba funcionando, esto puede ayudar.

Una cosa que usted puede encontrar interesante es que el error cambia en su código original si saca la llamada Console.WriteLine de Experiment. Se convierte en un InvalidProgramException en su lugar. No tengo idea de por qué...

 8
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-06 07:30:31

Si entiendo bien su problema, y solo desea generar dinámicamente algo de código.NET y ejecutarlo en cliente remoto, puede echar un vistazo a IronPython. Solo necesita crear una cadena con script y luego enviarla al cliente, y el cliente puede ejecutarla en tiempo de ejecución con acceso a todo. NET Framework, incluso interferir con su aplicación cliente en tiempo de ejecución.

 4
Author: Piotr Styczyński,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-25 09:33:25

Hay una manera difícil de hacer que el método "copiar" funcione y tomará un tiempo.

Echa un vistazo a ILSpy, esta aplicación se utiliza para ver y analizar código existente y es de código abierto. Puede extraer el código del proyecto que se utiliza para analizar el código IL-ASM y usarlo para copiar el método.

 3
Author: Felix K.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-19 18:52:23

Esta respuesta es un poco ortogonal - más sobre el problema que la tecnología involucrada.

Puedes usar árboles de expresión - es bueno trabajar con ellos y tienen VS azúcar sintáctico.

Para la serialización necesitas esta biblioteca (también tendrás que compilarla): http://expressiontree.codeplex.com / (Esto también funciona con Silverlight 4, aparentemente.)

La limitación de los árboles de expresión es que solo admiten expresiones Lambda, es decir, no bloque.

Esto no es realmente una limitación porque aún puede definir otros métodos lambda dentro de un lambda y pasarlos a ayudantes de programación funcional como la API Linq.

He incluido un método de extensión simple para mostrarle cómo agregar otros métodos auxiliares para cosas funcionales - el punto clave es que necesita incluir los ensamblados que contienen las extensiones en su serializador.

Este código funciona:

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();
        }
    }
}
 2
Author: Seth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-24 02:00:25