Créer une méthode dynamique et l'exécuter

Contexte:

je veux définir quelques static méthodes en C#, et générer du code IL comme tableau d'octets, à partir de l'une de ces méthodes, sélectionnée à l'exécution (sur le client), et envoyer le tableau d'octets sur le réseau à une autre machine (serveur) où il devrait être exécuté après avoir recréé le code IL À partir du tableau d'octets.

Ma Tentative: ( POC)

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

et puis j'obtiens le code IL du corps de la méthode, comme:

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

jusqu'à présent je n'ai rien créé dynamiquement. Mais j'ai le code IL comme tableau d'octets, et je veux créer un assemblage, puis un module dedans, puis un type, puis une méthode - tout dynamiquement. Lors de la création du corps de méthode de la méthode dynamiquement créée, j'utilise le code IL que j'ai obtenu en utilisant la réflexion dans le code ci-dessus.

Le code de génération de code est comme suit:

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

Mais au lieu d'imprimer 98780 sur la fenêtre de sortie, il lance un exception en disant:

Système.Réflexion.TargetInvocationException: Exception a été lancée par la cible d'une invocation. ---> Système.TypeLoadException: impossible de charger Type 'Invalid_Token.0x0100001E 'from assembly' MyDLL, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'.

     sur MyType.MyMethod(Int32 valeur1, Int32 valeur2) [...]

Merci de m'aider à trouver la cause de l'erreur et comment la corriger il.

27
demandé sur Nawaz 2011-10-06 11:14:01

6 réponses

Exécuter ildasm.exe sur un arbitraire de l'assemblée. Utilisez View + Show token values et regardez du code démonté. Vous verrez que L'IL contient des références à d'autres méthodes et variables à travers un nombre. Le numéro est un index dans les tables de métadonnées d'un assemblage.

peut-être voyez-vous maintenant le problème, vous ne pouvez pas transplanter un morceau de IL d'un assemblage à un autre à moins que cet assemblage cible a le même métadonnées. Ou à moins que vous ne remplaciez le jeton de métadonnées valeurs dans L'IL avec des valeurs qui correspondent aux métadonnées de l'ensemble cible. Bien sûr, c'est très peu pratique, vous finissez essentiellement par dupliquer l'Assemblée. Pourrait tout aussi bien faire une copie de l'assemblée. Ou d'ailleurs, pourrait aussi bien utiliser L'IL existant dans l'Assemblée d'origine.

vous avez besoin de réfléchir un peu, il est assez peu clair ce que vous essayez réellement d'accomplir. système.CodeDom et réflexion.Émettre des espaces de noms sont disponibles pour générer dynamiquement code.

18
répondu Hans Passant 2011-11-19 14:06:22

Si je prends la méthode suivante:

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

compiler en mode Release et à utiliser dans votre code, tout fonctionne bien. Et si je inspecter l' il array, il contient 4 octets qui correspondent exactement aux quatre instructions de Jon réponse (ldarg.0, ldarg.1, mul, ret).

si je le compile en mode debug, le code est de 9 octets. Et dans le Réflecteur, il ressemble à ceci:

.method public hidebysig static int32 Multiply(int32 a, int32 b) cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 CS00)
    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 partie problématique est la variable locale. Si vous venez de copier les octets d'instruction, vous émettez l'instruction qui utilise une variable locale, mais vous ne la déclarez jamais.

Si vous utilisez ILGenerator, vous pouvez utiliser DeclareLocal(). Il semble que vous serez capable de définir des variables locales en utilisant MethodBuilder.SetMethodBody().Net 4.5 (travaux ci-après pour moi de VS 11 DP):

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

mais je n'ai pas trouvé le moyen de le faire dans .Net 4, sauf en utilisant la réflexion pour définir un champ privé de MethodBuilder, qui contient les variables locales, après appel CreateMethodBody():

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

en ce qui concerne l'erreur initiale: Types et méthodes d'autres assemblages (comme System.Console et System.Console.WriteLine) sont référencés en utilisant des tokens. Et ces signes sont différents d'une assemblée à l'autre. Que signifie le code pour appeler Console.WriteLine() dans un assemblage sera différent du code pour appeler la même méthode dans un autre assemblage, si vous regardez les octets d'instruction.

ce que cela signifie est que vous devez réellement comprendre ce que les octets IL signifient et remplacer tous les tokens qui font référence aux types, méthodes, etc. à ceux qui sont valides dans l'assemblée que vous construisez.

10
répondu svick 2011-11-19 15:32:19

je pense que le problème est d'utiliser IL d'un type/assemblage dans un autre. Si vous remplacez ceci:

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

avec ceci:

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

puis il exécutera la méthode correctement (Non Console.WriteLine, mais il retourne la bonne valeur).

si vous avez vraiment besoin d'être en mesure de slurp IL à partir d'une méthode existante, vous aurez besoin de regarder plus loin - mais si vous avez juste besoin de validation que le reste du code fonctionnait, cela peut aider.

Une chose que vous pouvez trouver intéressant est que l'erreur change dans votre code original Si vous prenez le Console.WriteLine appel de Experiment. Il devient un InvalidProgramException à la place. Je n'ai aucune idée pourquoi...

8
répondu Jon Skeet 2011-10-06 07:30:31

si je comprends bien votre problème, et que vous voulez juste générer dynamiquement un peu de code .NET, et l'exécuter sur un client distant, maeby jeter un oeil à IronPython. Vous avez juste besoin de créer une chaîne de caractères avec le script puis de l'envoyer au client, et le client peut l'exécuter à l'exécution avec l'accès à tout le Framework .NET, même interférer avec votre application client à l'exécution.

4
répondu Piotr Styczyński 2011-11-25 09:33:25

il y a un moyen difficile d'obtenir la méthode de "copie" et cela prendra du temps.

regardez ILSpy, cette application est utilisée pour visualiser et analyser le code existant et est open-source. Vous pouvez extraire le code du projet qui est utilisé pour analyser le code IL-ASM et l'utiliser pour copier la méthode.

3
répondu Felix K. 2011-11-19 18:52:23

cette réponse est un peu orthogonale - plus sur le problème que la technologie impliquée.

vous pourriez utiliser des arbres d'expression-ils sont agréables à travailler et ont VS sucre syntaxique.

Pour la sérialisation vous avez besoin de cette bibliothèque (vous aurez besoin de compiler trop): http://expressiontree.codeplex.com/

la limitation des arbres d'expression est qu'ils ne supportent que Lambda expressions - c'est à dire pas de blocs.

ce n'est pas vraiment une limitation car vous pouvez encore définir d'autres méthodes lambda au sein d'une lambda et les passer à des helpers de programmation fonctionnelle tels que L'API Linq.

j'ai inclus une méthode d'extension simple pour vous montrer comment aller sur l'ajout d'autres méthodes d'aide pour les trucs fonctionnels - le point clé est que vous devez inclure les assemblages contenant les extensions dans votre serializer.

ce code fonctionne:

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
répondu Seth 2011-11-24 02:00:25