CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
Cours sur machine sous Eclipse

Contenu

Solution pour le cours du 22 décembre 2023

Introduction

L'objectif de cette séance est d'apprendre à créer un métamodèle pour un type de systèmes, et de savoir transformer les modèles conformes à ce métamodèle en d'autres modèles ou en texte (génération de code).

Faute de temps, nous ne rentrerons pas dans les détails de la création d'une syntaxe textuelle pour nos modèles (par exemple avec Xtext, ou d'une syntaxe graphique (par exemple avec Sirius). Nous nous contenterons d'utiliser les éditeurs structurés fournis par EMF (Eclipse Modeling Framework).

Cas d'étude

Le prétexte choisi pour vous faire utiliser des métamodèles et des transformations de modèles est la génération d'une page web permettant de simuler le comportement d'un automate. Pour avoir une idée de ce à quoi ressemblera le résultat, vous pouvez consulter cet exemple d'un régulateur de vitesse.

Pour parvenir à ce résultat, nous allons définir un métamodèle pour les automates, puis, de façon un peu artificielle, nous définirons un autre métamodèle des automates conçu pour rendre la génération de code plus facile. Nous définirons alors une transformation permettant de passer du métamodèle d'origine au métamodèle pour la génération de code. Enfin, nous écrirons une transformation permettant de générer une page web à partir d'un modèle pour la génération de code.

Métamodèle des automates Métamodèle I

La première étape consiste donc à définir le métamodèle des automates. Les concepts à modéliser sont :

  • StateMachine qui représente un automate et a un nom, des entrées, des sorties et des états
  • Input qui représente un événement d'entrée pour l'automate et a un nom
  • Outpout qui représente une action de l'automate et a un nom
  • State qui représente un état de l'automate et qui a un nom et des transitions
  • Transition qui représente une transition d'un état vers un état cible, déclenchée par un événement (ou pas) et qui exécute des actions avant de mettre l'automate dans son état cible.

Il faudra être particulièrement attentif aux relations de composition. En effet, chaque élément d'un modèle ne peut appartenir qu'à un unique élément, mais il faut également que le modèle résultat ait une structure d'arbre de façon à ce qu'il soit possible de naviguer de la racine du modèle vers chacun des éléments.

Ici, il est conseillé de choisir StateMachine pour racine, composée d'Inputs, d'Outputs et de States. Les States eux-mêmes sont composés de Transitions.

On cherche donc à créer un métamodèle correspondant à :

Pour cela, il faut créer un nouvel ECore Modeling Project (dans la section Eclipse Modeling Framework), donner un nom au projet (par exemple StateMachine), laisser les options par défaut (choisir uniquement Design dans les viewpoints). Dans la vue qui s'affiche, les concepts sont créés avec des Classifier > Class, les attributs avec Feature > Attribute, les références avec Relation > Reference ou Relation > Composition.

Création d'un automate Créer un automate

Pour créer un automate, on peut utiliser ''Create Dynamic Instance...' dans le menu contextuel qui apparaît quand on clique sur la classe StateMachine, comme illustré ci-dessous :

Vous pouvez enregistrer le fichier .xmi correspondant à la racine du projet StateMachine. Le modèle s'ouvre par défaut dans un éditeur qui n'est pas très agréable. Vous pouvez donc le fermer et rouvrir le fichier .xmi dans l'éditeur Sample Reflective Ecore Model Editor (menu Open With).

Il ne vous reste qu'à créer un automate en ajoutant les différents éléments au modèle.

Metamodèle pour la génération de code Métamodèle II

L'objectif final de ce projet est de générer du code pour exécuter le comportement des automates créés selon le métamodèle que nous venons de concevoir. Dans un but pédagogique, et à titre de prétexte pour utiliser une transformation M2M, nous allons passer par un modèle intermédiaire. Il est en effet assez fréquent que le métamodèle utilisé pour un DSL (Domaine Specific Language) ne soit pas très pratique pour générer du code via une transformation M2T. On fait alors une première transformation M2M vers un métamodèle plus approprié.

Nous allons donc créer un nouvel ECore Modeling Project, nommé par exemple CodeStateMachine, avec uniquement 3 concepts : StateMachine, State, et Transition. Les concepts d'input et d'output du premier métamodèle sont remplacés par des chaînes de caractères. Afin d'illustrer certaines possibilités des transformations M2M, l'état initial sera stocké dans la StateMachine et non plus comme un attribut de State. Vous devriez obtenir un métamodèle correspondant à :

Transformation vers le métamodèle de génération de code Transformation M2M

Il nous faut maintenant décrire comment passer d'un automate conforme au métamoèle StateMachine à un automate conforme au métamodèle CodeStateMachine. Il s'agit d'une transformation Model to model ou M2M, que nous allons décrire en QVT Operational. QVT Operational est une implémentation des aspects opérationnels du standard QVT (Query/View/Transformation) de l'OMG. Il existe d'autres langages de description de transformations M2M, un des plus connus étant ATL.

Nous allons donc créer un nouveau projet QVT Operational, en choisissant l'élément indiqué ci-dessous dans ''File > New > Project…' :

Vous pouvez conserver tous les réglages par défaut, sauf dans le dialogue ci-dessous où il faudra choisir de créer une transformation :

Le plus simple pour écrire la transformation est ensuite d'effacer le contenu du fichier qvto créé, et de le remplir en utilisant la complétion automatique (activée par le raccourci Ctrl-espace).

Vous devez tout d'abord déclarer les métamodèles source et cible à l'aide du mot-clef modeltype en utilisant la complétion automatique (le raccourci est Commande-espace sur Mac, Contrôle-espace sous Linux et Windows) afin que les références soient correctes dans le modèle (le copier-coller de texte ne suffit pas). Une fois la complétion automatique utilisée, vous devriez avoir des déclarations comme ci-dessous (au nom des types près) :

modeltype MACHINE "strict" uses stateMachine('http://www.example.org/stateMachine');
modeltype CODE "strict" uses codeStateMachine('http://www.example.org/codeStateMachine');

Vous déclarez ensuite la transformation, avec son modèle d'entrée et son modèle de sortie, ainsi que son point d'entrée, main :

transformation StateMachineToCode(in input : MACHINE, out output : CODE);

main() {
	input.rootObjects()[StateMachine]->map toCode();
}

La transformation est définie comme un ensemble d'applications de transformations élémentaires. Ici, le point d'entrée indique qu'il faut prendre les éléments racines (rootObjects) du modèle d'entrée, et leur appliquer la transformation toCode(). Il faudra définir cette transformation pour chacun des types d'objet auxquels nous allons l'appliquer. Voici, pour vous aider à démarrer et pour connaître les éléments syntaxiques nécessaires, la transformation de l'élément racine StateMachine. Il vous restera à définir la transformation pour les States et les Transitions :

mapping stateMachine::StateMachine::toCode() : codeStateMachine::StateMachine {
	assert(self.states->select(s | s.isInitial)->size() = 1)
		with log("A state machine should have a unique initial state");
	result.name := self.name;
	result.inputs := self.inputs->collect(i | i.name);
	result.outputs := self.outputs->collect(o | o.name);
	result.states := self.states->map toCode();
	result.initialState := self.states->select(s | s.isInitial).resolveoneIn(stateMachine::State::toCode);
}

La ligne 1 indique qu'il s'agit de la transformation toCode() qui, appliquée à une instance de StateMachine du package stateMchine, produit une instance de StateMachine du package codeStateMachine. Les lignes 2 et 3 utilisent une requête OCL pour vérifier que le nombre d'états qui sont initiaux est strictement égal à 1, sinon, la transformation est arrêtée avec un message d'erreur.

