CentraleSupélec LMF, UMR CNRS 9021
Département informatique Laboratoire Méthodes Formelles
Bât Breguet, 3 rue Joliot-Curie Bât 650 Ada Lovelace, Université Paris Sud
91190 Gif-sur-Yvette, France Rue Noetzlin, 91190 Gif-sur-Yvette, France
Bureau d'étude du cours de modélisation des systèmes

Table des matières

L'objectif de ce bureau d'étude est de définir un DSML (Domain Specific Modeling Language) très simple et de mettre en œuvre quelques outils de l'IDM (Ingénierie Dirigée par les Modèles).

Au cours de ce BE, vous apprendrez à :

  • définir la syntaxe concrète d'un langage en utilisant Xtext
  • utiliser le meta-modèle (syntaxe abstraite) généré par Xtext à partir de votre grammaire
  • définir la sémantique de votre langage par traduction vers un autre langage (Java), en utilisant Acceleo.

Votre DSML

Le langage de modélisation que vous allez définir doit permettre de modéliser le comportement de systèmes qui réagissent à des événements en changeant d'état. Cet état est composé de variables booléennes qui sont observables en sortie.

La syntaxe du langage devra comporter le nom du modèle, suivi du mot-clef inputs: et d'une liste de noms d'évènements, puis du mot-clef outputs: suivi d'une liste de noms de sorties et de leur valeur initiale sous la forme nom=booléen. Viendra ensuite le mot-clef rules: suivi de la liste des règles de changement d'état. Chaque règle sera composée d'une garde (le nom d'une sortie, éventuellement précédé de ! pour la négation) entre crochets carrés ([ et ]), d'un événement déclencheur, et du mot-clef -> suivi d'une liste d'actions de la forme sortie=booléen.

Exemple

Selon cette syntaxe, un modèle d'un régulateur de vitesse pourrait être :


cruise
inputs: on off cruise brake standby
outputs: on=true cruise=false
rules:
[] off -> on=false cruise=false
[!on] on -> on=true cruise=false
[on] cruise -> cruise=true
[cruise] brake -> cruise=false
[cruise] standby -> cruise=false

Création du projet Xtext Grammaire Xtext

Pour définir la grammaire de ce DSML, créez un projet Xtext dans Eclipse :

Vous pouvez laisser tous les paramètres à leur valeur par défaut, ce qui devrait créer 4 projets dans votre workspace, et ouvrir un fichier MyDsl.xtext.

Complétez ce fichier pour définir la grammaire présentée ci-dessus. Consultez la documentation de la grammaire de Xtext.

Pour simplifier, on utilisera le terminal ID pour le nom du modèle et pour les noms des événements et des sorties. Vous pouvez également éviter d'avoir à traiter les terminaux booléens en utilisant l'opérateur ?= pour mettre un attribut à true lorsque le mot-clef true est rencontré. La syntaxe pour l'initialisation et le changement des sorties pourra ainsi être :

Initialization:
	output=ID '=' isTrue?='true'
|	output=ID '=' 'false'
;

L'élément Initialization aura ainsi un attribut output égal au nom de la sortie, et un attribut isTrue qui sera vrai uniquement si le symbole terminal true est présent après le signe =.

grammar org.xtext.example.mydsl.MyDsl with org.eclipse.xtext.common.Terminals

generate myDsl "http://www.xtext.org/example/mydsl/MyDsl"

Model:
	name=ID
	'inputs:' inputs+=Input*
	'outputs:' outputs+=Initialization*
	'rules:' rules+=Rule*
;

Input:
	name=ID
;

Initialization:
	output=Output '=' (isTrue?='true' | 'false')
;

Output:
	name=ID
;

Rule:
	'[' guard=Guard? ']' input=[Input] '->' actions+=Action+
;

Guard:
	(isNot?='!')? output=[Output]
;

Action:
	output=[Output] '=' (isTrue?='true' | 'false')
;

Utilisation de l'éditeur généré par Xtext Éditeur

Une fois votre grammaire définie, sélectionnez Generate Xtext Artifacts dans le sous-menu Run As du menu contextuel. Si Eclipse vous demande de télécharger une version d'Antlr, répondez y dans la console.

Quand Xtext a terminé, vous disposez d'un analyseur syntaxique pour votre DSML, ainsi que d'un éditeur avec coloration syntaxique et complétion automatique. Il est possible d'ajouter des fonctionnalités à cet éditeur, mais pour ce bureau d'étude, nous l'utiliserons tel quel.

Vous pouvez inspecter le métamodèle qu'Xtext a construit à partir de la syntaxe concrète de votre DSML. Il se trouve dans le fichier model/generated/MyDsl.ecore du projet.

Votre éditeur n'est pas installé dans l'instance d'Eclipse que vous utilisez pour développer votre grammaire. Pour l'utiliser, il faut lancer une nouvelle instance d'Eclipse en choisissant Run As/Eclipse Application dans le menu contextuel du projet.

Dans cette nouvelle instance d'Eclipse, créez un nouveau projet (General/Project), et dans ce projet, un nouveau fichier cruise.mydsl. Répondez Yes à la demande d'ajouter la nature Xtext à votre projet.

Un fichier vide n'étant pas syntaxiquement correct, l'éditeur doit indiquer une erreur. Utilisez le raccourci Ctrl-espace pour voir les suggestions de complétion qui ont été générées automatiquement à partir de votre grammaire.

Saisissez l'exemple du régulateur de vitesse donné plus haut. L'éditeur ne doit indiquer aucune erreur. L'éditeur a analysé le code source et créé une instance du métamodèle de votre DSML. Cette instance réside en mémoire, nous allons maintenant la sérialiser en XMI pour pouvoir l'inspecter.

Sérialisation du modèle correspondant à un fichier source Sérialisation XMI

Afin d'enregistrer automatiquement le modèle décrit dans notre DSL en XMI à chaque fois que le fichier source est enregistré, il suffit de placer le code Xtend suivant dans la méthode doGenerate de la classe org.xtext.example.mydsl.generator.MyDslGenerator :

// Get the ResourceSet which contains 'resource', which is our model
var rset = resource.resourceSet;
// Build a URI for the XMI file by suppression the extension of the filename and appending the '.xmi' extension
var xmiuri = resource.URI.trimFileExtension.appendFileExtension('xmi');
// Create a resource for saving the model as XMI
var xmires = rset.createResource(xmiuri);
// Add the first element of the model (there is only one Machine per file) to this resource
xmires.contents.add(resource.contents.get(0));
// Save the resource (as XMI because of the '.xmi' extension).
xmires.save(null);

Après avoir relancé une instance d'Eclipse, votre modèle sera sérialisé en XMI à chaque fois que vous enregistrez un fichier .mydsl (il suffit de faire une modification mineure à votre fichier et de l'enregistrer pour faire apparaître le fichier XMI).

Génération de code Java DSML→Java

Pour donner une sémantique exécutable à votre DSML, vous allez maintenant générer du code Java. Ce code Java sera obtenu par une transformation Model to text, du métamodèle de votre DSML vers la syntaxe concrète de Java.

La classe Java que vous génèrerez devra implémenter l'interface Machine suivante :

import java.util.List;

public interface Machine {
	public void setGUI(GUI gui);

	public String[] getInputs();

	public String[] getOutputs();

	public void react(List<String> inputs);

}

GUI étant l'interface suivante :

import java.util.List;

public interface GUI {
	public void setOutputs(List<String> outputs);
}

La classe générée devra implémenter les méthodes de l'interface Machine, ainsi qu'une méthode main qui créera une instance de la classe SwingGUI en passant une instance de votre classe en argument à son constructeur. Le code de la classe SwingGUI, qui implémente l'interface GUI vous est fourni : SwingGUI.java.

import java.util.LinkedList;
import java.util.List;

public class Cruise implements Machine {
	private GUI myGUI;
	private boolean on_state = false;
	private boolean cruise_state = false;

	@Override
	public void setGUI(GUI gui) {
		myGUI = gui;
	}

	@Override
	public String[] getInputs() {
		String inputs[] = {
			"on",
			"off",
			"cruise",
			"brakes",
			"standby"
		};
		return inputs;
	}

	@Override
	public String[] getOutputs() {
		String outputs[] = {
			"on",
			"cruise"		
		};
		return outputs;
	}

	@Override
	public void react(List<String> inputs) {
		if (true) {
			if (inputs.contains("off")) {
				this.on_state = false;
				this.cruise_state = false;
				updateGUI();
				return;
			}
		}
		if (!on_state) {
			if (inputs.contains("on")) {
				this.on_state = true;
				this.cruise_state = false;
				updateGUI();
				return;
			}
		}
		if (on_state) {
			if (inputs.contains("cruise")) {
				this.cruise_state = true;
				updateGUI();
				return;
			}
		}
		if (cruise_state) {
			if (inputs.contains("brakes")) {
				this.cruise_state = false;
				updateGUI();
				return;
			}
		}
		if (cruise_state) {
			if (inputs.contains("standby")) {
				this.cruise_state = false;
				updateGUI();
				return;
			}
		}
	}

	private void updateGUI() {
		List<String> activeOutputs = new LinkedList<String>();

		if (this.on_state) {
			activeOutputs.add("on");
		}
		if (this.cruise_state) {
			activeOutputs.add("cruise");
		}
		this.myGUI.setOutputs(activeOutputs);
	}

	public static void main(String[] args) {
		Machine m = new Cruise();
		new SwingGUI(m);
	}
}

Transformation Acceleo Acceleo

La transformation de votre modèle en code Java est décrite en Acceleo. Créez un projet Acceleo :

Vous pouvez conserver les valeurs par défaut dans la première page du dialogue de création de projet. Sur la deuxième page, ajoutez le métamodèle de votre DSML, et cochez la case Main template.

Ouvrez le fichier org.eclipse.acceleo.module.sample.main/generate.mtl et complétez-le pour définir votre transformation.

Vous trouverez des exemples de code Acceleo sur :

Les caractères [ et ] sont spéciaux dans le code d'une transformation Acceleo. Pour les introduire dans le code généré, vous devrez utiliser ['['/] et [']'/].

Utilisez la complétion automatique (Ctrl-espace) pour découvrir les méthodes que vous pouvez appeler sur les éléments de votre métamodèle.

[comment encoding = UTF-8 /]
[module generate('http://www.xtext.org/example/mydsl/MyDsl')]

[template private generateBool(b: Boolean)]
[if (b)]true[else]false[/if]
[/template]

[template private guardCondition(guard:Guard)]
[if (guard._not)]![/if][guard.output.name/]_state
[/template]

[template private generateGuard(guard: Guard)]
[if (guard.oclIsUndefined())]true[else][guardCondition(guard)/][/if]
[/template]

[template public generateElement(aModel : Model)]
[comment @main/]
[let className:String = aModel.name.toUpperFirst()]
[file (className.concat('.java'), false, 'UTF-8')]
import java.util.LinkedList;
import java.util.List;

public class [className/] implements Machine {
	private GUI myGUI;
	[for (initialization:Initialization | aModel.outputs)]
	private boolean [initialization.output.name/]_state = [generateBool (initialization.isTrue)/];
	[/for]

	@Override
	public void setGUI(GUI gui) {
		myGUI = gui;
		updateGUI();
	}

	@Override
	public String['[]'/] getInputs() {
		String inputs['[]'/] = {
		[for (input: Input | aModel.inputs) separator(',\n')]
			"[input.name/]"[/for]

		};
		return inputs;
	}

	@Override
	public String['[]'/] getOutputs() {
		String outputs['[]'/] = {
		[for (initialization: Initialization | aModel.outputs) separator(',\n')]
			"[initialization.output.name/]"[/for]

		};
		return outputs;
	}

	@Override
	public void react(List<String> inputs) {
		[for (rule:Rule | aModel.rules)]
		if ([generateGuard(rule.guard)/]) {
			if (inputs.contains("[rule.input.name/]")) {
				[for (action: Action | rule.actions)]
				this.[action.output.name/]_state = [generateBool(action.isTrue)/];
				[/for]
				updateGUI();
				return;
			}
		}
		[/for]
	}

	private void updateGUI() {
		List<String> activeOutputs = new LinkedList<String>();

		[for (initialization:Initialization | aModel.outputs)]
		if (this.[initialization.output.name/]_state) {
			activeOutputs.add("[initialization.output.name/]");
		}
		[/for]
		this.myGUI.setOutputs(activeOutputs);
	}

	public static void main(String['[]'/] args) {
		Machine m = new [className/]();
		new SwingGUI(m);
	}
}
[/file]
[/let]
[/template]

