Die Effizienzmaschine

Xtext 2.3 – so generiert man Code für jeden Anwendungsfall
Kommentare

Einer der wesentlichsten Zeitfresser in größeren Softwareprojekten ist die Erstellung der Datenstrukturen sowie vergleichsweise einfache, aber mehr oder weniger aufwändig zu kodierende Standardoperationen. Gerade solcher Code kann aber prima generiert werden. Stellt die verwendete Entwicklungsumgebung für das konkrete Problem keine geeigneten Werkzeuge bereit, kann man sich diese dank Xtext auch recht schnell selbst schreiben, was mit der aktuellen Version noch einfacher geworden ist.

Erfahrungsgemäß setzt sich der Code jedes größeren Softwareprojektes aus zwei Arten von Code zusammen. Einerseits sind da spezifische Algorithmen, die die Kernfunktionalität des Produktes beinhalten. Sie sind typischerweise recht speziell und bestehen nicht selten aus in aufwändiger Arbeit hochoptimiertem Code. Der Rest des Projektes hingegen ist mehr oder weniger Standardcode; oder zumindest Code, der häufig wiederkehrende Dinge erledigt und daher idealerweise gut standardisiert und durch wiederverwendbare binäre oder Quellcodekomponenten abgedeckt ist. Moderne IDEs stellen für die Anwendungsfälle aus dieser zweiten Gruppe von Haus aus vielerlei Handreichungen, Tools und Assistenten bereit, mit deren Hilfe ein Teil des Quellcodes generiert wird. Einige praktische und täglich benutzte Beispiele hierfür sind die UI-Gestaltung mit grafischen Tools wie Formulareditoren, die dann Ressourcendateien, XAML und Quellcode produzieren. Ein anderes typisches Beispiel sind Codegeneratoren, die Modul-Kontrakte in Form von Typbibliotheken, WSDL oder ähnlichen Metadaten importieren und dem Entwickler in Form von generiertem Quellcode zur gefälligen Verwendung bereitstellen.

Trotz des wirklich beeindruckenden Leistungsumfanges moderner IDEs ist es gelegentlich wünschenswert, derartige Tools auch für eigene, spezielle Anwendungsfälle einzusetzen, die von der IDE entweder gar nicht oder nur mit in den hochpreisigeren Editionen verfügbaren Mitteln wie etwa UML-Modellierung unterstützt werden. Gar nicht so selten erweist sich die Bedienung dieser teuren Tools für den Gelegenheitsnutzer als recht umständlich, oder die produzierten Artefakte entsprechen aus dem einen oder anderen Grunde nicht so ganz den hauseigenen Coding-Standards. An dieser Stelle glänzen Tools wie Xtext, mit denen sich mit vergleichsweise überschaubarem Aufwand auch als absoluter Neuling bereits nach kurzer Einarbeitungszeit beeindruckende Ergebnisse produzieren lassen. Xtext ist ein Werkzeug, mit dem sich vergleichsweise effizient eigene domänenspezifische Sprachen (DSL) entwickeln und nutzen lassen. Das Xtext-System, zu dem auch Xtend, Xpand und einige weitere Komponenten gehören, wurde ursprünglich als openArchitectureWare entwickelt und ist seit Eclipse Galileo ein Bestandteil des Eclipse-Modeling-Projektes. Primär wird Xtext als „ein anderer Weg, Java-Applikationen zu schreiben“ positioniert, tatsächlich lassen sich aber mit Xtext praktisch alle textbasierten Ausgabeformate erzeugen, was es als Alternative zu Microsofts T4 für alle Entwickler interessant macht.

Um in den Genuss der aktuellen Version zu kommen, laden Sie zunächst das in unter „Download Eclipse and Install Xtext“ benannte Paket herunter und entpacken es in ein Verzeichnis Ihrer Wahl. Da Eclipse auf Java basiert, sollte außerdem eine aktuelle Java Runtime installiert sein. Sind diese Vorbereitungen abgeschlossen, lässt sich die Eclipse-Umgebung normalerweise ohne weitere Installations- oder Konfigurationsarbeiten problemlos starten. Folgen Sie nun den in besagtem Blogposting beschriebenen Schritten zur Installation des Xtext SDK. Dieser Vorgang, der je nach Internetanbindung durchaus einige Minuten dauern kann, schließt die Einrichtung von Xtext auf dem System ab. Während die Installation läuft, sollten Sie vielleicht die Gelegenheit nutzen und gleich ein wenig weiter in Sven Efftinges Blog stöbern, in dem viele gute Beispiele und Use Cases zu finden sind.

Grammatik definiert Sprache

