Utilisation de CodeDOM en C#
Date de publication : 07/06/2006 , Date de mise à jour : 07/06/2006
Par
Lainé Vincent (autres articles)
Nous avons parfois besoin de compiler du code à la volée que ce soit pour faire de petit moteur de scrip 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.
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. Remerciements
IX. 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 capable de générer le code correspondant à votre demande et cela dans le langage de votre choix.
A 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, tout 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...
Toute 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.
Tout les arbres ont une racine. La racine de cet "arbre CodeDOM" est une instance de la classe CodeCompileUnit.
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.
CodeNamespace nameSpace = new CodeNamespace("MonEspaceDeNom");
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;
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'énumerations et de structures se font de la même façon.
| Création d'une énumeration |
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();
method.Name = nameMethod;
method.ReturnType = new CodeTypeReference("string");
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
method.Statements.Add(new CodeVariableDeclarationStatement(
new CodeTypeReference(this.codeBase.Namespaces[nameSpace].Types[c].Name), "anInstanceOfClass"));
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 fonctionnerais 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) })); |
A 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 loins 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 quelque petits "trucs" que j'ai relevé pendant la rédaction de cet article :
- Les classes du CodeDOM finissant par "Expression" représente du code "fonctionnel" c'est à dire qui est directement acteur comme 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 permetteras 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)
{
CodeGeneratorOptions options = new CodeGeneratorOptions();
options.VerbatimOrder = true;
options.BracingStyle = "C";
IndentedTextWriter tw = new IndentedTextWriter(new StreamWriter(file, false), "\t");
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 permez 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 permez 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ébuggage 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.
Mis il peut s'avérer utile de compiler à la volé 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'as 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êment efficace et puissant.
La génération de code par CodeDOM vous permettera 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 interessé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 part 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écrit 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. Remerciements
Je tiens à remercier toute l'équipe dotnet et plus particulièrement Ditch pour ses conseils et neguib pour la relecture.
IX. Liens


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'à 3 ans de prison et jusqu'à 300 000 E
de dommages et intérêts.
Cette page est déposée à la
SACD.