Les lignes suivantes établissent la correspondance (mapping) entre les deux types d'objet. Le mot clef result désigne l'objet produit, le mot clef self désigne l'objet source. Il est possible d'utiliser OCL pour naviguer dans le modèle et sélectionner des éléments, comme illustré avec select et collect. il est aussi possible d'appliquer des transformations en utilisant map.

Un élément important pour établir les liens entres objets dans le modèle produit est d'être capable de retrouver un objet produit par une transformation à partir d'un objet source. C'est le but des primitives resolveIn et resolveOneIn. Ainsi, la ligne 8 trouve l'état initial du modèle cible en recherchant l'état produit par la correspondance stateMachine::State::toCode à partir de l'état initial de l'automate source. On peut ici utiliser resolveoneIn puisqu'on a vérifié au début de la transformation que l'état initial est unique et que les correspondances de cette transformation de produisent qu'un objet dans le modèle cible pour chaque objet du modèle source. Une variante des opérateurs de résolution utilise le mot clef late pour indiquer que la résolution doit être faite tardivement (comme dans une deuxième passe), une fois que tous les objets cibles ont été créés. Elle vous sera utile dans la transformation des transitions (la syntaxe est late resolveoneIn(...)).

Exécution de la transformation Exécution M2M

Pour exécuter cette transformation sur notre automate, il suffit de choisir Run As > Run Configurations… dans le menu contextuel du fichier .qvto, et de créer une configuration QVT Operational Transformation puis de renseigner les modèles source et cible dans le dialogue qui s'affiche :

Vous obtiendrez un fichier .xmi contenant le modèle cible, que vous pouvez inspecter comme le modèle source dans le Sample Reflective Ecore Model Editor.

Transformation M2T (génération de code) Transformation M2T

Il nous reste à générer le code pour exécuter notre automate. Nous allons pour cela utiliser une transformation M2T (Model to text) avec l'outil Acceleo.

Cette transformation part du métamodèle CodeStateMachine et doit générer une page HTML avec du code JavaScript qui exécute le comportement de l'automate.

