Génération dynamique de code .NET et Java
Introduction
La génération dynamique de code est certainement l’une des fonctionnalités les plus intéressantes du Framework .NET. Imaginez un instant de pouvoir générer dans une application du code C# ou VB.NET à la volée, et ensuite de compiler et exécuter ce code dans le même processus !. Vous vous demandez sûrement dans quel but ? Et bien il existe une multitude de domaines où cette fonctionnalité peut s’avérer intéressante. Par exemple, dans le cadre d’une application ayant besoin de modifier son comportement dynamiquement afin d’introduire de nouveaux types et ce, sans être arrêtée. Typiquement, en Java, cette caractéristique est essentielle dans le cadre des moteurs de pages JSP. Ainsi, lorsque le développeur modifie le contenu d’une page JSP, le serveur génère l’implémentation d’une servlet liée à la page en question. Cette servlet est ensuite chargée par le conteneur et devient aussitôt disponible pour l’application. Vous pouvez ainsi à tout moment créer et compiler de nouvelles servlets sans avoir à arrêter le serveur Web. Quelle magie !
L’article suivant se propose de vous illustrer la génération dynamique de code sous .NET, tout en faisant un parallèle avec Java.
Comment ça marche ?
La génération dynamique de code ou Emit Code est basée sur le principe d’introspection ou de Reflection. C’est la capacité pour un environnement donné à intégrer dynamiquement de nouveaux types. Alors, bien entendu cette fonctionnalité n’est pas sans poser de problèmes, notamment au niveau de la sécurité. Imaginez un code ayant le pouvoir de s’auto-modifier tel un mutant. Il est très difficile d’analyser avec des outils « à priori » le comportement d’un tel programme. Par exemple, le CLR utilise, tout comme Java, un Verifier de code dont le rôle principal est de vérifier que le code MSIL généré ne brise pas les règles élémentaires énoncées dans les spécifications de la CLS (typages, gestion mémoire…). Vous imaginez bien que dans un tel contexte, la maîtrise du comportement d’un tel programme s’avère des plus complexes car elle ne se résume pas à une simple analyse statique.
En Java, ce sont les ClassLoaders qui ont la lourde tâche d’effectuer le chargement de nouvelles classes dans la JVM. Il suffit d’indiquer au ClassLoader le chemin où se situe le ByteCode (fichiers .class) de la classe en question et ensuite d’invoquer la méthode loadClass() du chargeur attitré. En cas de problème, un SecurityManager jouant le rôle d’une SandBox (bac à sable) se charge de vérifier que le code en question ne porte pas atteinte aux différentes règles de sécurité configurées par le développeur.
Ainsi en .NET le chargement d’une classe s’effectue de la manière suivante :
public static int Main()
{
Assembly
a =
Assembly.LoadFrom(@"DotNetGuru.Samples.classes.dll");
MonType
myvar = (MonType) a.CreateInstance("MyClassInThisDLL");
}
En Java, l’équivalent nous donne :
{
ClassLoader loader = this.getClassLoader();
MyType main = (MyType)loader.loadClass("MyClass", true).newInstance();
}
Le Framework .NET pousse le raisonnement un peu plus loin avec la possibilité de générer et de manipuler dynamiquement du ByteCode, ce qui n’est pas le cas en Java à moins de passer par des outils externes. En Java, cet aspect est laissé à la charge des compilateurs.
Dans l’exemple suivant, nous allons créer dynamiquement une méthode HelloWorld() à l’aide de l’API de Reflection puis générer ensuite le code MSIL correspondant.
{
//
La méthode à créer est public String HelloWorld()
//
On commence par définir les paramètres d'entrées et de sorties
Type[]
paramTypes = new Type[0];
Type
returnType = typeof(string);
// Définition de
la signature de la méthode
MethodBuilder
simpleMethod =
myType.DefineMethod("HelloWorld",
MethodAttributes.Public,
returnType,
paramTypes);
}
Une fois la méthode construite, il ne nous reste plus qu’à
générer le code MSIL correspondant en utilisant : ILGenerator
generator = simpleMethod.GetILGenerator( );
ILGenerator generator =
simpleMethod.GetILGenerator( );
generator.Emit(OpCodes.Ldstr,
"Bonjour tout le monde !");
generator.Emit(OpCodes.Ret);
// Introduction
du nouveau type dans la machine virtuelle et le tour est joué !
MyType.CreateType();
Dans l’exemple précédent, nous avons juste placé la chaîne « Bonjour tout le monde » sur la pile et ensuite effectué un appel de retour de fonction (ret) tel que décrit sur le schéma suivant :