Création du menu Acceleo Menu Acceleo

Pour pouvoir exécuter votre transformation sur un fichier source de votre DSML, nous allons maintenant créer un projet qui va ajouter un élément au menu contextuel d'Eclipse.

Faites un clic droit sur votre projet Acceleo, et choisissez New/Acceleo UI Lancher Project. Passez les deux premières pages du dialogue, et dans la dernière, indiquez le nom du générateur (par exemple MyDsl to Java), et le filtre pour le nom des fichiers auxquels la transformation s'applique (*.mydsl dans votre cas).

Vous pouvez maintenant lancer une nouvelle instance d'Eclipse comportant vos plug-ins. Dans cette instance, sélectionnez Acceleo Model to Text/Generate MyDsl to Java. Un dossier src-gen doit alors apparaître dans le projet. Ce dossier contient la classe Java que vous venez de générer.

Création du projet Java pour exécuter le code généré Exécution du code

Créez un nouveau projet Java, décochez la case Use default location, et indiquez comme dossier du projet le dossier src-gen qui vient d'être créé (il se trouve dans le dossier runtime-EclipseXtext de votre compte). Donnez un nom à votre projet, et cliquez sur Finish.

Copiez dans ce projet les fichiers :

Exécutez la classe Java avec Run As/Java Application. Vous devez voir apparaître une fenêtre similaire à celle présentée ci-dessous :

Chaque appui sur un des boutons provoque la réaction de votre modèle avec comme entrée celle correspondant au bouton. Les cases à cocher indiquent la valeur des sorties.

Si votre code ne fonctionne pas correctement, vous pouvez mettre à jour votre transformation Acceleo dans l'instance mère d'Eclipse sans avoir à quitter et relancer l'instance fille.

Autres modèles

De manière similaire à ce que vous venez de faire pour le régulateur de vitesse, créez des modèles pour un four micro-ondes et un croisement piétons/voitures, générez le code Java correspondant et vérifiez leur fonctionnement en les exécutant.

Micro-ondes

microwave
inputs: start stop open close
outputs: on=false opened=false
rules:
   ...

Piétons et voitures

traffic
inputs: pedestrian car time
outputs: ped_red=false ped_green=true
         car_red=true car_orange=false car_green=false
rules:
   ...

L'événement pedestrian correspond à l'appel piéton, car correspond à la détection d'une voiture en attente, et time permet de modéliser l'écoulement du temps (par exemple pour le passage du feu orange au feu rouge).

Vous pouvez être amenés à ajouter d'autres sorties pour gérer des états transitoires.

Pour aller plus loin

Au lieu de générer du code Java à partir de votre modèle, il est aussi possible d'interpréter le modèle directement. Cela permet un cycle de développement plus court en évitant la phase de génération et la création du projet Java nécessaire à l'exécution du code généré.

Pour pouvoir interpréter un modèle correspondant un un fichier source de votre DSL, il suffit d'ajouter une entrée au menu contextuel Run As d'Eclipse. Pour cela, nous allons ajouter une extension au projet org.xtext.example.mydsl.ui qui a été créé par Xtext quand vous avez défini la syntaxe de votre langage.

Créez un nouveau package org.xtext.example.mydsl.ui.run dans le projet, et ajoutez-y les deux fichiers MyDslLauncher.java et LaunchMyDslExecution.java.

Ouvrez le fichier plugin.xml de ce projet, et dans l'onglet Extensions, cliquez sur Add.... Sélectionnez org.eclipse.debug.ui.launchShortcuts. Renseignez les éléments suivants :

  • id*: org.xtext.example.mydsl.ui.run.myDslLauncher
  • modes*: run
  • class*: org.xtext.example.mydsl.ui.run.MyDslLauncher
  • label*: MyDsl simulation

Ajoutez à ce shortcut un contextualLaunch, auquel vous ajouterez un enablement, auquel vous ajouterez un count en indiquant + pour la propriété value*

Il ne vous reste alors qu'à créer la classe org.xtext.example.mydsl.interp.ModelExtension, soit en Java, soit en Xtend si vous voulez essayer un langage un peu plus sympathique que Java. Vous pouvez vous inspirer du code fourni précédemment pour créer l'interface graphique.