• Aucun résultat trouvé

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