In Bezug auf Programmiersprachen herrscht oft ein regelrechter Meinungskrieg darüber, welche Sprache die beste sei. Java, Python, PHP, C# und JavaScript sind derzeit die beliebtesten Sprachen – und es gibt noch viele mehr, die um Beliebtheit buhlen. Eines haben all diese Sprachen gemeinsam: Es sind so genannte General-Purpose-Languages (GPL). Mit ihnen lassen sich so ziemlich alle Bestandteile und Schichten eines Softwaresystems entwickeln; manche davon besser, andere schlechter.
Ein anderes Ziel verfolgen die domänenspezifischen Sprachen (Domain-Specific-Language, DSL): Sie werden für den Einsatz für eine abgegrenzte Domäne entwickelt, unterstützen diesen Zweck allerdings deutlich besser als eine GPL. SQL ist ein bekannter Vertreter einer DSL, deren Domäne bekanntlich relationale Datenbanken ist.
Die Akzeptanz beider Sprachtypen steht und fällt mit der Unterstützung in Entwicklungsumgebungen. Dabei sind für jede zu unterstützende Umgebung zahlreiche Features zu entwickeln, die stark von der API der IDE abhängen. Zu solchen Features gehören u.a. Syntax-Highlighting, Content-Assist, Referenzsuche, syntaktische und semantische Fehlermeldungen und einiges mehr.
Sogenannte interne DSLs werden mit Hilfe einer anderen GPL entwickelt und müssen somit auf die Werkzeugunterstützung dieser Sprache zurückgreifen. Diese wird dann Host-Sprache genannt. Ein prominentes Beispiel hierfür ist Gradle, das letztlich eine DSL für die Domäne Build in der Sprache Groovy (in Zukunft auch Kotlin) ist. Der Vorteil der Nutzung einer Host-Sprache kann gleichzeitig ein großer Nachteil sein, da wenig sprachspezifische Unterstützung geboten werden kann. Für externe DSLs dagegen muss die IDE-Unterstützung selbst beigesteuert werden. Dazu zählen Parser, IDE-Integration, Typsystem, syntaktische und semantische Validierung und einiges mehr.
Das klingt aufwendig, muss es aber gar nicht sein. Denn genau hier setzt das Language-Development-Framework Eclipse Xtext an. Durch Xtext lassen sich mit einem Bruchteil des Aufwands DSLs sowie vollwertige Programmiersprachen mit umfangreicher Integration in IDEs erzeugen.
Was ist eigentlich Xtext?
Die Entwicklung von Xtext kann nunmehr auf eine fast 10-jährige Historie zurückblicken. Eine erste Version wurde im Rahmen des openArchitectureWare-Projekts entwickelt und 2008 zu Eclipse als Unterprojekt des Eclipse Modeling Projects übertragen. Als solches ist Xtext Open-Source und EPL-lizensiert. Entstanden ist die Idee für Xtext damals aus der Erkenntnis, dass sich die Implementierungen von Eclipse-Editoren für unterschiedliche Sprachen ähneln. Der spezifische Anteil kann durch eine Sprachgrammatik dargestellt beschrieben werden.
Die Grammatik ist zentraler Bestandteil jedes Xtext-Projekts und beschreibt in einer erweiterten Backus-Naur-Form (EBNF), wie sich die Sprache dem Benutzer präsentiert indem sie beispielsweise Schlüsselwörter oder den Aufbau von Ausdrücken definiert. Xtext speichert die abstrakte Syntax in Form von EMF-Modellen. Aus der Grammatik erzeugt der Xtext-eigene Code-Generator die nötigen Metaklassen, Lexer, Parser und sämtliche Unterstützung, die man von IDEs für General-Purpose-Languages gewohnt ist und zu schätzen weiß.
Seitdem erfreut sich das Framework zunehmender Beliebtheit und aktiver Weiterentwicklung. Seit Eclipse 3.5 (Galileo) ist Xtext fester Bestandteil des Eclipse Simultaneous Release und steuert neben diesen jährlichen auch unterjährigen Releases bei. Die aktuelle Version 2.11 wurde im Februar 2017 veröffentlicht.
Mittlerweile hat Xtext die reine Eclipse Umgebung verlassen und ermöglicht die Nutzung von DSLs in anderen IDEs wie IntelliJ-IDEA, MS Visual Studio Code, Eclipse Che. In der aktuellen Version unterstützt Xtext das Microsoft Language Server Protocol und damit die zunehmende Anzahl an IDEs und Editoren, die dieses Protokoll nutzen.
Anwendungsfälle Xtext basierter DSLs
Mit Xtext entwickelte Sprachen sind heute in einer Reihe von Open-Source-Projekten und kommerziellen Produkten (z.B. Sigasi Studio) zu finden. Die meisten Sprachen jedoch werden im Rahmen von sehr individuellen Closed-Source-Projekten entwickelt. Dazu zählen Engineering Workbenches der Automotive-Industrie, Sprachen zur Entwicklung von Softwareprodukten in Enterprise-Projekten sowie fachliche Sprachen, z.B. zur formalen Beschreibung von Vertragsrechten. Außerdem kommen Xtext-DSLs bei der Modernisierung von Legacy Systemen zum Einsatz, wobei Informationen zum Altsystem mit speziell entwickelten Werkzeugen extrahiert und in DSL-Dateien überführt werden. Diese werden auch in anderen Anwendungsszenarien als Eingabe für individuelle Codegeneratoren verwendet. Zur Implementierung der Codegeneratoren wird meist die JVM-Sprache Xtend eingesetzt, die Bestandteil des Xtext-Projekts ist und selbst mit Xtext entwickelt wurde. Auch die Xtext Grammatik Sprache ist mit Xtext entwickelt, getreu dem Motto „eat your own dogfood“.
Einige Eclipse-Projekte, z.B. ef(x)clipse, nutzen selbst Xtext. Auch auf Open-Source-Hosting-Plattformen wie GitHub finden sich zahlreiche Xtext-Projekte. Neben diesen praktischen Anwendungen hat Xtext mittlerweile Einzug in die akademische Ausbildung erhalten und ist Gegenstand zahlreicher Vorlesungen, Bachelor- und Masterarbeiten. Auf der Xtext-Homepage ist eine Auswahl von weiteren Anwendungen und Projekten gelistet, die Xtext einsetzen.
Beispiel: Regelsprache zur Heimautomatisierung
Wir wollen nun an einem konkreten Beispiel zeigen, wie eine Sprache mit Xtext entwickelt wird. Als Beispiel soll eine Sprache zur Definition von Regeln für die Automatisierung von Smart Home Geräten dienen. Bevor wir die Sprachimplementierung beschreiben, wollen wir zunächst einen Blick darauf werfen, wie “Programme” in dieser Sprache aussehen können.
In (Listing 1) werden zunächst drei Geräte und ihre möglichen Zustände definiert: Light, Camera und Siren. Eine Regel alarm on motion soll festlegen, dass das Licht und die Aufzeichnung aktiviert werden, wenn der Bewegungsmelder der Kamera auslöst. Nach 22:00 Uhr wird zudem eine Sirene aktiviert.
Automatisierungsregel Beispiel (Abb. 1)In dem Listing sind auch schon erste Features eines Xtext Editors ersichtlich: Schlüsselwörter, Strings, Zahlenwerte und Enum-Literale werden durch Syntax Highlighting farblich hervorgehoben. Doch das ist noch lange nicht der einzige Mehrwert für die Verwendung einer DSL.
Im Folgenden wird veranschaulicht, wie eine solche Sprache mit Xtext entwickelt wird. Die hier beschriebene Sprache ist eine der Sprachen, die als Beispiel-Projekte mit dem Xtext SDK ausgeliefert wird. Die dazugehörigen Projekte werden über das Menü File > New > Example > Xtext Examples > Xtext Home Automation Example in den Workspace importiert (Abb. 2).
Es werden nun 4 Projekte im Workspace angelegt (Abb. 3):
- eclipse.xtext.example.homeautomation – Das DSL Runtime Projekt mit der Sprachdefinition und ohne UI-Abhängigkeiten
- eclipse.xtext.example.homeautomation.ide – Ein Projekt mit IDE Code, welcher für alle unterstützten IDEs gleichermaßen verwendet werden kann
- eclipse.xtext.example.homeautomation.tests – Enthält Tests für die Sprache
- eclipse.xtext.example.homeautomation.ui – Die Eclipse spezifische UI Integration
Die Xtext Projekte des Beispiels im Package Explorer. (Abb. 3)
Grammatik Definition
Das Runtime Projekt enthält die Datei RuleEngine.xtext. Diese enthält die Definition der Sprachgrammatik und damit das Herzstück einer Xtext-DSL. Die vollständige Grammatik der Regel-Sprache zeigt (Abb. 4).
Die Grammatik beinhaltet sowohl Terminal-Regeln, gekennzeichnet durch das Schlüsselwort terminal, als auch Parser-Regeln. Die erste (Parser-)Regel ist die Startregel, hier die Regel Model. In Hochkommata gestellte Wörter (blau dargestellt) sind Schlüsselwörter der Sprache.
Xtext Sprachen erlauben Einfachvererbung. Diese Vererbung wird in der Präambel der Grammatik über das Schlüsselwort
with definiert. Im Fall dieser Sprache wird von der in Xtext enthaltenen Sprache Xbase geerbt. (Abb. 4, Zeile 1) Xbase stellt
eine auf dem Java-Typsystem aufbauende Expression Language zur Verfügung. Hinzu kommt ein Compiler, welcher Xbase Code nach Java übersetzt. Im Übrigen setzt auch die Sprache Xtend auf Xbase auf.
Cross-Referenzen und Linking
Referenzen sind essentieller Bestandteil von Programmiersprachen. Xtext löst Namen auf Elemente, die an anderer Stelle definiert wurden, über ihren Namen auf und verlinkt diese zur Laufzeit. Im AST werden diese Querverweise als EReference in den Metaklassen abgebildet und im AST verlinkt. So ist es möglich, als Auslöser der Regel Rule (Abb. 5) über das Referenz-Attribut deviceState den Status eines Gerätes (Device) zu referenzieren. In der Grammatik werden Cross-Referenzen durch eckige Klammern gekennzeichnet, hier in der Zuweisung deviceState=[State|QualifiedName]. State gibt hierbei an, dass die referenzierbaren Elemente vom Typ State sind und |QualifiedName, dass der Name des Elements im Modell als qualifizierter Name dargestellt wird.
Um den Status eines Gerätes referenzieren zu können, müssen sowohl das Gerät als auch der Status innerhalb eines Geräts einen eindeutigen Namen haben. Dazu wird jeweils ein Namens-Attribut definiert. Namens-Attribute werden per Konvention mit name benannt und sind vom Typ ID. Die Terminal Rule ID entstammt wieder aus der geerbten Sprache und legt fest, dass Identifier eine alphanumerische Zeichenkette inklusive Unterstrichen ist. Die name Attribute werden zur Berechnung des qualifizierten Namens von Elementen verwendet, wobei ausgehend von einem Element die Containment Hierarchie in einer Resource nach oben gewandert wird und die Namen der Elemente zusammengesetzt werden. Xtext legt alle im Workspace erreichbaren Elemente in einem zentralen Index ab, der bei Speichern von Dateien aktualisiert wird. Diese können auch in JARs verpackte DSL Dateien auf dem Classpath liegen. In diesem Index sind Beschreibungen aller bekannten Xtext Resourcen (IResourceDescription) und Beschreibungen von exportierten Elementen (IEObjectDescription) hinterlegt. Dies wird für das sogenannte Scoping verwendet, welches vom Scope-Provider berechnet wird. Grob beschrieben berechnet der Scope-Provider die Menge möglicher Namen von Elementen für Referenzen. Xtext enthält eine sehr gute Standardimplementierung, die auch im Falle dieser Beispiel DSL vollkommen ausreicht. Scoping ist ein recht komplexes Thema, womit gerade Einsteiger häufig vor Verständnisproblemen gestellt werden, falls Anpassungen nötig sind. An dieser Stelle hilft Referenzdokumentation sowie das Buch “Implementing Domain-Specific Languages with Xtext and Xtend (Second Edition)”.
Whitespace sensitive Sprachen
Eine Besonderheit der RulesEngine DSL ist es, dass Code-Blöcke nicht wie C-ähnlichen Sprachen durch geschweifte Klammern umschlossen werden, sondern durch Einrückung wie zum Beispiel in Python markiert werden. Um eine solche Einrückung in Xtext zu realisieren, können sogenannte synthetische Token verwendet werden. Wie in der Grammatik des Beispiels gezeigt können sie mittels Terminal-Regeln der Form ‘synthetic:<terminal name>’ definiert werden.
terminal BEGIN: ‚synthetic:BEGIN‘;
terminal END: ‚synthetic:END‘;
Innerhalb der Grammatik des Beispiels werden die Xbase-Regeln XBlockExpression und XSwitchExpression überschrieben (Abb. 6), um die normalerweise durch geschweifte Klammern erfolgte Gruppierung durch Einrückung zu ersetzen.
Eine Xbase XSwitchExpression ist syntaktisch ähnlich aufgebaut wie ihr Pendant in Java, dabei allerdings um einiges mächtiger. So können beispielsweise nicht nur konstante Werte, sondern alle Arten von Objekt-Referenzen verwendet werden. Darüber hinaus ist es möglich, über sogenannte Type-Guards direkte Typ-Überprüfungen und entsprechende Konvertierungen durchzuführen. Mehr zu diesem Thema findet man in der Xtext Dokumentation. Zudem gibt es dazu im Xtext SDK ein Xbase Tutorial Example.
Dependency Injection
Xtext verwendet Google Guice als Dependency Injection Framework. Die Verwendung von Dependency Injection erfolgt durchgehend und erlaubt es, nahezu alles an Xtext für eine DSL anzupassen. Dies ermöglicht Xtext eine ungemeine Flexibilität, um auch sehr spezielle Anforderungen einer DSL umzusetzen, ohne das Framework umgehen zu müssen.
Die Konfiguration erfolgt in Form von Guice-Modulen, wobei jede “Schicht” (Framework, Runtime, UI) eigene Module beisteuert, die auch einander überlagern können. Dies ist u.a. nötig, weil der Zugriff auf Resourcen im Eclipse Workspace anders erfolgt als im Standalone-Modus, d.h. ganz ohne Equinox-Runtime. In dem Fall werden für das gleiche Interface unterschiedliche Implementierungen gebunden und abhängig von der Umgebung verwendet.
Code Generator
Auf Basis der Grammatik generiert Xtext alle zur Nutzung der Sprache benötigten Bestandteile. Diese Generierung lässt sich nach einem Rechtsklick auf die Grammatik-Datei über das Kontextmenü Run As > Generate Xtext Artifacts starten. Dieser Schritt ist nach jeder Änderung an der Grammatik-Datei erforderlich. Der Generator erzeugt Java- und Xtend-Klassen, welche in die src-gen Ordner der einzelnen Projekte ausgegeben werden und bei jedem Generatorlauf überschrieben werden. Initial erzeugt der Generator auch einige Skeletons in die src Ordner, überschreibt diese dann aber nicht mehr bei Neugenerierung. Anpassungen in diesen Dateien bleiben also erhalten. Der Xtext-Generator folgt damit dem Generation-Gap-Pattern.
Zu den daraus generierten Artefakten zählen:
- Lexer und Parser
- Ecore-Metamodell und daraus erzeugte EMF-Metaklassen
- Konfigurationen in Form von Guice-Modulen
- Serializer
- Klassen-Validierung
- plugin.xml
und einiges mehr.
Metamodell und AST
Einer der generierten Bestandteile einer Xtext-Sprache ist ihr in Ecore definiertes Metamodell. Mittels der in der Grammatik definierten Regeln und dem ebenfalls generierten Parser und Lexer werden die in der Sprache verfassten Programme auf das Metamodell abgebildet und so der AST erzeugt. Dazu besitzt jede Regel einen Rückgabetyp. Wird dieser nicht mit dem returns Schlüsselwort explizit angegeben, so leitet sich dieser aus dem Regelnamen ab.
Produktionsregeln werden auf AST-Knoten abgebildet. Enum-, Datentyp- und Terminal-Regeln werden auf Attribute des aktuellen AST-Knotens abgebildet. Enum-Regeln werden in Form von Literalen, Datentyp- sowie einfache Regeln in Form von primitiven oder einfachen Typen gespeichert. Zudem können abstrakte Regeln erstellt werden. Sie definieren einen gemeinsamen Obertyp und können selbst nicht instanziiert werden.
(Abb. 7) zeigt das aus dieser Grammatik generierte Metamodell, bestehend aus den Produktionsregeln sowie ihren Attributen.
Dass das EMF-Metamodell automatisch aus der Sprachgrammatik abgeleitet wird ist sehr praktisch und erlaubt eine rasche Evolution der Sprache. Allerdings haben die Mittel, die Struktur des Metamodells durch die Grammatik zu beeinflussen, auch Grenzen. Für fortgeschrittene Sprachen besteht daher auch die Möglichkeit, eine Sprache auf ein manuell gepflegtes Metamodell abzubilden. In einigen Anwendungsfällen wird auch bevorzugt Xcore zur Definition des Metamodells eingesetzt und die automatische Ableitung deaktiviert. Für die meisten Anwendungsfälle ist die automatische Erzeugung des Metamodells aus
der Grammatik jedoch vollkommen ausreichend und daher auch am meisten vorzufinden.
Zusätzliches Customizing der Regelsprache
Bis jetzt sind wir nur auf die Grammatik der Sprache und was Xtext daraus automatisch herleitet eingegangen. Tatsächlich kann Xtext allein aus der Grammatik schon ein ziemlich gutes Ergebnis erzeugen. Wer sich die manuellen Sourcen in den Projekten ansieht, wird feststellen, dass manche Skeletons noch etwas ausgearbeitet wurden. Es empfiehlt sich, diese Sourcen eingehender zu untersuchen.
Im Runtime Projekt befindet sich die Klasse RuleEngineFormatter. In dieser Xtend-Klasse wird konfiguriert, wie Dokumente der DSL zu formatieren sind. Hier kann zum Beispiel implementiert werden, dass vor bestimmten Regeln Zeilenumbrüche oder Einrückungen zu erfolgen haben oder, dass Schlüsselwörter durch Whitespace umgeben sein sollen. Xtend-Klassen werden übrigens automatisch in Java-Code übersetzt. Die entsprechende Java-Klasse befindet sich dann im Ordner xtend-gen.
Die Klasse RuleEngineJvmModelInferrer hat die Aufgabe, die Konzepte der DSL auf ein Java-Typsystem abzubilden. So lässt sich etwa eine Model-Instanz auf das JVM-Konzept Klasse abbilden. Oder die verschiedenen State Literale eines Device lassen sich auf ein Enum abbilden. Der Xbase-Compiler nutzt
diese Implementierung, um bei Speichern einer DSL-Datei automatisch Java-Code zu generieren. In der Klasse RuleEngineValidator sind semantische Validierungsregeln definiert. Diese werden automatisch bei der Editierung eines Modells geprüft und erzeugen bei Verletzung entsprechende Resource-Marker
für die Datei.
Im UI-Projekt sorgen der RuleEngineOutlineTreeProvider sowie der RuleEngineLabelProvider dafür, dass die Struktur, Text und Images im Outline-View verbessert werden. Im RuleEngineProposalProvider werden Anpassungen am Content-Assist für ein paar Regeln vorgenommen.
Verwendung der Sprache
Nach Abschluss der Generierung können Programme der Sprache in einer separaten Eclipse-Instanz verfasst werden. Dazu lässt sich das Projekt org.eclipse.xtext.examples.homeautomation über das Run-Menü als Eclipse-Applikation ausführen. In der gestarteten Eclipse-Instanz wird für Dateien mit der Endung .rules ein auf die jeweilige Beispielsprache ausgelegter Editor bereitgestellt. Um ihn nutzen zu können, legt man ein neues Java-Projekt an. In den Java-Build-Path Einstellungen des Projekts muss auf dem Libraries-Reiter noch die Xtend-Library ergänzt werden, da sonst Ausdrücke in den Aktionsbeschreibungen nicht ausgewertet werden können. Innerhalb dieses Projektes erstellt man dann eine Textdatei mit Namen MySmartHome.rules. Xtext erkennt nun an der Dateiendung, dass der für die Sprache spezifische Editor geöffnet werden muss.
(Abb. 8) zeigt die eingangs beschriebene Automatisierungsregeln im spezifischen DSL-Editor. Neben dem Syntax-Highlighting ist in dem Screenshot der Content-Assist mit CTRL+SPACE nach Light.O aufgerufen worden. Daher werden die beiden möglichen Zustände von Light, ON und OFF, als mögliche Vorschläge präsentiert. Außerdem wurde am Ende der Regel absichtlich ein Fehler eingebaut, der mit einem Erro- Marker und einem Eintrag im Problems View angezeigt wird.
Auf der rechten Seite ist noch der Outline-View zu sehen, welcher den Inhalt des Dokuments kompakt anzeigt und die einfache Navigation auch in großen Dokumenten erlaubt.
Im Package-Explorer auf der linken Seite ist im src-gen Ordner zu sehen, dass Xtext für die .rules Datei Java-Klassen generiert hat. Die Datei MySmartHome.java enthält eine Klasse mit main Methode, die eine einfache Zustandsmaschine mit Kommandozeilen Interpreter anbietet.
Fazit
Eclipse Xtext ist ein sehr reifes und weit verbreitetes Framework zur Erstellung von domänenspezifischen Sprachen mit vollständiger Integration in gängige IDEs. Aus der Grammatikbeschreibung einer DSL generiert Xtext den nötigen Glue-Code für die zahlreichen zu unterstützenden IDE-Features.
Xtext besticht dadurch, dass auch Anfänger im Bereich Language-Engineering in kurzer Zeit beeindruckende Ergebnisse erzielen können, aber auch gleichzeitig Experten die Möglichkeit bietet, die sehr speziellen Anforderungen komplexerer Sprachenflexibel umsetzen zu können.
Karsten Thoms arbeitet seit 2003 bei itemis. Er ist Committer im Eclipse Xtext Projekt und berät Kunden bei der Einführung und dem Entwurf von domänenspezifischen Sprachen. Sein Wissen gibt Karsten Thoms neben seinen Coaching-Aufgaben in Projekten und Beratungsaufträgen regelmäßig und gern über Konferenzvorträge, Fachartikel und Blogs weiter.
Christoph Knauf ist seit 2015 Berater bei itemis. Seine Schwerpunkte liegen in den Bereichen domänenspezifische Sprachen, Microservices und OSGi. Dabei interessiert er sich besonders für die Qualitätssicherung in agilen Projekten.
Diesen Artikel finden Sie auch in der Erstausgabe der JAVAPRO. Einfach hier kostenlos bestellen.