Utilisation de CodeDOM en C#

Lainé Vincent

Nous avons parfois besoin de compiler du code à la volée que ce soit pour faire de petit moteur de script ou bien on aimerait générer du code de façon indépendante du langage final. Dans cet article nous verrons comment le faire grâce au mécanisme de CodeDOM de .NET.

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Utilisation de CodeDOM en C#

- Auteur : Lainé Vincent

- Parution : 7 juin 2006

- Mise à jour : 7 juin 2006

- Licence : Copyright ® 2006 Lainé Vincent. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. La création de code dans CodeDOM

- II-A. Ajouter un espace de noms

- II-B. Ajouter une directive using

- II-C. Ajouter une classe

- II-D. Ajouter une méthode

- II-D-1. Ajouter un corps de fonction

- II-D-1-a. Déclaration d'un type

- II-D-1-b. Instancier un objet

- II-D-1-c. Invoquer une méthode

- II-E. Conclusion

- III. Générer le code source dans un langage spécifique

- IV. Générer un assembly à partir du graph d'objets

- V. Générer un assembly à partir d'un fichier source

- VI. Pourquoi utiliser CodeDOM dans mes applications ?

- VII. Description de l'application exemple

- VIII. Liens

I - Introduction

Peu de gens connaissent l'espace de nom System.CodeDOM du framework dotnet. Pourtant cet espace de nom est utilisé a chaque fois que vous vous servez de votre EDI préféré et de designer graphique.
C'est grâce au CodeDOM que les designers comme celui de Visual Studio ou de SharpDevelop sont capables de générer le code correspondant à votre demande et cela dans le langage de votre choix.
À travers cet article nous verrons les principales méthodes à utiliser pour générer de façon indépendante du langage final.

II - La création de code dans CodeDOM

Avant de parler de compilation dynamique ou de génération de code source, il faut savoir comment représenter le programme dans CodeDOM. Dans CodeDOM, tous les aspects des langages de programmation sont représentés par des classes. Ainsi vous avez une classe qui va permettre de représenter une classe, une autre qui vous permettra de placer un test « if », etc. Toutes ces classes doivent et sont reliées entre elles dans ce que l'on appelle un graphique d'objets. Ce terme est plus simple qu'il n'y paraît, car en pratique il ne signifie rien d'autre que tout objet (qu'il représente une classe, une méthode ou autre chose) doit posséder un et un seul parent.
Tous les arbres ont une racine. La racine de cet « arbre CodeDOM » est une instance de la classe CodeCompileUnit.

//Création de la racine de notre "arbre CodeDOM"
CodeCompileUnit codeBase = new CodeCompileUnit();

Une fois que l'on a la racine, voyons ce que l'on peut mettre dedans.

II-A - Ajouter un espace de noms

La première et la seule chose que nous pouvons ajouter à cette racine est un espace de noms.

//Création de l'espace de noms
CodeNamespace nameSpace = new CodeNamespace("MonEspaceDeNom");

//Ajout de l'espace de noms dans l'arbre
codeBase.Namespaces.Add(nameSpace);

L'ajout d'espace de noms est l'opération la plus simple qui soit.

II-B - Ajouter une directive using

Afin de pouvoir utiliser les nombreuses classes du framework, nous allons ajouter une directive using.
La particularité de CodeDOM sur ce point est que les directives using sont insérées dans l'espace de noms.

codeBase.Namespaces[0].Imports.Add(new CodeNamespaceImport("System"));

Là encore ce n'est pas bien compliqué.

II-C - Ajouter une classe

Maintenant que nous avons nos outils prêts, nous pouvons passer à la construction de notre classe.
En premier lieu, nous allons ajouter une classe à notre code.

CodeTypeDeclaration newClass = new CodeTypeDeclaration(className);
newClass.IsClass = true;
this.codeBase.Namespaces[0].Types.Add(newClass);

Grâce à ce code, nous avons ajouté une classe toute simple. Par défaut les classes générées sont publiques.
Voyons maintenant comment faire de l'héritage.

CodeTypeDeclaration newClass = new CodeTypeDeclaration(className);
newClass.IsClass = true;
//On spécifie un type de base.
newClass.BaseTypes.Add("System.Windows.Forms.Form");
this.codeBase.Namespaces[0].Types.Add(newClass);

Petite remarque sur le code au-dessus : Rien dans ce code ne vous empêche de faire de l'héritage multiple. Par contre le compilateur ne l'acceptera pas. Cela nous montre bien qu'il n'y a pas de vérification de syntaxe pendant l'écriture du code avec CodeDOM.
Les créations d'interfaces, d'énumérations et de structures se font de la même façon.

Création d'une énumération

CodeTypeDeclaration newClass = new CodeTypeDeclaration(className);
newClass.IsEnum = true;
this.codeBase.Namespaces[0].Types.Add(newClass);