La génération et le chargement de code sont des opérations qui occupent du temps processeur. Il faut donc veiller à s’en servir efficacement compte tenu du fait que leur utilisation est forcément moins performante que la résolution statique de types.
L’exemple précédent, s’il vous montre bien comment fonctionne le Framework .NET à propos de l’émission de code, ne doit pas faire oublier que cette caractéristique ne sera vraisemblablement utilisée que dans des cas bien particuliers. En effet, combien d’entre nous maîtrisent réellement le MSIL pour générer des applications réellement complexes ?
C’est pourquoi, dans ce souci de simplification, nous allons poursuivre l’étude des APIs afin de nous apercevoir qu’il est aussi possible de faire appel à un compilateur de haut niveau tel que C# ou VB.NET pour effectuer cette génération de byte-code. Il est évident que nous préfèrerons tous générer du code source C# et le compiler de manière programmatique, plutôt que d’utiliser directement du MSIL un peu « barbare », mais il faut garder à l’esprit que cela se fera encore une fois au détriment du temps machine puisque nous introduirons une étape supplémentaire (la compilation).
Compiler et charger du code C# à l’intérieur d’une application
Imaginez une application chargée de réaliser le mapping entre un modèle objet et un modèle physique de stockage (une base relationnelle par exemple). Un mapping consiste à créer un objet métier par table dans votre base. Or, il est possible à tout moment qu’une nouvelle table soit créée manuellement (en SQL par exemple), et cela doit se traduire par la génération d’une classe d’accès aux données fournissant toute l’implémentation ADO.NET nécessaire aux accès en base (select, update, delete). Toute apparition de la table Facture par exemple, doit se traduire par la génération d’un objet du type FactureADO (données) et Facture (métier) et par le chargement dynamique de ces classes dans l’application, tout cela sans arrêter l’application. C’est un des cas d’utilisation de la génération dynamique de code et contrairement à l’exemple précédent, on imagine difficilement pouvoir générer du ByteCode MSIL afin de réaliser toutes ces tâches ô combien complexes « à la main ». C’est pourquoi, le Framework met à disposition du développeur un ensemble d’APIs permettant de contrôler le compilateur C# ou VB.NET de manière totalement programmatique. Vous pouvez ainsi générer du code source, beaucoup plus parlant pour vous que du MSIL, et le compiler directement dans le même processus.

Vous seriez surpris à quel point cette opération est d’une étonnante simplicité. Pour vous le démontrer, nous avons choisi d’implémenter un IDE UltraLight en C#. Vous pensez peut-être que ce genre d’outil doit être complexe à réaliser au regard des fonctionnalités requises. Que ne ni, la figure suivante vous montre l’outil en question.