Um ein neues Xtext-Projekt zu erstellen, klickt man wie gewohnt NEW | PROJECT an. Im daraufhin erscheinenden Dialog wählt man „Xtext project“ aus. Als Nächstes wird man aufgefordert, ein paar grundlegende Eckdaten zu seinem Projekt einzugeben. Füllen Sie den Dialog wie in Abbildung 1 zu sehen aus, um die Namen für Projekt und DSL entsprechend dem üblichen Schema anzugeben. Ebenfalls von Bedeutung ist die Vorgabe einer geeigneten Dateierweiterung. Das ist zwar auch für den Entwickler selbst sehr sinnvoll, vor allem aber erkennt Eclipse daran später, dass es sich um eine unserer DSLs handelt, sodass die Editor-Umgebung von Eclipse in geeigneter Weise eingerichtet werden kann. Wurde auch dieser Schritt erfolgreich abgeschlossen, begrüßt uns Xtext statt mit einem leeren Fenster mit einer einfachen DSL-Vorlage als Muster. Für unser Beispiel löschen wir alles ab der Deklaration von „Model:“ weg und ersetzen es durch den entsprechenden Code aus Listing 1. Die beiden oberen Zeilen lassen wir fürs erste unberührt so stehen, wie sie ursprünglich erstellt wurden. Mit der grammar-Zeile wird im Grunde die Basis festgelegt, auf der unsere neue DSL aufbaut. Für einfache Fälle ist der Vorgabewert normalerweise vollkommen ausreichend. Dieser Mechanismus kann dazu verwendet werden, bestehende Grammatiken wiederzuverwenden, was uns aber im Rahmen dieses Artikels nicht weiter beschäftigen wird.

Abb. 1: Anlage eines neuen Xtext-Projekts

grammar org.example.tests.first.FirstDSL with org.eclipse.xtext.common.Terminals
generate firstDSL "http://www.example.org/tests/first/ForstDSL"

Model:
'root' root= [Element]
'caption' caption= STRING
elements+=Element*;