Création d'une interface

CodeTypeDeclaration newClass = new CodeTypeDeclaration(className);
newClass.IsInterface = true;
this.codeBase.Namespaces[0].Types.Add(newClass);

Création d'une structure

CodeTypeDeclaration newClass = new CodeTypeDeclaration(className);
newClass.IsStruct = true;
this.codeBase.Namespaces[0].Types.Add(newClass);

II-D - Ajouter une méthode

Maintenant que nous avons notre classe nous allons y ajouter une méthode.

CodeMemberMethod method = new CodeMemberMethod();
//Le nom de la méthode
method.Name = nameMethod;
//Le type de retour de la méthode
method.ReturnType = new CodeTypeReference("string");
//Les paramètres de la méthode : Oon ajoute un CodeParameterDeclarationExpression
//qui prend en paramètre le type et le nom du paramètre
method.Parameters.Add(new CodeParameterDeclarationExpression("string", "maString"));

Avec le code ci-dessus nous avons ajouté une méthode vide à notre classe. Nous allons maintenant voir comment ajouter du contenu à cette méthode.

II-D-1 - Ajouter un corps de fonction

Afin de rendre le code un peu plus complet, nous allons ajouter une déclaration de variable et une instanciation. Puis nous ferons appel à une de ces méthodes.

II-D-1-a - Déclaration d'un type

method.Statements.Add(new CodeVariableDeclarationStatement(typeof(string), "i"));

Nous avons donc ajouté une variable i à notre méthode de type string.
Il y a deux types de données en dotnet. Les types primitifs et les types références. Nous venons d'ajouter un type primitif grâce à CodePrimitiveExpression. Afin d'ajouter un type référence, il faut utiliser CodeTypeReference à la place de CodePrimitiveExpression.

method.Statements.Add(new CodeVariableDeclarationStatement(
new CodeTypeReference(this.codeBase.Namespaces[nameSpace].Types[c].Name), "anInstanceOfClass"));

II-D-1-b - Instancier un objet

//Déclaration de la variable
method.Statements.Add(new CodeVariableDeclarationStatement(
new CodeTypeReference(this.codeBase.Namespaces[nameSpace].Types[c].Name), "anInstanceOfClass"));
//Instanciation du type de l'objet
method.Statements.Add(new CodeAssignStatement(new CodeTypeReferenceExpression("anInstanceOfClass"),
new CodeObjectCreateExpression(new CodeTypeReference(this.codeBase.Namespaces[nameSpace].Types[c].Name))));

Afin de pouvoir utiliser l'objet que nous avons déclaré, il faut l'instancier. Cela ce fait grâce à la classe CodeAssignStatement qui représente une assignation quelconque. Ainsi, ici nous assignons un objet à sa référence, mais cela pourrait très bien être une assignation entre deux entiers cela fonctionnerait pareil.

II-D-1-c - Invoquer une méthode

method.Statements.Add(new CodeMethodInvokeExpression(new CodeTypeReferenceExpression(method.Parameters[0].Name),
"ToUpper", new CodeExpression[] { new CodePrimitiveExpression(null) }));

à travers ce code nous avons fait appel à la méthode ToUpper de la classe string. Il faut noter la petite subtilité qui consister à passer null comme argument de la fonction quand celle-ci n'en a pas.
Cela ce fait grâce CodePrimitiveExpression qui prend en argument une instance de la primitive que l'on veut.
Ainsi quand nous voulons obtenir null, il faut lui passer null, mais si l'on souhaite obtenir un int, il faut lui passer, par exemple, 10.

II-E - Conclusion

Ces quelques exemples de code sont loin de montrer toute la puissance du modèle CodeDOM. En effet ce n'est ici qu'une infime partie de ce que CodeCOM permet, car il est en réalité capable de représenter l'intégralité des méthodes de codage. Cela va de la déclaration de classes ou de variables, comme nous l'avons vu, jusqu'à la gestion des exceptions en passant par les if, for, foreach et autres.
Détailler la procédure pour chaque morceau de code serait bien trop long. Je ne peux que vous conseiller l'aide du framework (qui est bien trop souvent oubliée :-) ).
De plus voici quelques petits « trucs » que j'ai relevés pendant la rédaction de cet article :

- les classes du CodeDOM finissant par « Expression » représentent du code « fonctionnel » c'est-à-dire qui est directement acteur par exemple un appel à méthode ou l'utilisation d'une variable (mais pas la déclaration de la variable) ;

- l'aide du framework n'est pas toujours claire, le mieux est encore de faire les tests soit même ;

- si le code ne compile pas, tentez les types finissant par « Expression » si ça n'en est pas, et inversement ;

- seule la pratique vous permettra de bien comprendre comment fonctionnent les différentes classes du CodeDOM entre elles.