Nous commençons par créer un projet Acceleo avec File > New > Project… > Acceleo Model to Text > Acceleo Project. Dans le dialogue de configuration du projet, il faut indiquer le métamodèle source (pensez à cliquer sur Runtime Version pour voir les métamodèles définis dans cette instance d'Eclipse), l'élément racine auquel s'applique la transformation (menu Type), et indiquer que vous aller générer un fichier et que vous voulez un point d'entrée de transformation (Main template), ce qui donne :

Le code de la transformation se trouve dans le fichier generate.mtl du package main du projet. Le contenu initial décrit un gabarit de transformation pour un objet StateMachine, qui crée un fichier de nom aStateMachine.name. Nous allons commencer par corriger le nom du fichier généré en lui ajoutant l'extension .html :

[template public generateElement(aStateMachine : StateMachine)]
[comment @main/]
[file (aStateMachine.name.concat('.html'), false, 'UTF-8')]

[/file]
[/template]

Ensuite, il est peu pratique de mettre au point le code à générer dans l'éditeur Acceleo. Il vaut mieux écrire le code désiré pour un modèle simple, puis modifier ce code en remplaçant toutes les parties qui dépendent du modèle par des structures Acceleo. Les principales structures que vous aurez à utiliser sont :

  • la boucle for, qui permet d'itérer sur une collection :
[for (variable : Type | collection)]
  On peut utiliser la variable ainsi : [variable/]
[/for]
  • la conditionnelle et l'alternative, pour générer du texte différent selon une condition :
[if (condition)]texte produit si la condition est satisfaite[/if]
[if (condition)]texte produit si la condition est satisfaite[else]texte produit sinon[/if]

Les crochets [ faisant passer dans le contexte Acceleo du modèle, il est nécessaire de les échapper si on doit les inclure dans le texte généré. Par exemple, pour déclarer un tableau d'entier en C, on devra écrire :

int tab['['/]];

le crochet ouvrant étant échappé en le plaçant dans un chaîne de caractères Acceleo.

Exemple de code

Pour vous aider à écrire la transformation, considérons l'automate suivant, dont l'état initial est A, et qui a pour événements d'entrée a et b, et pour actions u et v :

On souhaite obtenir une page HTML avec le rendu suivant :

où le bouton vierge dans les inputs correspond à une activation de l'automate sans événement d'entrée.

Le code correspondant est :

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example</title>

<style>
  * {
    font-family: sans-serif;
    font-size: 100%;
  }

  div.inputs:before {
    display: inline-block;
    content: "Inputs: ";
    font-weight: bold;
    font-size: 110%;
    width: 5em;
  }

  div.outputs:before {
    display: inline-block;
    content: "Outputs: ";
    font-weight: bold;
    font-size: 110%;
    width: 5em;
  }

  div.states:before {
    display: inline-block;
    content: "State: ";
    font-weight: bold;
    font-size: 110%;
    width: 5em;
  }

  div.outputs {
    padding-top: 2ex;
  }

  div.outputs > div.buttons {
    font-size: 95%;
  }

  div.buttons {
    display: inline;
    border-style: solid;
    border-width: 1px;
    border-radius: 7px;
    padding: 5px;
  }

  div.buttons input[type="button"] {
    font-size: 100%;
  }

  div.states {
    padding-top: 2ex;
  }

  div.states > div.buttons {
    font-size: 95%;
  }
</style>

<script>
function react(msg) {
  var out_u = document.getElementById("out_u");
  var out_v = document.getElementById("out_v");
  out_u.checked = false;
  out_v.checked = false;
  var state_A = document.getElementById("state_A");
  var state_B = document.getElementById("state_B");
  if (state_A.checked) {
    if (msg == "a") {
      // Actions
	  out_u.checked = true;
      // Target state
      state_B.checked = true;
      return;
    }
  }
  if (state_B.checked) {
    if (msg == "b") {
      // Actions
	  out_v.checked = true;
      // Target state
      state_A.checked = true;
      return;
    }
  }
}
</script>
</head>

<body>

<div class="inputs">
  <div class="buttons">
    <input type="button" id="inp_void" value=" " onclick="react('')"/>
    <input type="button" id="inp_a" value="a" onclick="react('a')"/>
    <input type="button" id="inp_b" value="b" onclick="react('b')"/>
  </div>
</div>

<div class="outputs">
  <div class="buttons">
    <input type="checkbox" disabled id="out_u"/>u
    <input type="checkbox" disabled id="out_v"/>v
  </div>
</div>

<div class="states">
  <div class="buttons">
    <input type="radio" name="state" disabled checked id="state_A"/>A
    <input type="radio" name="state" disabled  id="state_B"/>B
  </div>
</div>

</body>
</html>

Vous pouvez recopier ce code dans la transformation Acceleo (attention aux crochets ligne 53), puis éditer les lignes qui dépendent du modèle. Par exemple, la ligne 5 devrait devenir :

<title>[aStateMachine.name/]</title>

De même, lignes 68 à 92, dans la fonction react(), les différents éléments vont apparaître dans des boucles for qui itèrent sur les entrées de l'automate, ses sorties, ses états, et pour chaque état, sur les transitions. Pour traiter le cas des transitions sans garde, vous pouvez tester si un attribut n'est pas défini avec la méthode oclIsUndefined().

Exécution M2T

Pour exécuter la transformation Acceleo, il suffit de faire un clic droit sur le fichier .mtl et de choisir Run As… > Launch Acceleo Application, puis de renseigner le modèle à transformer (Model), et le dossier cible (Target), puis de cliquer sur Run dans le dialogue qui apparaît :

Il ne vous reste plus qu'à tester vos deux transformations sur différents automates !