Une fois que le code lié à l’interface graphique est éliminé du fichier source, il nous reste en tout et pour tout, 16 Lignes de code !! pour réaliser ce programme. Non vous ne rêvez pas, l’API du Framework vous simplifie énormément la tâche, encore faut-il savoir s’en servir. Etudions maintenant le source de cette application :
private void compiler_Click(object sender, System.EventArgs e)
{
//
Utilisation de l'API pour demander un provider C#
// Pattern Factory
CSharpCodeProvider
codeProvider = new CSharpCodeProvider();
ICodeCompiler icc =
codeProvider.CreateCompiler();
// Nom
temporaire du programme qu'on veut générer
// Peut
être paramétré dans l'IHM
string Output = "tmp.exe";
Button
ButtonObject = (Button) sender;
textBox2.Text
= "";
System.CodeDom.Compiler.CompilerParameters
parameters = new CompilerParameters();
// Nous utiliserons Process.Start() pour
lancer l'application ainsi créée
// C'est
pourquoi, il est préférable de générer un EXE
parameters.GenerateExecutable
= true;
parameters.OutputAssembly
= Output;
// Lance la
compilation et récupère les messages du compilateur C#
CompilerResults
results = icc.CompileAssemblyFromSource(parameters,textBox1.Text);
// On itère sur
l'ensemble des messages d'erreurs
if (results.Errors.Count > 0)
{
textBox2.ForeColor
= Color.Red;
foreach(CompilerError CompErr in results.Errors)
{
textBox2.Text
= textBox2.Text +
"Line
number " + CompErr.Line +
",
Error Number: " + CompErr.ErrorNumber +
", '" +
CompErr.ErrorText + ";" +
Environment.NewLine
+ Environment.NewLine;
}
}
else
{
//La
compilation s'est bien passée
textBox2.ForeColor
= Color.Blue;
textBox2.Text =
"Compilation OK";
//Si
l'utilisateur l'a demandé, on exécute dans un process séparé le programme
if (ButtonObject.Text == "Run")
Process.Start(Output);
}
}
Tout se passe au niveau du code : icc.CompileAssemblyFromSource(String) .
L’Assembly ainsi générée peut ensuite être chargée dans le programme tel que nous l’avons vu précédemment avec le code MSIL à l’aide de l’instruction Assembly.LoadFrom(). Quant au namespace CodeDom, son rôle est de vous représenter sous la forme d’un arbre hiérarchique un fichier source. Nous ne l’utilisons pas réellement dans ce programme du fait que le source est généré via un TextField et non à partir des APIs.
Comment
faire l’équivalent en Java ?
En Java, l’ensemble des outils liés au compilateur sont dans une bibliothèque incluse dans le J2SDK (Java 2 Software Development Kit) : tools.jar. Les développeurs de pages JSP connaissent bien cette archive qui est requise par les conteneurs Web pour disposer du mécanisme de compilation dynamique de servlets.
Tools.jar contient l’ensemble des outils fournis par Sun, ce sont des wrappers qui encapsulent les outils javac, jdb, java, rmic, etc … Officiellement, ces packages ne sont pas destinés à être utilisé par le public car leur implémentation peut à tout moment évoluer sans que Sun prenne les précautions habituelles de notification à la communauté. Il existe plusieurs projets tendant à standardiser l’accès à ces outils avec notamment JPDA (Java Platform Debugger Architecture) pour la partie Debug. L’exemple suivant illustre l’utilisation de l’API com.sun.tools afin de générer le code d’une classe qui sera ensuite instancié et chargé dans l’application. Vous remarquerez les similitudes entre .NET et Java dans la démarche générale.
import java.io.*;
import java.util.*;
import java.lang.reflect.*;
// API contenu dans la bibliothèque tools.jar de SUN
import com.sun.tools.javac.Main;
public class Compile {
String ClassName = "MyClass";
String ClassSource = "MyClass.java";
public static void main (String args[]){
Compile mtc = new Compile();
// On génère à la
volée le code source Java du programme
mtc.createIt();
// Puis on le compile
if (mtc.compileIt()) {
System.out.println("Running
" + mtc.ClassName + ":\n\n");
// Et enfin on l'exécute
mtc.runIt();
}
else
System.out.println(mtc.ClassSource
+ "
contient des erreurs de compilation.");
}
public void createIt()
{
try {
// Créer le fichier
dans un répertoire visible du default ClassLoader
FileWriter aWriter = new FileWriter(ClassSource, true);
// Voici la génération du contenu
aWriter.write(" ");
aWriter.write(" public class
MyClass {");
aWriter.write(" public void hello() {");
aWriter.write("
System.out.println(\"Hello tous le monde ! \");");
aWriter.write(" }}\n");
aWriter.flush();
aWriter.close();
}
catch(Exception
e){
e.printStackTrace();
}
}
public boolean compileIt()
{
String [] source = { new String(ClassSource)};
ByteArrayOutputStream baos= new ByteArrayOutputStream();
//new
sun.tools.javac.Main(baos,source[0]).compile(source);
// if using JDK >= 1.3 then use
int result
= com.sun.tools.javac.Main.compile(source);
System.out.println(baos.toString());
return (baos.toString().indexOf("error")==-1);
}
public void runIt()
{
try {
// Utilise l'API
Reflection pour charger la classe
Class params[] = {};
Object paramsObj[] = {};
// Introduit le nouveau type : magique, non !?
Class thisClass = Class.forName(ClassName);
Object iClass = thisClass.newInstance();
/// Invoke la méthode hello de la classe dynamiquement créée
Method thisMethod = thisClass.getDeclaredMethod("hello", params);
thisMethod.invoke(iClass, paramsObj);
}
catch (Exception
e) {
e.printStackTrace();
}
}
}
WebServices
et Proxy Dynamique
Un autre domaine d’application de la génération dynamique de code pourrait être l’invocation de WebServices. Rappelez-vous, l’interface d’un WebService est spécifiée dans son fichier WSDL. Ce même fichier est ensuite utilisé afin de générer un fichier Proxy. Toute cette partie se fait en règle générale manuellement à l’aide d’outils tels que Wsdl.exe pour .NET. Nous pourrions très bien imaginer un programme qui se chargerait de lire le contenu du fichier WSDL et de générer dynamiquement le code du Proxy en fonction de l’interface. D’ailleurs, vous aurez peut-être remarqué que IIS vous permet via votre navigateur de tester un WebService à partir d’une interface Web spécifique. Il utilise pour réaliser cette opération, la génération dynamique de code afin de simplifier la tâche du développeur.
Une idée
originale de WebService
Comme nous l’avons vu précédemment, il existe une multitude de domaines où la génération dynamique de code peut être intéressante mais aussi parfois, surprenante, suivez bien.
Les applets .NET que nous avons déjà abordé dans un précédent article, permettent d’exécuter du code .NET coté client. Pour cela, le client doit disposer d’une machine virtuelle adéquate (CLR) compatible avec l’application (Assembly) téléchargée (DLL ou EXE). Or, à l’heure actuelle, il existe encore certaines personnes n’ayant pas pu disposer des versions ultérieures du Framework et possédant encore des BETA 2 de ce même Framework. Sans compter ceux ayant opté pour la Release Candidate fournie par Microsoft en attendant de faire l’acquisition de la RTM. A l’avenir, ce genre de situation apparaîtra forcément de la même manière qu’en Java il existe de multiples versions du Plug-In ou de la JRE (Java Runtime Environment). Dans ce contexte, vous comprendrez donc que l’implémentation d’un contrôle client WinForm (Applet .NET) s’avère des plus délicates lorsqu’il s’agit de cibler l’ensemble des différentes versions de CLR.
Dans ce cas précis, est-ce que l’association WebService + Compilation Dynamique de code ne pourrait-elle pas nous aider ? Imaginez un WebService dont le rôle est de compiler un programme source passé en paramètre et de renvoyer l’Assembly correspondante sous la forme d’un flux binaire. Ce WebService aurait en plus la faculté de vous compiler le source en ciblant n’importe quelle version du Framework. Il suffit que plusieurs versions co-existent sur la même machine ou sur plusieurs WebServices différents. De plus, un serveur est tout à fait capable de récupérer la version de la CLR cliente installée en utilisant la propriété User-Agent tel que décrit ci-après.
GET /dotnetguru.html HTTP/1.1Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-powerpoint, application/vnd.ms-excel, application/msword, */*Accept-Language: en-usAccept-Encoding: gzip, deflateUser-Agent: Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 4.0; .NET CLR 1.0.2914)
Host: localhostConnection: Keep-Alive
Vous l’aurez compris, notre serveur hébergeant l’applet se
servirait du WebService précédent afin de compiler le code source de l’applet en
fonction de la plate-forme cliente. Nul besoin de disposer du Framework .NET
sur ce serveur, plus aucun problème de compatibilité de versions, sans compter
que les ressources dédiées à la compilation se trouvent déportées sur une
machine tierce sûrement plus robuste, etc … Bref, vous l’aurez compris, la
génération de code dynamique associée au WebServices permet d’entrevoir une
multitude d’applications de toutes sortes. Reste ensuite à régler les problèmes
liés à la sécurité mais aussi au transfert d’Assembly spécifiques dépendantes.
Ce mécanisme fonctionne très bien dans le cadre d’un programme autonome mais
dès qu’il s’agit d’associer des Assemblies externes, la manipulation s’avère
plus compliquée…

Conclusion
.NET et Java trouvent encore un point commun dans la génération dynamique de code, cet exemple est là pour vous le démontrer. Si l’implémentation diffère sur certains aspects, la démarche générale reste très similaire, avec l’API de Reflection qui n’est jamais très loin car elle permet de manipuler dynamiquement les classes ainsi créées dont la résolution statique de type n’est pas possible à priori.
Cet article est loin (très loin) d’être exhaustif sur cette
fonctionnalité intéressante de .NET, il reste encore d’autres sujets connexes à
celui-ci tels que les domaines d’application ou les chargeurs de classe
(ClassLoader). Un prochain article pourrait très bien être Java
ClassLoader versus .NET ClassLoader, A suivre donc …
Téléchargez le code source de cet article :
DynamicGen.cs (IDE ultra-light)
Auteur : Sami Jaber
Copyright 2002 Ó
DotNetGuru