III - Générer le code source dans un langage spécifique

Après avoir construit votre graph d'objets, vous voulez certainement en faire quelque chose. Dans cette partie nous verrons comment faire pour générer le code source dans un langage particulier.

public void GenerateCSharpCode(CodeCompileUnit codeBase, string file)
{
this.GenerateCode(this.csharp, codeBase, file);
}

private void GenerateCode(CodeDomProvider codeDomProvider, CodeCompileUnit codeBase, string file)
{
//On définit les options de génération de code
CodeGeneratorOptions options = new CodeGeneratorOptions();
//On demande a ce que le code généré soit dans le même ordre que le code inséré
options.VerbatimOrder = true;
options.BracingStyle = "C";

IndentedTextWriter tw = new IndentedTextWriter(new StreamWriter(file, false), "\t");

//On demande la génération proprement dite
codeDomProvider.GenerateCodeFromCompileUnit(codeBase, tw, options);

tw.Close();
}

Ce code vous permet de générer le code source en C#. La propriété VerbatimOrder vous permet de spécifier que vous voulez que le code soit généré dans le même ordre que le graph d'objets. La deuxième (BracingStyle) vous permet de spécifier que vous voulez les accolades à la mode « C ». L'aide du framework vous en dira plus sur les possibilités de cette propriété.
Lors de la génération de code, il n'y a aucune vérification de conformité des instructions. Vous pouvez tout à fait avoir un graph d'objets qui vous permette de générer le code, mais qui ne compile pas.

IV - Générer un assembly à partir du graph d'objets

Le graph d'objets vous permet également de générer un assembly sans passer par le code source. Pour cela il suffit de faire :

CompilerParameters compilerParameters = new CompilerParameters();
compilerParameters.GenerateExecutable = false;
compilerParameters.GenerateInMemory = false;
compilerParameters.IncludeDebugInformation = true;
compilerParameters.OutputAssembly = file;
compilerParameters.ReferencedAssemblies.Add("System.dll");

this.compilerResults = this.csharp.CompileAssemblyFromDom(compilerParameters, codeBase);

Ce code va générer un assembly non exécutable (dll) et le résultat va être écrit dans un fichier ( GenerateInMemory = false). Les informations de débogage sont également incluses et la référence System.dll est ajoutée aux options du compilateur. La génération proprement dite se fait par l'appel de la méthode CompileAssemblyFromDom qui prend en paramètres les options de compilation et la racine du graph (le CodeDomUnit )
Pour pouvoir utiliser l'assembly fraîchement compilé vous pouvez employer les méthodes décrites dans les articles sur les applications modulaires ou sur les domaines d'application.

V - Générer un assembly à partir d'un fichier source

Ce moyen d'utiliser CodeDOM est le moins sécurisé de tous. En effet vous n'avez aucun moyen de vérifier le contenu du code et de le modifier.
Mais il peut s'avérer utile de compiler à la volée un fichier source.
Pour cela il vous faut tout simplement faire appel à la fonction CompileAssemblyFromFile.

this.csharp.CompileAssemblyFromFile(compilerParameters, file);

VI - Pourquoi utiliser CodeDOM dans mes applications ?

Assurément tout le monde n'a pas besoin d'utiliser CodeDOM dans son application. En effet bien peu de personnes doivent générer du code ou en interpréter.
Par contre dans ces deux cas l'utilisation de CodeDOM se révèlera extrêmement efficace et puissant. La génération de code par CodeDOM vous permettra d'être totalement indépendant du langage cible, ce qui est très agréable. De plus vous n'êtes pas limité aux langages fournis par Microsoft, car tous les langages fournissent leur provider pour CodeDOM, car ceci fait partie des spécifications pour la création d'un langage .NET.
Les personnes plus intéressées par l'interprétation de code, trouveront là un moyen puissant et fortement structuré de décrire les actions à effectuer et de manière indépendante d'un langage de programmation. La représentation sous forme de graph permet un parcours assez aisé et la vérification de certaines conditions se fait assez bien.
Enfin le stockage de la description du code sous forme d'un fichier Xml et là aussi tout à fait approprié de par la représentation sous forme de graph.

VII - Description de l'application exemple

Vous trouverez à la fin de cet article une petite application d'exemple d'utilisation de CodeDOM.
Cette application ne se veut pas complète et ne montre qu'une infime partie des possibilités, mais vous permet toutefois d'appréhender les concepts décrits avant.
Je vous invite à parcourir le code et à vous pencher plus particulièrement sur les classes Generator et Motor. Ces deux classes contiennent la majorité du code relatif à CodeDOM

VIII - Liens

Les sources de l'application exemple (Projet C# Visual Studio 2005)

L'article sur les domaines d'application
L'article sur les applications modulaires