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.
CodeTypeDeclaration newClass =
new
CodeTypeDeclaration
(
className);
newClass.
IsEnum =
true
;
this
.
codeBase.
Namespaces[
0
].
Types.
Add
(
newClass);
CodeTypeDeclaration newClass =
new
CodeTypeDeclaration
(
className);
newClass.
IsInterface =
true
;
this
.
codeBase.
Namespaces[
0
].
Types.
Add
(
newClass);
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