Element:
  "element" name=ID
  ("display" (allowed+=[Element] 
  ("contains" (allowed+=[Element] ("," allowed+=[Element]*))?
  ("properties" props*=Eigenschaft+)?;

Eigenschaft:
  name=ID
  typ=Datentyp
  (
    ("calculated" "{" calculation=Berechnung) "}" |
    ("select one from" options+=STRING ("," options+=STRING)*)
  )?;

Berechnung:
  {Berechnung}
  ('var' (variablen+=STRING)+)?
  'begin' (zeilen+=STRING)+ 'end'
;
enum Datentyp:
  BOOL="boolean" | STR="string" | INT="integer";

Zu Beginn empfiehlt es sich natürlich, sich zunächst über das Ziel klar zu werden, das mit der DSL erreicht werden soll. Nehmen wir an, wir möchten mehrere Anwendungen nach dem in Abbildung 2 ersichtlichen Grundschema erzeugen. Die Anwendungen dienen dazu, ganz bestimmte, aber immer wieder unterschiedliche hierarchische Sachverhalte abzubilden. Dem Anwender soll es ermöglicht werden, die Objektstruktur auf der linken Seite in gewissen vorgegebenen Grenzen frei zu editieren. Gemeint ist damit zweierlei: Erstens soll die Anwendung die unterhalb jedes Eltern-Elementtyps einfügbaren Kind-Elementtypen vorgeben, sodass nicht jeder Knoten jeden beliebigen Kind-Knoten aufnehmen kann. Beispielsweise ergibt es sicherlich wenig Sinn, wenn man ein Element vom Typ See unterhalb eines anderen Sees oder etwa einer Person platzieren könnte. Zweitens haben die einzelnen Klassen natürlich ganz spezifische, aber fest vorgegebene Eigenschaften, die man auf der rechten Seite einsehen und bearbeiten können soll. Für diese Eigenschaften sollen textuelle, numerische und logische Datentypen unterschieden werden. Außerdem soll es möglich sein, einzelne Eigenschaften über etwas Code berechnen zu lassen. Als Letztes wünschen wir uns noch die Möglichkeit, einzelne Eigenschaften über eine Auswahlliste mit fest vordefinierten Optionen einschränken zu können.

Abb. 2: Der generierte Code im Einsatz

Unser erster Entwurf könnte also etwa wie in Abbildung 3 aussehen. Aus diesem Entwurf oder einem analogen E/R-Diagramm können wir nun direkt die Grammatik der DSL im Listing 1 ableiten. Als grundlegende Datentypen bringt die eingebundene Standardgrammatik bereits STRING, INT und einige andere Typen mit, die man direkt verwenden kann. Andere Datentypen lassen sich natürlich ebenfalls definieren. Wie man sieht, finden sich alle Elemente und Verbindungen exakt so auch in der DSL wieder, wobei folgende Regeln gelten:

eigenschaft = Datentyp
liste += Datentyp

Abb. 3: Struktur der DSL-Grammatik

Eine Besonderheit stellen boolesche Eigenschaften dar, die wie folgt über Schlüsselworte angegeben werden können: boolesche_eigenschaft ?= „schlüsselwort“. Für die Angabe der Kardinalitäten (oder Multiplizitäten) gilt folgende Syntax:

pflicht_eigenschaft = Datentyp
optionale_eigenschaft = Datentyp?

wobei optionale_eigenschaft einer 0…1-Verbindung entspricht. Für Listen gilt analog:

liste_0_zu_N += Datentyp*
liste_1_zu_N += Datentyp+

Eine Definition eines eigenen Datentyps wird durch den Bezeichner, gefolgt von einem Doppelpunkt, eingeleitet und durch ein Semikolon abgeschlossen:

Model:
  "root" root=[Element]
  "caption" caption=STRING
  elements+=Element*;

Um einen Aufzählungstyp zu definieren, verwendet man folgende, leicht abweichende, Syntax:

enum Datentyp:
  BOOL | STR | INT;

Wobei sich auch alternative Zeichenketten angeben lassen, mit denen die Eingabe im DSL-Editor bessere oder einfach nur gewohnte Benennungen angepasst werden kann:

enum Datentyp:
  BOOL="boolean" | STR="string" | INT="integer";

Alle hier in Form von Zeichenketten eingefügten Elemente werden später im DSL-Editor als Schlüsselwörter verwendet. Das Model könnte also im DSL-Dokument wie folgt verwendet werden:

root Welt
caption "Hallo DSL-Welt"

gefolgt von den Elementedefinitionen. Dem aufmerksamen Betrachter ist wahrscheinlich bereits aufgefallen, dass wir in obiger Definition von Model zwei leicht unterschiedliche Verweise auf den Datentyp Element verwendet haben. Hinter dem Schlüsselwort root steht [Element] in eckigen Klammern, während es bei der elements-Liste ohne diese Klammern auftaucht. In Xtext stehen eckige Klammern für Referenzen auf definierte Elemente, sodass der im Beispiel angegebene Datentyp Welt in der root-Zeile noch nicht vollständig definiert sein muss, sondern eben erst später. Xtext ist allerdings intelligent genug zu erkennen, ob dieser Datentyp überhaupt irgendwo in unserem DSL-Dokument definiert wurde und wird sich andernfalls prompt darüber beschweren.
Eine spezielle Bedeutung hat die eingebaute Datentyp-ID. Im Prinzip ist das zunächst nur eine ganz normale Zeichenkette mit programmiersprachenüblichen Einschränkungen für einen Bezeichner, mit denen später im DSL-Editor konkrete Elemente der definierten Sprache benannt werden. Nun kennt Xtext aber die Bedeutung dieses Datentyps und kann beispielsweise Referenzen innerhalb des DSL-Editors korrekt auflösen. Es empfiehlt sich daher immer, jedem Datentyp ein Element mit dem Typ ID zu geben, anhand dessen jede Instanz dieses Typs eindeutig identifiziert und referenziert werden kann

Aufmacherbild: Efficiency von Shutterstock / Urheberrecht: docstockmedia

[ header = Seite 2: Der Editor für die eigene DSL ]

Der Editor für die eigene DSL

Nachdem wir also die Grammatik unserer DSL entworfen und alle gemeldeten Fehler beseitigt haben, brennen wir natürlich darauf, endlich den Editor auszuprobieren und unser erstes DSL-Dokument zu erstellen. Hierzu klicken wir mit der rechten Maustaste in den Editor mit der *.xtext-Datei und wählen im Kontextmenü den Eintrag RUN AS | GENERATE XTEXT ARTIFACTS. Haben wir alles richtig gemacht, wird Eclipse nun ein wenig Aktivität entfalten und im Log-Fenster allerlei Meldungen protokollieren. Solange diese nicht rot sind und die letzte Meldung ein lakonisches „Done.“ ist, ist alles im grünen Bereich.

Andernfalls stört Xtext oder Java irgendetwas an dem im Hintergrund generierten Java-Code, was in bestimmten Fällen auch ohne zuvor gemeldeten Fehler passieren kann. Ein schicker Stolperstein ist beispielsweise das Vorhandensein einer Regel für ein Element namens „class“ in seiner Grammatik, was dazu führt, dass der im Hintergrund von Xtext generierte Java-Code für die Umsetzung der DSL-Grammatik fehlerhaft wird. In jedem Fall hilft es nur weiter, die ausgegebene Fehlermeldung zu verstehen, gegebenenfalls danach zu googeln und vor allem die zuletzt getätigten Änderungen nochmals kritisch Revue passieren zu lassen. Überhaupt ist es eine gute Strategie, größere Umbauten an Grammatik oder Codegenerator in kleinere Schritte zu zerlegen und zwischendurch immer mal wieder zu prüfen, ob auch noch alles funktioniert. Und natürlich sollte man unbedingt eine Quellcodeverwaltung nutzen, um notfalls problemlos auf einen früheren Stand zurückwechseln zu können.

Hat soweit also alles geklappt, klicken wir mit der rechten Maustaste im Package-Explorer-Baum der Eclipse Workbench auf den obersten Knoten und wählen im Kontextmenü den Punkt RUN AS | ECLIPSE APPLICATION. Daraufhin sollte eine neue Eclipse-Instanz gestartet werden, die unseren DSL-Editor als Plug-in enthält. Um ein neues DSL-Dokument zu erstellen, wählen wir nun zunächst FILE | NEW | PROJECT und dort dann GENERAL/PROJECT. Anschließend klicken wir erneut auf FILE | NEW, wählen diesmal allerdings FILE und geben im folgenden Dialog (Abb. 4) der Datei einen beliebigen Namen, welcher aber unbedingt mit der von uns eingangs festgelegten Dateierweiterung versehen sein muss. Daraufhin sollte Eclipse nach Klick auf den OK-Button die in Abbildung 4 zu sehende Abfrage präsentieren, ob dem Projekt die Xtext-Nature hinzugefügt werden soll, was wir natürlich bestätigen.

Abb. 4: Erstellen einer neuen DSL-Datei

Eclipse wird uns nun mit einem leeren Editorfenster begrüßen. Um zu testen, ob unsere Sprache so funktioniert wie wir es geplant haben, spielen wir ein wenig mit dem Editor herum und stellen fest, dass sowohl Syntaxeinfärbung als auch Grammatik und Code-Completion mit STRG+LEERTASTE funktionieren, wie man es von einem ordentlichen Editor gewohnt ist. Wir könnten nun beispielsweise den Code aus Listing 2 eintippen, der komplette Code aller relevanten Dateien befindet sich aber auch wie üblich im Downloadpaket zu diesem Artikel, sodass alle Beispiele auch ohne viel Tipparbeit einfach nachvollziehbar sind. Wie man in Listing 2 auch sieht, wurde für die Berechnungsroutinen der calculated-Eigenschaften einfach als Datentyp STRING festgelegt, was uns ermöglicht, quasi beliebigen Code einzufügen, ohne zuvor die komplette Grammatik von Pascal definieren zu müssen. Allerdings kann Xtext dann natürlich den so eingegebenen Code auch nicht mehr validieren, sodass wir selbst dafür Sorge tragen müssen, dass er später syntaktisch fehlerfrei zum Rest des generierten Codes passt. Das gezeigte Verfahren ist also keinesfalls eine hundertprozent wasserdichte Lösung, dafür aber umso schneller implementiert und zumindest für kurze Codepassagen unter Umständen auch vollkommen ausreichend.

root Welt
caption "Hallo, DSL-Welt!"
element Welt
  contains
    Kontinent, Meer

element Meer
  contains Fisch
    properties
      temperatur integer
      maxtiefe integer

element Kontinent
  contains Staat, Fluss, See
  properties
    einwohner integer calculated {
      var "child" : TElementBase;"
      begin
        "result :=0;"
        "for child in Childs do begin"
        "  if child is TStaat then Inc( result, TStaat(child).einwohner);"
        "end;"
      end
    }

element Staat
  contains Ort, Fluss, See
  properties
    Einwohner integer

element Ort
  contains Ort, Fluss, See
  properties
    Stadt boolean

element See
  contains Fisch

Falls wir an dieser Stelle einen Fehler in unserer Grammatik bemerken, können wir jederzeit in die erste Eclipse-Instanz zurückwechseln und den Fehler ausbessern. Nachdem wir wieder den „Generate-Xtext-Artifacts“-Workflow ausgeführt haben, switchen wir zur zweiten Instanz mit dem geöffneten DSL-Editor und wählen FILE | RESTART, um Änderungen an Grammatik oder Codegenerator auch tatsächlich wirksam werden zu lassen.

Es werde Code

Wie Sie vielleicht schon ahnen, passiert hinter den Kulissen eine ganze Menge mit unserer definierten Grammatik. Es wird nicht nur ein passendes Plug-in für Eclipse gebaut, auch das DSL-Dokument wird geparst und ein entsprechender Syntaxbaum, das EMF-Ecore-Meta-Model, daraus erzeugt. Selbst die ausgeklügeltste Sprachdefinition und das tollste Modell nützen aber nur dann wirklich etwas, wenn man damit auch noch irgendetwas Sinnstiftendes anstellen kann. Im Falle von Xtext heißt dies natürlich ganz klar: Code generieren!

Hierzu öffnen wir als Erstes die beim Projektstart von Xtext automatisch angelegte Vorlage für den Xtend-Codegenerator, der sich im Package Explorer im DSL-Ast unterhalb des /src/-Ordners befindet (Abb. 5). Damit überhaupt Dateien erstellt werden, überschreiben wir also zuerst die Methode doGenerate() wie in Listing 3 zu sehen. Dieser Code bewirkt, dass Xtend durch das geparste Model iteriert und für jedes der auf oberster Ebene definierten Elemente, oder präziser alle Einträge der elements-Kollektion aus dem Model-Datentyp, eine eigene Delphi-Unit erstellt. Der Inhalt jeder Unit kommt aus einer weiteren Methode namens renderElementUnit(), die wir uns gleich näher ansehen werden. Zusätzlich wird noch eine Metadaten-Unit angelegt, die hauptsächlich die als root-Element markierte Klasse und den ebenfalls in der DSL definierten Titel für die Anwendung enthält.

Abb. 5: Das vom Wizard erstellte Xtend-Generator-Skelett

/*
 *generated by Xtext
 */
package org.example.tests.first.generator
import org.eclipse.emf.ecore.resource.Resource

class FirstDSLGenerator implements IGenerator {
  override void doGenerate(Resource resource, IFileSystemAccess fsa) {
    var Model model = resource.contents.head as Model;
    // eine Unit pro Element
    for( elm : model.elements) {
      var String elmname = elm.name.toFirstUpper();
      var String unitname = "Elements."+elmname;
      var CharSequence content = renderElementUnit( elm, unitname);
      fsa.generateFile( unitname*".pas", content);
    }

    // Metadaten in separate Unit
    var String unitname = "MetaDaten";
    var CharSequence content = renderMetadataUnit( model, unitname);
    fsa.generateFile( unitname+ ".pas", content);
  }

  def String pascalEncodeString( String aSTr) {
    return aStr.replaceAll("'","'");
  }

  def String pascalType( Datentyp typ) {
    switch( typ) {
      case Datentyp::BOOL: {return "Boolean"; }
      case Datentyp::INT:  {return "Integer"; }
      case Datentyp::STR: {return "String"; }
    }
    return "Unbekannter Datentyp in pascalType()";
  }

Der Name jeder Unit wird aus dem Präfix „Elements.“ und dem Namen des Elementes gebildet, sodass aus dem DSL-Element „Kontinent“ schließlich die Unit Elements.Kontinent.pas wird, die ihrerseits eine Klasse TKontinent enthält. Die Routine renderElementUnit() des Generators bekommt das jeweilige geparste Element und den Namen der Unit als Parameter übergeben. Die Syntax der Routine weicht vom üblichen Schema insofern ab, als der Rumpf, anstatt mit den üblichen geschweiften Klammern, mittels dreier Hochkommas an Beginn und Ende eingerahmt ist. Diese Syntax ist für Methoden üblich, die generierten Text zurückliefern. Der Editor bietet dafür auch eine besser geeignete Form der Syntaxeinfärbung an als die bei normalen Methoden verwendete Form (Listing 4).

//--- Metadaten-Unit ------------------------

def renderMetadataUnit( Model model, String unitname) '''
unit «unitname»;

// Generated content. Do not modify.

interface

uses
  Classes, Windows, SysUtils, Math, Generics.Collections,
  Elements;

type
  «var String elmname = elm.name.toFirstUpper()»
  T«elmname» 0 class( TElementBase)
  «IF ! elm.props.empty»
  strict private
    «FOR prop : elm.props»
    «IF null == prop.calculation»
    «var String propname = prop.name.toFirstUpper()»
    F«propname» : «pascalType(prop.typ)»;
    «ENDIF»
    «ENDFOR»

  «ENDIF»
  public
    class procedure AllowedElementTypes( const classes : TList< TElementBaseClass>); override;
    «IF (null !=elm.display)»
    class function GetClassDisplayName : string; override;
    «ENDIF»

    «IF ! elm.props.empty»
    «FOR prop : elm.props»
    «var String propname = prop.name.toFirstUpper()»
    «IF null == prop.calculation»
    property «propname>> : «pascalType(prop.typ)» read F«propname» write F «propname»;
    «ELSE»
    function «propname» : «pascalType(prop.typ)»; // calculated property
    «ENDIF»
    «ENDFOR»
    procedure GetPropertiesAndValues( const aStrings : TStrings); override
    function IsEditable( const aPropKey : string) : TElementEditing; override;
    function IsSelectable( const aPropKey : string; const Values : TStrings) : Boolean; override;
    procedure SetPropertyValue( const aPropKey : string; const TextValue : string); override;
    «ENDIF»
  end;

implementation

Wie man sieht, enthält die Routine das Skelett des zu generierenden Quelltextes, in welches die Steueranweisungen mehr oder weniger eingebettet sind. Diese Steueranweisungen sind vom umgebenden Text durch so genante Guillemets getrennt, die man innerhalb Eclipse über die leicht zu merkenden Tastenkombinationen STRG+< und STRG+> eingeben kann. Die für die Steuersequenzen verwendete Syntax enthält nur wenige Kontrollstrukturen und ist äußerst intuitiv zu handhaben. Um einen Wert in den Text zu generieren, schreibt man einfach die entsprechende Variable innerhalb der Guillemets. Für die Kontrollstrukturen if und for gilt Ähnliches, so werden für Bedingungen eben einfach die Java-üblichen logischen Operatoren verwendet. Fast noch leichter wird es mit for-Schleifen, hier erkennt das System automatisch den Typ des Elementes, sodass man mit der folgenden Schreibweise ohne langes Nachdenken eine vollwertige Iterationsanweisung formulieren kann:

«FOR element : liste»
  beliebiger Text oder Code
«ENDFOR»

In der aktuellen Version von Xtext hat man das in früheren Versionen schon gute Xtend-System nochmals kräftig poliert. Besonders bezüglich Einrückungen und gelegentlicher überzähliger Leerzeilen, die noch unter Eclipse Helios einige abenteuerliche Klimmzüge im Xtend-Template notwendig machten, kann man wirklich von einer signifikanten Verbesserung sprechen. Auch die Syntax der Xtend-Template-Sprache selbst ist wesentlich eingängiger und anwenderfreundlicher geworden und fühlt sich auch und gerade durch die besseren Möglichkeiten, Subroutinen einfach und unkompliziert erstellen und aufrufen zu können, fast nativ an.

[ header = Seite 3: Einbindung der generierten Dateien in die Rahmenanwendung ]

Einbindung der generierten Dateien in die Rahmenanwendung

Ist der Generator allem Anschein nach vollständig und soweit fehlerfrei implementiert, wechseln wir erneut auf den Tab mit der DSL-Grammatik und wählen im Kontextmenü wieder „Generate Xtext Artifacts“. Wie schon zuvor starten wir sodann den DSL-Editor wieder beziehungsweise starten diesen neu, um die Änderungen am Codegenerator zu übernehmen. Für die Codegenerierung ist es nicht nötig, irgendeinen weiteren Befehl anzuklicken, es passiert völlig automatisch. Nach jeder Änderung an unserem DSL-Dokument, die zu einem validen und somit von Xtext verarbeitbarem Stand führen, werden selbige ohne weiteres Zutun automatisch im generierten Code nachgeführt. Hat man nur Änderungen an der Grammatik oder am Generator gemacht, die sich auf das Dokument nicht auswirken, löscht man nach dem allfälligen Restart des Editors entweder alle generierten Dateien manuell oder nimmt irgendeine belanglose Änderung im Editor vor, um den Generierungsprozess anzustoßen. Das Ergebnis sollte dann in jedem Falle ungefähr so wie im Listing 5 aussehen.

unit Elements.Kontinent;
// Generated content. Do not modify. 

interface

uses
  Classes, Windows, SysUtils, Math, Generics.Collections;
  Elements;

Type
  TKontinent = class( TElementBase)
  strict private

  public
    class procedure AllowedElementTypes ( const classes : TList< TElementBaseClass>); override;

    function Einweohner : Integer // calculated property
    procedure GetPropertiesAndValues ( const aStrings : TStrings); override;
    function IsEditable ( const aPropKey : string) : TElementEditing; override
    function IsSelectable ( const aPropKey : string; const Values : TStrings) : Boolean; override;
    procedure SetPropertyValue ( const aPropKey : string; const TextValue : string); override;
   end;

implementation

Wie in Abbildung 6 zu erkennen ist, werden die vom Codegenerator produzierten Artefakte in einem automatisch erstellten Verzeichnis /src-gen/ in demselben Ordner erstellt, in dem auch unser DSL-Dokument abgespeichert ist. In der Verzeichnisstruktur unserer Beispielanwendung geben wir diesen Ordner als Suchpfad an, sodass der Compiler alle benötigten Units finden kann.

Abb. 6: Die generierten Dateien erscheinen unter src-gen

Der restliche und ganz normal handgeschriebene Quellcode der Rahmenanwendung besteht neben den üblichen Projektdateien lediglich aus einem einzigen Formular, welches den von uns gewünschten Aufbau und die Funktionalität der Benutzeroberfläche implementiert. Der Code im Formular hat außer der Uses-Klausel, die die Metadaten-Unit einbindet, keinerlei Bezug auf konkrete Elemente des generierten Codes und lässt sich daher sofort ohne weitere Änderungen mit einer anderen Ausprägung der DSL wiederverwenden. Alle variablen Teile der Anwendung, also konkret die Caption des Formulars, der Application.Title und der Datentyp des Wurzelknotens, kommen über namentlich bekannte Konstanten der Metadaten. Da jede generierte Unit auch immer alle benötigten Subklassen im Implementationsteil einbindet, werden auch alle in der DSL verwendeten Elementeklassen sicher und vollautomatisch eingebunden. Alle Elementetypen werden von einer gemeinsamen Basisklasse TElementBase abgeleitet, die der Formularcode kennt. Außerdem können an jeder Stelle des Programms alle bekannten Klassen über einen Registriermechanismus, die ElementeRegistry in der Unit Elements.pas, problemlos abgefragt werden. Sicherlich könnte man nun auch noch die Projektdateien und das Icon der Anwendung entsprechend einbinden, ein eigenes Icon im Tree für jede Klasse wäre auch recht nett. Diese Erweiterungen seien zwecks Lernerfahrung dem geneigten Leser überlassen.

Troubleshooting

Der Eclipse-Editor und die Xtext-Plug-ins erweisen sich in der Praxis als eine zweifellos sehr hilfreiche und produktive Umgebung für den Umgang mit selbsterstellten domänenspezifischen Sprachen. Auch die offerierten Fehlermeldungen und Quickfixes (Abb. 7) sind meist zutreffend und der Situation angemessen. Dennoch kommt es gelegentlich zu Komplikationen, denen ein unerfahrener Anwender manchmal etwas hilflos gegenübersteht. Ein häufiger Fehler bei Umbauten ist eine fehlerhafte Verschachtelung von Kontrollstrukturen, zum Beispiel durch ein versehentlich weggelöschtes oder vergessenes ENDIF. Dies wirkt sich unter Umständen aber erst an ganz anderer Stelle im Generator aus, wo dann scheinbar falsche Fehlermeldungen und völlig absurde Quickfixes angeboten werden. In seltenen Fällen hat sich dann lediglich der Editor verhaspelt, ein Neustart der gesamten Eclipse-Umgebung behebt dieses Problem schnell und zuverlässig.

Abb. 7: Quickfix, um einen verwendeten DSL-Datentyp zu importieren

Bleibt der gemeldete Fehler aber auch nach dem Neustart der Umgebung hartnäckig bestehen, liegt tatsächlich ein Problem mit unserem Code vor. In so einem Fall hilft es, wenn man die Struktur aller IF/ENDIF- und FOR/ENDFOR-Paare in der gesamten Routine nochmals aufs Peinlichste überprüft. Findet man immer noch keine Ursache, sollte man den Code nochmals mit dem letzten funktionierenden Stand im Quellcode-Repository vergleichen, um der Ursache auf die Spur zu kommen.

Analoges kann auch beim Erstellen der Grammatik auftreten. Obgleich der Xtext-Editor nichts gegen unsere Grammatik einzuwenden hat, schlägt der „Generate-Xtext-Artifacts“-Lauf mit Meldungen wie in Listing 6 fehl. In diesem konkreten Fall deutet die Meldung darauf hin, dass unsere Grammatik zwar syntaktisch in Ordnung ist, bestimmte Konstrukte in darauf basierenden DSL-Dokumenten aber mehrdeutige Ergebnisse produzieren würden, was natürlich nicht zulässig ist. Normalerweise liefert die Meldung ausreichend Informationen, um der Ursache mit etwas Nachdenken auf die Spur zu kommen, man sollte sie allerdings auch tatsächlich sorgfältig durchlesen.

warning(200): ../org.example.tests.firstDSL/src-gen/org/example/tests/first/parser/antlr/internal/InternalFirstDSL.g:444:1: Decision can match input such as "RULE_STRING ';'" using multiple alternatives: 1, 2

As a result, alternative(s) 2 were disabled for that input

error(201): ../org.example.tests.firstDSL/src-gen/org/example/tests/first/parser/antlr/internal/InternalFirstDSL.g:444:1: The following alternatives can never be matched: 2

Sprache und Strategie

Seine größte Power entfaltet der aus DSL generierte Code typischerweise erst mit seiner Umgebung, sodass sich generierter Code mit einer dazugehörigen Bibliothek aus Codebausteinen ergänzt. In unserem konkreten Beispiel bestand diese Bibliothek aus der Basisklasse und dem Registrierungsmechanismus, ohne den die gesamte Anwendung wesentlich komplexer und aufwändiger geworden wäre. Bei aller Euphorie über die Möglichkeiten des quasi im Handumdrehen generierten Quellcodes sollte man jedoch nie den Wartungsaspekt aus den Augen verlieren. Im Gegensatz zu vollwertigen Roundtrip-tauglichen IDEs haben wir hier ein fast lupenreines One-Way-Tool – aus dem generierten Code gibt es keinen Weg zurück in die DSL. Werden Änderungen oder Korrekturen am Template oder gar der Architektur der Lösung fällig, muss im schlimmsten Fall die gesamte DSL inklusive Codegenerator angepasst werden.

Der Kerngedanke hinter der Erfindung aufrufbarer Unterprogramme und später der objektorientierten Programmierung war und ist immer das Bedürfnis, bestehenden Code und aufwändig ausgetüftelte Lösungen einfach und effizient wiederverwenden zu können. Effizient heißt vor allem eben auch mit möglichst wenig zusätzlichem Code. In gewisser Weise birgt jede Form von Codegenerierung immer die Gefahr, diesen Ansatz zu konterkarieren, indem nämlich sehr viel Code, für alles und jeden Einsatzfall, generiert wird. Wie so oft ist auch hier der Schlüssel zum Problem nicht in den Werkzeugen DSL und Codegenerator zu suchen, sondern im verantwortungsvollen, klugen und maßvollen Umgang mit selbigen. Man sollte also stets zunächst alle Möglichkeiten ausschöpfen, die die jeweilige Programmierumgebung über Sprachmittel wie beispielsweise Generics, Reflection beziehungsweise RTTI oder über Drittanbieter-Tools wie etwa eine Two-Way-Modellierungsunterstützung bereitstellt, bevor man zu einer DSL greift.

Wo allerdings trotz aller Anstrengungen immer noch größere Mengen an langweiligem Standardcode zu schreiben sind, sei es, weil es dafür keine geeigneten Mittel gibt oder weil die verfügbaren Mittel aus welchem Grund auch immer nicht sinnvoll nutzbar sind, kann sich der Einsatz einer DSL durchaus lohnen. Ein typisches Beispiel sind die im OOP-Umfeld nahezu unvermeidlichen Serialisierungsmechanismen für die Datenklassen, die häufig einem immer wiederkehrenden und auch syntaktisch wenig aufregendem Schema folgen. Da sich mit ein- und derselben DSL mehrere ganz unterschiedliche Generatoren ansteuern lassen, sind somit sogar verschiedene alternative Wege möglich, wie der generierte Code aussehen kann. Das könnte beispielsweise in einem Fall eine Serialisierung nach XML sein, während man in einem anderen Fall vielleicht einen Satz SQL-Statements nach CRUD-Schema für eine adäquate objektrelationale Abbildung der Instanzendaten benötigt.

Der Mehrwert, den DSL und Codegenerierung bieten können, ist also die Entlastung des Entwicklers, indem ihm das manuelle und genauso langwierige wie fehlerträchtige Schreiben von Standardcode abgenommen wird. Neben dem naheliegenden Effekt, dass das Arbeitsergebnis damit schneller und in konstanter Qualität fertig ist, kann der Entwickler einen Teil der gewonnenen Zeit nutzen, um sich sorgfältiger mit den kniffligeren Teilen der Algorithmen des zu implementierenden Moduls zu befassen sowie mehr und bessere Tests zu schreiben. All diese Faktoren können bei sachgemäßer Anwendung sowohl Produktivität als auch Qualität der Gesamtlösung signifikant steigern. Wenn wir nochmal unser obiges Beispiel hernehmen und den handgeschriebenen mit dem per DSL generierten Code ins Verhältnis setzen, kommen wir auf einen Anteil von etwa 50 % pro Seite. Von den etwa 1500 Zeilen beansprucht das DSL-Dokument selbst lediglich 69, also ungefähr 4,5 %, oder sieben Zeilen pro Elementeklasse. Man kann sich leicht ausrechnen, dass eine neue Anwendung derselben Problemklasse dank unserer DSL lediglich folgenden Aufwand verursacht:

• Anwendungsrahmen duplizieren und präparieren
• neues DSL-Dokument schreiben
• Anwendung kompilieren und testen

Stünde man vor derselben Aufgabe und müsste die Aufgabe ohne generierten Code, aber unter Beibehaltung derselben Struktur lösen, hätte man bei angenommenen zehn Klassen statt etwa 70 Zeilen eines DSL-Dokumentes immerhin 700 Zeilen Quellcode neu zu schreiben, also etwa das Zehnfache. Dabei ist aber noch gar nicht der Fakt eingerechnet, dass der generierte Code ja in einer konstanten Qualität aus der Vorlage produziert wird, während 700 Zeilen handgeschriebener Quellcode nach den industrieüblichen Mittelwerten, die je nach Komplexitätsklasse zwischen 40 und 180 Fehler pro KLOC liegen, im ersten Anlauf ziemlich wahrscheinlich mindestens 25 Fehler enthalten wird, die erst noch gefunden und behoben werden wollen. Fairerweise muss man im Gegenzug natürlich auch die Möglichkeit einbeziehen, dass es aufgrund neu hinzukommender Anforderungen unter Umständen auch zu Änderungen an DSL beziehungsweise Generator kommen kann. Auch gilt der alte Lehrsatz, dass die Meisterschaft mit der Erfahrung kommt, sodass schon die Lernkurve dafür sorgen wird, dass man beim ersten Projekt unterm Strich vielleicht genauso viel Zeit aufwenden muss, als wenn man den kompletten Code gleich von Hand geschrieben hätte. Dennoch wird bereits an diesem vergleichsweise einfachen Beispiel deutlich, welches Potenzial im geschickten Einsatz von DSLs liegen kann.

DSL-gerechte Architekturen

Aus der Erkenntnis, dass sich DSL am ehesten dort bezahlt machen, wo Code möglichst einfachen und komplett standardisierten Strukturen folgt, lässt sich unmittelbar ableiten, dass es eine gute Idee sein dürfte, bereits beim Entwurf der modulinternen Architektur dafür zu sorgen, dass wir solche Bedingungen nach Kräften forcieren. Als Richtlinie ist es immer eine gute Idee, generierte Dateien vom restlichen Code separat zu halten und nie manuell zu bearbeiten, obwohl Xtext dies mit den so genannten Protected Regions, die nach wie vor als Erweiterung verfügbar sind, theoretisch sogar unterstützen würde. Einige moderne Sprachen wie etwa C# unterstützen das Konzept partieller Klassen, welches sich dafür hervorragend dafür einsetzen lässt – und tatsächlich nutzt Visual Studio partielle Klassen genau zu diesem Zweck, um etwa XAML-generierten Binding-Code mit dem Rest des Formularcodes zu verquicken. Leider unterstützen noch immer nicht alle modernen Programmierumgebungen diese Feature. So waren class fragments bereits für Delphi 2007 angekündigt, haben es aber nie in das finale Produkt geschafft.

Auch ohne dieses nette Feature kann man derartige Probleme aber zumindest noch halbwegs elegant lösen, indem man eine doppelte Ableitung verwendet und die Funktionalität sozusagen „schichtet“. Man nehme eine Basisklasse, also beispielsweise TElementBase. Der generierte Code erbt wie bisher von dieser Basisklasse. Eine weitere dritte Klasse, die vom generierten Code erbt, fügt sodann die noch fehlenden Puzzlestücke hinzu. Möglich wird dies alles durch entsprechende virtuelle, gegebenenfalls auch abstrakte, Methoden in Basisklasse beziehungsweise generiertem Code. Wie gesagt, nicht unbedingt elegant und durch die Notwendigkeit, diverse Methoden virtuell zu machen, die ansonsten hätten statisch sein können, vermutlich auch kein Performancewunder, aber eine mögliche Lösung. Falls es die Architektur erlaubt, sollte man solche Problemquellen aber am besten gleich von vornherein vermeiden.

Natürlich haben DSL auch einige Eigenschaften und Nachteile, die sie ungeeignet für bestimmte Einsatzfälle machen. Zuallererst ist eine mit Xtext erstellte DSL keine Skriptsprache, die sich in eine Anwendung integrieren lässt, weil ja stets ganz normaler statischer Programmcode erzeugt wird. Wie bereits weiter oben erwähnt, besteht bei Einsatz eines Codegenerators die latente Gefahr, dass das Werkzeug missbraucht und übermäßig viel Code generiert wird, obwohl sich die Probleme auch anders und ziemlich wahrscheinlich sogar besser hätten lösen lassen. Last not least bergen konzeptuelle Fehler oder eine unausgereifte Architektur das nicht unerhebliche Risiko, dass im worst case die gesamte Lösung, beginnend beim Entwurf von DSL-Grammatik und Begleit-Library, erneut von vorn beginnen muss, zumindest aber sehr aufwändig werden kann. Hier ist es also wichtig, dass zumindest das grobe Konzept vor Beginn der Arbeiten steht und vor allem auch auf Tauglichkeit für den konkreten Einsatzfall geprüft wurde.

Beachtet man diese Punkte, steht einem mit Xtext 2.3 ein ausgereiftes und äußerst funktionales Tool zur Verfügung, mit dem man Textdateien aller Art schnell, bequem und effizient generieren kann. Da das Xtext-System in der Tat ein sehr mächtiges Werkzeug ist, konnten im Rahmen dieses Artikels einige Dinge nur gestreift werden, andere wurden gar nicht erwähnt. Auf jeden Fall sollte deutlich geworden sein, dass man mit Xtext bereits in nur wenigen Stunden tatsächlich zu produktiv nutzbaren Ergebnissen kommen kann – und das ist nach Meinung des Autors ein wesentlicher Punkt, der Xtext so faszinierend macht. Es ist daher auf jeden Fall eine gute Idee, sich, nachdem man die erste Einstiegshürde überwunden hat, mit den weiterführenden Möglichkeiten wie Validierung, benutzerdefinierter Syntaxeinfärbung und vielen weiteren Dingen zu befassen.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -