22 janvier 2009

Différentes façons de créer dynamiquement une méthode (3/3)

Ce billet es t le 3ème et dernier de la série. Tout comme les 2 précédents, il traite de la création dynamique d'une méthode et de son exécution.

Avec Mono.Cecil :

Tout d'abord, qu'est ce que Mono ? C'est un projet Open-Source supporté par Novell. Celui-ci a pour but de fournir une machine virtuelle, les compilateurs C# 3.0 et VB.Net (il doit y en avoir d'autres) et supporte notamment Silverlight (projet MoonLight). Mono.Cecil est un sous projet de Mono et propose à peu près les mêmes fonctionnalités que les classes des espaces de nom System.Reflection et System.Reflection.Emit.

D'une manière générale, Mono.Cecil est plus performant que son homologue et offre plus de possibilités. Patrick Smacchia a écrit un billet à ce sujet si cela vous intéresse.

Comparé au billet précédent, l'utilisation de Mono.Cecil, pour générer et exécuter dynamiquement une méthode, est sensiblement pareille. Il y a cependant quelques exceptions :
  • Je n'ai pas réussi à utiliser directement la méthode générée. J'ai du créer un assemblage à partir de la définition de l'assemblage modifié. Il doit être possible de faire autrement, mais je n'ai pas trouvé comment …

  • Mono.Cecil fait bien la distinction entre les TypeDef et le TypeRef (c'est un sujet un peu avancé j'avoue, même pour moi …). En gros, un TypeDef (définition de type) contient toutes les métadonnées pour un type donné, comme par exemple sa classe mère, les interfaces implémentées, les méthodes et les propriétés qui le composent, etc … Un typeRef (référence à un type) contient uniquement le nom du type et une variable faisant référence à l'assemblage/module auquel il appartient. Du coup, avec l'espace de nom System.Reflection, il se peut qu'on obtienne parfois des exceptions car on utilise sans le savoir un TypeRef alors qu'il faudrait un TypeDef …

Voici donc le code définissant et exécutant la méthode créée dynamiquement :

var defAssembly = AssemblyFactory.GetAssembly(Assembly.GetExecutingAssembly().Location);
addGeneratedMethod(defAssembly);
var modifiedAssembly = AssemblyFactory.CreateReflectionAssembly(defAssembly);
var modifiedType = modifiedAssembly.GetType("DynamicIL.Program");
modifiedType.GetMethod("helloCecil").Invoke(null, null);


Voilà l'explication de chaque instruction :
  • Premièrement, on récupère une définition de l'assemblage.

  • On fait appel à la méthode addGeneratedMethod qui créé et ajoute une méthode statique au type de la classe contenant la méthode Main. Son implémentation est expliquée juste après.

  • Un assemblage est créé à partir de la définition de l'assemblage modifié.

  • On récupère le type de la classe DynamicIL.Program?.

  • La méthode générée est appelée.

Voyons maintenant le corps de la méthode addGeneratedMethod :

private static void addGeneratedMethod(AssemblyDefinition pAssembly)
{
    var type = pAssembly.MainModule.Types.Cast<TypeDefinition>().Where(typeDef => typeDef.Name == "Program").First();
    var methodAttributes = Mono.Cecil.MethodAttributes.Public  Mono.Cecil.MethodAttributes.Static; 
    TypeReference voidType = pAssembly.MainModule.Import(typeof(void));
    MethodDefinition methodDef = new MethodDefinition("helloCecil", methodAttributes, voidType);
    MethodInfo methodInfo = typeof(System.Console).GetMethod("WriteLine", new Type[] { typeof(string) });
    var methodRef = pAssembly.MainModule.Import(methodInfo);

    CilWorker cilWorker = methodDef.Body.CilWorker;
    cilWorker.Emit(Mono.Cecil.Cil.OpCodes.Ldstr, "Hello World avec Mono.Cecil !");
    cilWorker.Emit(Mono.Cecil.Cil.OpCodes.Call,methodRef);
    cilWorker.Emit(Mono.Cecil.Cil.OpCodes.Ret);
 
    type.Methods.Add(methodDef);
}


Voici le détail de l'implémentation de la méthode:
  • On récupère la définition du type Program.

  • Une énumératoin Mono.Cecil.MethodAttributes est assignée pour que la méthode générée soit publique et statique.

  • Une référence au type void est récupérée. Celle-ci sert aussi à la création de la méthode statique.

  • Via une instance de la classe MethodDefinition, on définie la signature de la méthode. Celle-ci s'appelle helloCecil, elle est publique ainsi que statique et renvoie un type void (c'est-à-dire rien).

  • Les 2 instructions suivantes servent à récupérer une référence à la méthode WriteLine de la classe Console prenant une chaine de caractère en paramètre.

  • Les 4 instructions d'après permettent de définir le corps de la méthode de la méthode avec des instructions MSIL. Celles-ci sont équivalentes à celles utilisées dans l'exemple précédent.

  • Finalement, la définition de la méthode est ajoutée à la définition du type "Program".

A l'exécution du programme, vous devriez voire apparaître à l'écran "Hello World avec Mono.Cecil !".

Pour plus d'informations, l'archive ci-dessous reprend le code des 3 méthodes de génération dynamique présentées.

Aucun commentaire: