Analyseur syntaxique Usine de nœuds .fractal ADL .fractal ADL Fichier Parser
FIG. 6.8 – Architecture des composants d’analyse syntaxique.
l’implantation d’un parseur à partir des règles syntaxiques sous forme de LL(1).
A l’exception du parseur spécifique au langage qui dirige le chargement (i.e. le composant qui est au bout de la chaîne), tous les analyseurs syntaxiques reçoivent un AST en entrée et ont la responsabilité de fusionner l’AST obtenu par la lecture du fichier avec ce dernier. Ceci est par exemple le cas du parseur de
THINKIDL qui construit des ASTs de définition d’interfaces et qui les insère dans l’AST d’architecture
reçu en entrée (voir la figure 6.5).
6.3.3 Composants d’analyse sémantique
Les composants d’analyse sémantiques peuvent être considérés comme des filtres recevant un AST en entrée et le retournant après avoir effectué des vérifications (et éventuellement des modifications). Parmi les fonctions d’analyse sémantique de base implantées dans notre chaîne d’outils, citons la vérifi-cation de compatibilité entre les interfaces interconnectées, la vérifivérifi-cation des propriétés de surcharge ou
d’extension de définitions de composants pour le langage FRACTALADL, et la vérification de
l’établis-sement de connections pour les interfaces clientes.
Un autre type d’analyse sémantique consiste en l’enrichissement ou en la modification de l’archi-tecture de l’AST. Un exemple d’enrichissement est la mise en place d’adaptateurs de communication pour établir des liaisons entre des composants implantés dans des langages différents. Par exemple, un analyseur peut décider d’établir un pont JNI (Java Native Interface) entre deux composants implantés en C et Java respectivement. Ceci est effectué en insérant deux composants au niveau de la liaison entre ces composants. Ces composants vont respectivement jouer le rôle de stub et de skeleton. Cette modification est effectuée au niveau de l’AST. Une usine de nœuds d’AST est utilisée pour instancier de nouveaux nœuds lors de l’analyse. Nous détaillons cet exemple dans la section 7.3.1.
6.4 Traitement de l’arbre de syntaxe abstraite
Cette section présente la façon dont l’AST est traité afin de réaliser l’ensemble des tâches qui sont attendues de la chaîne d’outils. Nous commençons par introduire l’architecture générale du module de traitement de l’AST. Nous décrivons ensuite en détail les différents composants du module.
6.4.1 Architecture du module de traitement
Le rôle du module de traitement est de lire une description d’architecture exprimée sous forme d’AST est d’exécuter des opérations de traitement correspondant à la fonction supportée par le compilateur ADL. Le comportement implanté par le module de traitement s’exécute en deux temps. Le premier consiste en la génération d’un graphe de tâches modélisant l’organisation des opérations à exécuter.
Le deuxième consiste en l’exécution des opérations contenues dans le graphe de tâches dans un ordre correct. Afin de mieux comprendre la fonction implantée par le module de traitement, nous pouvons faire une analogie avec l’outil de compilation et de déploiement intitulé Ant [Ant]. Dans cette perspective, la première passe correspondrait à l’écriture des fichiers de scripts définissant les tâches à exécuter avec des règles de dépendances et la deuxième correspondrait à l’exécution des tâches par Ant.
Ordonnanceur Tâche 1 Tâche 3 Tâche 2 Tâche 4 Module de traitement Implém. Tâche 1 Graphe de tâches Graphe de tâches Liste de tâches Génération de code ... Résolution de dépendences Executi on Exe cution Moteur d'exécution AST Correct Implém. Tâche 2 Implém. Tâche x Visiteur d'AST
FIG. 6.9 – Flot d’exécution du module de traitement.
Par souci de modularité et d’extensibilité, le module de traitement est conçu comme une architecture à base de composants. La figure 6.9 présente les composants principaux de cette architecture. L’élément centrale du module de traitement est le graphe de tâches construit à partir de l’AST par le module de construction de tâches (i.e. visiteurs d’AST). Ce graphe permet de définir des dépendances unidirection-nelles entre les tâches ainsi que des flots de données qui doivent êtres échangés.
Illustrons ce modèle de graphe dans le cadre d’un générateur de code de composants Java. Considé-rons qu’un composant est implanté par une classe Java implantant ses interfaces serveurs et déclarant des champs privés pour référencer ses interfaces clientes. La figure 6.10 représente sous forme de grammaire les tâches de génération de code et leurs dépendances. Trois tâches sont identifiées : les deux dernières produisent le code pour les interface serveurs et clientes. Ces tâches prennent en entrée des données
archi-tecturales comme le nom (i.e.ClientItfName) ou le type (i.e.ServerItfType,ClientItfType)
d’une interface. Ces données se trouvent dans l’AST. Par conséquent, ces tâches n’ont aucune dépen-dance envers d’autres tâches. La première tâche est responsable de la génération de code du type de composant. Elle utilise non-seulement une information architecturale pour le type du composant (i.e.
CompType), mais également le code qui est généré par les deux tâches précédentes (dont elle dépend de
fait).
CompDefinition → public abstract classCompType
implementsServerItfs{ ClientItfs
}
ServerItfs → ServerItfType1{,ServerItfTypei}*
ClientItfs → {privateClientItfType ClientItfName;}*
FIG. 6.10 – Règles de génération de code pour l’implantation de composants Java. Les mots soulignés
re-présentent les données obtenues à partir de l’AST, alors que les mots en italique rere-présentent les données qui sont produites par l’exécution d’autres règles.
L’implantation concrète des tâches est fournie par les composants backend. Par analogie aux outils existants, les composants backends correspondent aux tâches Ant ou encore à JDT [JDT] qui implante des
6.4. Traitement de l’arbre de syntaxe abstraite
fonctions de génération de code dans l’environnement Eclipse. Ces composants permettent d’implanter divers comportement pour une tâche donnée en fonction de l’environnement cible. Par exemple, nous montrons dans le chapitre suivant qu’un même graphe de tâches peut être utilisé pour la génération de code en C, C++ et Java en modifiant les composants backends utilisés.
Enfin, le graphe de tâches est exécuté par un composant ordonnanceur. Celui-ci exécute les tâches en respectant les dépendances entre celles-ci. Par ailleurs, il assure leurs échanges de données.
6.4.2 Canevas de tâches
Cette section est dédiée à la présentation du canevas de tâches utilisé dans le module de traitement. Ce canevas est utilisé pour l’organisation de l’exécution des opérations de traitement. Sa fonction peut être comparée à des outils de compilation tel que Ant ou Make. Il définit un modèle de programmation pour décrire un graphe de tâches et implante un moteur d’exécution, appelé ordonnanceur, pour exécuter les opérations contenues dans le graphe. Le modèle de programmation fourni permet de définir des types de tâches, similairement à Ant. Or, si ce dernier permet de dénoter uniquement des dépendances, notre canevas permet aussi de définir des flots de données typés qui seront échangés entre les tâches.
Nous commençons par présenter les différents éléments qu’inclut ce canevas. Ensuite, nous présen-tons la structure des tâches et nous détaillions les facilités fournies pour la programmation du graphe de tâches.
6.4.2.1 Elements du canevas de tâches
Le canevas de tâches est constitué de trois principaux éléments :
– Les tâches sont les éléments de base du canevas. Elles sont utilisées pour modéliser une action de traitement et sont connectées à des composants backend fournissant l’implantation concrète de
l’action à réaliser. Chaque tâche implante une méthodeexecutequi permet de lancer de manière
unifiée l’action implantée par le backend associé à la tâche. En plus de cette interface, les tâches peuvent implanter des interfaces spécifiques au type d’action qu’elles modélisent. Au travers de ces interfaces spécifiques, les tâches peuvent accéder aux informations se trouvant dans l’AST et aux résultat des tâches dont elles dépendent.
– Le graphe de tâches permet de modéliser l’ensemble des tâches à réaliser par le module de trai-tement. Il s’agit d’un graphe acyclique qui permet de représenter les tâches et leurs interdépen-dances. Il fournit des opérations d’ajout et de suppression de tâches, ainsi que de spécification des interdépendances. Remarquons que le graphe vérifie après chaque modification qu’aucun cycle n’est créé. En effet, cette propriété est cruciale pour garantir que le graphe pourra être exécuté. – L’ordonnanceur est en charge d’exécuter les tâches du graphe en respectant leurs
interdépen-dances. Pour ce faire, il implante une méthode, appeléeexecute, prenant en paramètre un graphe
de tâches à exécuter. Notons qu’il est possible de réaliser diverses implantation de ce composant. 6.4.2.2 Définition de types de tâches
Chaque tâche est représentée par un objet dérivant d’une classe, appeléeTask, qui implante diverses
méthodes :
– Les méthodesaddDependancy(anotherTask,role)etremoveDependancy(anotherTask)
permettent respectivement d’ajouter et de supprimer des dépendances vers une autre tâche. Le
pa-ramètreanotherTaskidentifie la tâche vers laquelle la dépendance doit être ajoutée/supprimée,
alors que le paramètreroleest un identifiant de rôle qui permet d’associer à la dépendance un
type bien précis. Le méchanisme d’attribution de rôles peut être, par exemple, utilisé pour distin-guer les codes sources fournis pour les interfaces clientes et serveurs dans l’exemple présenté dans la section 6.4.1.
– La méthodeexecute(context)permet de lancer l’exécution de la tâche. Le paramètrecontext
permet de passer des informations sur le contexte d’exécution à la tâche.
– La méthodegetResult()retourne le résultat d’exécution de la tâche. Le résultat de retour n’est
pas typé au niveau de cette interface générique ; cependant il est possible de le caster dans le type de résultat attendu.
Pour spécialiser un type de tâches, il suffit de définir de nouvelles interfaces étendant le type de base présenté ci-dessus. Par exemple, dans le cadre de l’exemple présenté en section 6.4.1, il est nécessaire de définir deux type de tâches modélisant les actions de génération de code. La figure 6.11 présente la définition de ces types. La première interface modélise une tâche de production de code source. Cette tâche est utilisée pour produire les codes sources des interfaces clientes et serveurs. La deuxième
inter-face, appeléeSourceCodeProviderConsumerTask, modélise une tâche produisant et consommant
du code source. Ce type de tâche est celui qui est utilisé pour créer les définitions de type de composants. L’interface définit tout d’abord un rôle qui permet de modéliser les interactions entre la tâche possé-dant l’interface et les tâches dont elle va dépendre. Plus précisément, il est spécifié qu’une tâche de type
SourceCodeProviderConsumerTask peut dépendre de producteurs de codes (SourceCodeProviderTask).
L’interface spécifie également deux méthodes qui permettent de rajouter et supprimer des dépendances envers ces producteurs de code.
p u b l i c i n t e r f a c e S o u r c e C o d e P r o v i d e r T a s k e x t e n d s Task { /∗ ∗ ∗ R e t o u r n e l e code s o u r c e p r o d u i t par c e t t e t â c h e . ∗ / O b j e c t g e t S o u r c e C o d e ( ) ; } p u b l i c i n t e r f a c e S o u r c e C o d e P r o v i d e r C o n s u m e r T a s k e x t e n d s S o u r c e C o d e P r o v i d e r T a s k { /∗ ∗
∗ Le p a r a m è t r e ’ r o l e ’ e s t u t i l i s é pour i d e n t i f i e r une d épen dan ce de code s o u r c e . ∗ /
C l a s s SOURCE_CODE_PROVIDER_TASK_ROLE = S o u r c e C o d e P r o v i d e r T a s k . c l a s s ; /∗ ∗
∗ A j o u t d ’ une n o u v e l l e dép enda nce de code p r o d u c t i o n de code ∗ s o u r c e en l u i a s s o c i a n t l e nom d o n n é e p a r ’ c o d e P r o v i d e r N a m e ’ . ∗ /
void a d d S o u r c e C o d e P r o v i d e r T a s k ( S t r i n g codeProviderName , TaskMap . P l a c e H o l d e r t a s k ) ; /∗ ∗
∗ S u p p r e s s i o n de l a t â c h e i d e n t i f i é par ’ codeProviderName ’ . ∗ /
void r e m o v e S o u r c e C o d e P r o v i d e r T a s k ( S t r i n g codeProviderName ) ; }
FIG. 6.11 – Définition de types de tâches pour modéliser (1) la production de code et (2) la production
et consommation de code.
Le canevas de tâche peut donc être étendu par les concepteurs du compilateur d’ADL afin de définir des types de tâches répondant à leurs besoins. Dans le cadre de nos travaux, nous avons, entre autres, étendu ce canevas afin de mettre en place un outil de traitement spécialisé pour la génération de code et la compilation. Celui-ci est décrit en détail dans le chapitre suivant.
6.4.2.3 Déclaration de dépendances entre tâches
Comme nous l’avons expliqué dans la section 6.4.1, divers composants peuvent participer à l’éla-boration du graphe de tâches. Afin de déclarer des dépendances entre tâches, ces composants doivent
6.4. Traitement de l’arbre de syntaxe abstraite
connaître leurs identifiants. Il n’est pas possible de prendre pour identifiant de tâches les références vers les objets les représentant. En effet, un tel choix imposerait que les tâches soient crées avant de pouvoir déclarer des dépendances vers elles. Ceci n’est pas possible dès lors que le graphe peut être construit par des composants distincts.
Afin de remédier à ce problème, nous proposons un mécanisme permettant de découpler l’instancia-tion des tâches de la déclaral’instancia-tion de leurs dépendances. Ce mécanisme, appelé mécanisme d’emplacement (PlaceHolder), permet de référencer les différentes tâches du graphe sans connaître les objets qui les re-présentent. Pour ce faire, les nœuds du graphe de tâches ne sont pas des tâches, mais des “emplacements”
référençant ces tâches. Ainsi, lors qu’un composant crée une tâcheT1et veut déclarer une dépendence
entre cette dernière et une tâcheT2, il consulte le graphe de tâches pour connaître l’emplacement de
la tâcheT2. Si la tâche T2a préalablement été créée et enregistrée, l’emplacement retourné est déjà
rempli. Sinon, un emplacement vide portant le nomT2est retourné. Cette emplacement sera rempli plus
tard par un autre composant de construction du graphe de tâches. Notons que le module d’exécution du graphe ne débute son exécution que si l’ensemble des emplacements a été rempli.
6.4.3 Construction du graphe de tâches
La construction de graphes de tâches consiste en la définition des opérations à exécuter pour fournir les fonctions attendues du module de traitement. Si l’on reprend l’analogie avec les outils de compilation classiques comme Ant, il s’agit d’écrire un équivalent des fichiers de script décrivant le graphe de tâches à exécuter. Ce processus est effectué automatiquement de manière à générer le graphe de tâches à partir de l’interprétation des données architecturales contenues dans l’AST.
Dans cette section, nous décrivons la structure du module de construction qui prend en entrée l’AST produit par le module de chargement, et qui produit en sortie un graphe de tâches. Nous débutons notre présentation par la description de l’architecture du module de construction qui est basée sur le patron de programmation visiteur. Nous présentons ensuite le modèle de programmation générique employé pour implanter les visiteurs.
6.4.3.1 Architecture du module de construction du graphe de tâches
Étant donnée que le module de construction de graphe de tâches est en charge de définir les opérations de traitement implantées par un compilateur ADL donné, sa modularité et son extensibilité est un pré-requis pour la capacité à intégrer de nouvelles fonctions de traitement. Dans cet objectif, nous optons pour une architecture à base de composants à grain fin où chaque composant implante une fonction de traitement spécifique au travers le patron de programmation visiteur. Par assurer la généricité d’une telle architecture, nous définissons trois types de composants nécessaires pour le parcours et l’interprétation de l’AST.
– Les composants voyageurs implantent une interface Traveler et possèdent une interface cliente de
type Visitor. L’interface Traveler définit une méthodetravel(AST), dans laquelle le paramètre
ASTdésigne la racine de l’AST qui doit être traversé par le composant. Le composant voyageur
effectue un parcours en profondeur de l’AST et utilise son interface client de type Visitor pour déclencher un parcours de chaque nœud composant trouvé.
– Les composant expéditeurs permettent d’adapter une interface de type Visitor en n interfaces du même type. Ces composants permettent de diffuser une invocation en provenance d’un voyageur vers plusieurs composants visiteurs. Dans certains cas, les expéditeurs peuvent être utilisés pour sélectionner un composant visiteur particulier, à l’aide d’un critère de choix.
– Les composants visiteurs implantent l’interface Visitor qui est invoquée par les composants
voya-geurs. Cette interface définit une méthodevisit(component)dont le rôle est de créer des tâches
sont connectés à des composants backend qui fournissent l’implantation concrète des tâches crées. Module de traitement Visiteur ServerItf Visiteur ClientItf Visiteur CompDef Backend CompDef Backend ServerItf Backend ClientItf Voyageur d'AST
FIG. 6.12 – Architecture d’un module de traitement permettant de créer un graphe de tâches pour la
fonction de génération de code présentée sur la figure 6.10.
La figure 6.12 illustre l’utilisation des différents composants du module de construction de graphe dans le cadre de l’exemple présenté sur la figure 6.10. Le module de traitement est un composant compo-site encapsulant un composant voyageur, connecté à un expéditeur, lui même connecté à trois vicompo-siteurs. Le composant voyageur effectue un parcours en profondeur de l’AST et invoque l’expéditeur à chaque fois qu’il rencontre un nœud composant. L’expéditeur diffuse les invocations du voyageur aux trois
vi-siteurs auxquels il est connecté. Chaque visiteur implante une des trois règles de génération de code
présentée dans la figure 6.10. Lorsqu’un visiteur est invoqué, il crée une tâche adéquate et déclare les dépendances de cette tâche envers les autres tâches. Sa connexion vers un composant backend lui permet d’associer les tâches créées à une implantation concrète.
Notons que l’ordre dans lequel les visiteurs sont connectés à l’expéditeur influence l’ordre dans lequel ils sont exécutés par ce dernier. Par conséquent, l’ordre de la création des tâches et de l’enregistrement de leurs dépendances est fonction de l’organisation des connexions des visiteurs. Néanmoins, cet ordre n’a aucune incidence du fait du mécanisme d’emplacement présenté dans la section 6.4.2.3.
Enfin, notons que le seul composant dont la présence est imposée est le composant voyageur, dont le rôle est de fournir une implantation de l’interface Traveler. Il est, en revanche, possible de bâtir des organisations d’expéditeurs/visiteurs arbitrairement complexes. Dans le chapitre suivant, nous décrivons plusieurs architectures que nous avons élaborées afin de décliner l’outil de traitement d’ADL spécialisé pour la génération de code en un générateur de code multi-cibles.
6.4.3.2 Programmation des visiteurs
Dans cette section, nous illustrons la programmation des visiteurs au travers de l’exemple de la fi-gure 6.10. La fifi-gure 6.13 présente un extrait de pseudo-code du composant visiteur assurant la création
de tâches pour la définition de type de composants. La méthodevisite implanté par ce composant prend
deux arguments en paramètres : le paramètrecomponentNode donne accès au nœud de l’AST pour
lequel la méthode est invoquée ; le paramètre taskGraphdonne accès au graphe de tâches en cours
de construction. La méthode commence par créer une tâche correspondant à la définition d’un type de composant. Pour ce faire, elle récupère le type de composant dans l’AST (ligne 7). Ensuite, l’interface du composant backend est enregistrée auprès de la tâche en utilisant la connexion que le visiteur a vers ce composant. La méthode recherche ensuite les interfaces serveurs et clientes du composant, afin d’en-registrer des dépendances envers les tâches qui leurs sont associées. La ligne 13 récupère la liste des interfaces serveurs du composant. Ensuite, le graphe de tâches est consulté pour retrouver les emplace-ments des tâches associées à chacune des interfaces. Les lignes 21/22 correspondent à l’enregistrement des dépendances vers les tâches en charge des interfaces. Les mêmes opérations sont répétées pour enre-gistrer les dépendances vers le code produit pour les interfaces clientes. Une fois toutes les dépendances
6.4. Traitement de l’arbre de syntaxe abstraite
enregistrées, la tâche créée par ce visiteur est rajoutée dans le graphe de tâches (ligne 30) en lui associant un nom et la référence du nœud d’AST pour lequel elle est créée.
1 . p r o c e d u r e c o m p D e f V i s i t ( ComponentNode compNode , TaskGraph t a s k G r a p h ) { 2 . / / C r e a t i o n d ’ une t â c h e de d é f i n i t i o n de t y p e de c o m p o s a n t
3 . CompDefTask compDefTask = new CompDefTask ( ) ; 4 .
5 . / / A f f e c t a t i o n du nom de c o m p o s a n t en u t i l i s a n t l ’ i n f o r m a t i o n 6 . / / s e t r o u v a n t s u r l ’ AST
7 . compDefTask . setCompType ( compNode . g e t T y p e ( ) ) ; 8 . 9 . / / A f f e c t a t i o n de l ’ i m p l a n t a t i o n c o n c r è t e à l a t â c h e c r é é e 1 0 . / / en u t i l i s a n t l a r é f é r e n c e du b a c k e n d q u i e s t c o n n e c t é e 1 1 . compDefTask . s e t I m p l e m e n t a t i o n B a c k e n d ( m y B a c k e n d I n t e r f a c e ) ; 1 0 . 1 2 . / / T r o u v e r l e s i n t e r f a c e s s e r v e u r s du c o m p o s a n t 1 3 . S e r v I t f [ ] s e r v I t f s = compNode . g e t S e r v e r I n t e r f a c e s ( ) ; 1 4 . 1 5 . f o r e a c h ( s e r v I t f i n s e r v I t f s ) { 1 6 . / / T r o u v e r l a t â c h e a s s o c i é à c e t t e i n t e r f a c e s e r v e u r du c o m p o s a n t 1 7 . s e r v I t f T a s k P H = t a s k G r a p h . g e t T a s k P l a c e H o l d e r ( s e r v I t f . getName ( ) , 1 8 . compNode ) ; 1 9 . / / E n r e g i s t r e r une d é p e n d a n c e e n v e r s l a t â c h e t r o u v é e p o u r o b t e n i r 2 0 . / / l e c o d e s o u r c e qu ’ e l l e p r o d u i t . 2 1 . compDefTask . addDependency ( s e r v I t f T a s k P H , 2 2 . S r c C o d e P r o v i d e r ) ; 2 3 . } 2 4 . 2 5 . / / De même p o u r l ’ e n r e g i s t r e m e n t d e s d é p e n d a n c e s e n v e r s l e c o d e 2 6 . / / p r o d u i t p o u r l e s i n t e r f a c e s c l i e n t e s . 2 7 . . . . 2 8 . 2 9 . / / E n r e g i s t r e r l a t â c h e c r é é e d a n s l e g r a p h e d e s t â c h e s 3 0 . t a s k G r a p h . r e g i s t e r T a s k ( " CompDef " , 3 1 . compNode , 3 2 . compDefTask ) ; 3 3 . }
FIG. 6.13 – Implantation en pseudo-code du composant visiteur qui organise la génération de code pour
la définition du type de composant conformément à l’exemple 6.10. 6.4.4 Implantation des tâches
Dans cette section, nous décrivons la façon dont les tâches sont implantées au sein des composants