Implementation eines Konfigurationsparsers mit Hilfe von boost::spirit (Teil 2a)
Nachdem wir im ersten Teil das Format unserer Konfigurationsdateien definiert hatten, wollen wir heute einen Parser fuer dieses Format schreiben. Es waere prinzipiell moeglich, einen solchen Parser von Hand zu schreiben, allerdings ist die Spezifikation unserer Konfigurationssyntax bereits so komplex, dass wir sicherlich sehr lange brauchen wuerden, um eine funktionsfaehige und fehlerfreie Implementierung zu erhalten (sofern wir nicht ueber langjaehrige Erfahrung in der Entwicklung von Parsern verfuegen). Daher greifen wir in diesem Beispiel auf einen sogenannten Parsergenerator zurueck.
Gewoehnliche Parsergeneratoren — wie zum Beispiel lex/yacc oder ANTLR — erwarten als Input eine Beschreibung der Syntax des zu parsenden Formats, die ueblicherweise in der Backus-Naur-Form (BNF) oder einer Erweiterung davon spezifiziert wird und die wir im Folgenden als Grammatik bezeichnen. Daraus erzeugt der Generator Code in der Zielsprache, der die Syntax analysiert und diese entweder in einen Syntaxbaum transformiert oder aber mit den Symbolen verknuepfte Aktionen ausfuehrt. Diese Separierung von Grammatik und generiertem Code hat den Vorteil, dass fuer verschiedene Zielsprachen ein und dieselbe Grammatik verwendet werden kann (sofern der Generator alle gewuenschten Zielsprachen unterstuetzt). Der Vorteil schmilzt jedoch dahin, sobald man semantische Aktionen verwenden will, da diese normalerweise bereits in der Grammatik angegeben werden und in der Zielsprache verfasst sein muessen.
Die boost Library bietet fuer C++ eine wesentlich bessere und elegantere Alternative: Den spirit Parser. Dieser verwendet Paradigmen des Template Metaprogramming, um eine Domain Specific Language auf dem C++ Syntaxraum zu definieren, die der BNF erstaunlich aehnlich ist. Was bedeutet das nun? Betrachten wir als Beispiel die Definition eines Symbols fuer ein if Konstrukt in der Sprache Pascal. In BNF wuerde man schreiben:
<if statement> ::= if <expression> then <statement> | if <expression> then <statement> else <statement>
Die verwendeten Symbole “expression” und “statement” werden als bereits definiert vorausgesetzt. Uebertragen wir diese Regel in boost::spirit, so erhalten wir
if_statement = "if" >> expression >> "then" >> statement | "if" >> expression >> "then" >> statement >> "else" >> statement;
Wir sehen, dass die Umsetzung der Grammatik in spirit fast eins zu eins der Backus Naur Form entspricht. Um den Syntaxregeln von C++ zu entsprechen, muessen wir allerdings die String Literale in Anfuehrungszeichen einschliessen und die einzelnen Elemente der Regel durch den Operator “>>” trennen (C++ erlaubt nicht die einfache Aneinanderreihung von Variablen ohne Verwendung von Operatoren zwischen den Variablen, weshalb in spirit der right shift Operator zu diesem Zweck ueberladen wurde). Regeln in spirit muessen zudem mit einem Semikolon abgeschlossen werden, da diese nichts anderes als gewoehnliche Variablenzuweisungen sind. Die Regel laesst sich mit Hilfe zusaetzlicher Operatoren noch vereinfachen zu
if_statement = "if" >> expression >> "then" >> statement >> -( "else" >> statement );
Der Operator “-” macht den folgenden, in Klammern eingeschlossenen Ausdruck optional und erspart uns damit die in der vorherigen Version noch vorhandene zweite Regelvariante. Ein ausfuehrliches Benutzerhandbuch und ein Tutorial zu boost::spirit finden Sie unter http://www.boost.org/doc/libs/1_41_0/libs/spirit/doc/html/index.html.
Kommen wir zurueck zu unserem Konfigurationsformat. Wir werden nun eine Grammatik dafuer definieren, ueberspringen dabei aber den Zwischenschritt BNF und schreiben direkt gueltige spirit Regeln. Zunaechst benoetigen wir eine Regel, die die gesamte Datei repraesentiert. Eine Konfigurationsdatei besteht aus beliebig vielen Variablenzuweisungen, also definieren wir eine Regel
file = *( variableAssignment ) >> eoi;
Der Asterisk vor dem geklammerten Ausdruck steht fuer “beliebig viele”. Das abschliessende “eoi” steht fuer “end of input” und sorgt dafuer, dass sowohl Leerzeichen am Ende der Datei erlaubt sind und gleichzeitig der gesamte String gematcht wird (ansonsten koennte man in der Datei auch ungueltige Syntax verwenden, solange davor ein Teil steht, auf den das file Symbol passt).
Als naechstes muessen wir das Symbol “variableAssignment” definieren. Eine solche Variablenzuweisung besteht gemaess unserer Syntaxvorgaben aus einem Variablennamen gefolgt von einem Gleichheitszeichen und einer Variablendefinition:
variableAssignment = literal >> '=' >> ( groupDefinition | listDefinition | arrayDefinition | atom ) > ';' ;
Wir haben also festgelegt, dass eine Variablendefinition entweder eine “groupDefinition”, “listDefinition”, “arrayDefinition” oder ein “atom” sein kann. Um den Ausdruck abzuschliessen, fordern wir noch ein Semikolon am Ende, dass in diesem Fall nicht durch “>>” sondern durch “>” angehaengt wird, was dafuer sorgt, dass der Parser sofort einen Fehler wirft, wenn die vorherigen Symbole erfolgreich erkannt wurden, das Semikolon aber fehlt. Das letzte Semikolon dient wieder dazu, das eigentliche C++ Statement abzuschliessen und gehoert nicht zur Grammatik!
Das Symbol “literal” repraesentiert einen Variablennamen. Um gaengigen Programmiersprachenkonventionen zu folgen, erlauben wir hierfuer nur alphanumerische Zeichen und den Unterstrich “_”, und definieren zusaetzlich, dass das erste Zeichen keine Ziffer sein darf. Dies fuehrt uns auf folgende Regel:
literal = (alpha | '_') >> *(alnum | '_');
Ein “literal” ist also ein Buchstabe oder ein Unterstrich gefolgt von beliebig vielen Buchstaben, Ziffern oder Unterstrichen. Beachten Sie bitte, dass nur die zweite Teilregel optional ist, der Variablenname muss also mindestens ein Zeichen lang sein (was Sinn macht).
Betrachten wir nun noch die Definition des Symbols “atom”, das wir wie folgt festlegen:
atom = (strict_double | long_ | bool_ | string);
“long_” und “bool_” sind in spirit bereits eingebaute Parser, die, wie der Name schon suggeriert, long und bool Werte parsen. Das Symbol “string” wollen wir selbst definieren. Es sieht folgendermassen aus:
string = lexeme[ lit('"') >> *(char_ - '"') > '"' ];
Ein String in unserer Konfigurationsdatei muss also mit einem Anfuehrungszeichen beginnen, gefolgt von beliebig vielen Zeichen ausser dem Anfuehrungszeichen (char_ ist wieder ein eingebauter Parser und matcht jedes beliebige Zeichen), abgeschlossen von einem weiteren Anfuehrungszeichen.
Die Regel “strict_double” muessen wir noch definieren. Ein einfaches “double_” wuerde hier nicht wie gewuenscht funktionieren, da es auch Zahlen ohne Dezimalpunkt parst und damit der long_ Parser nie aufgerufen wuerde. Die Reihenfolge der Regeln umzukehren ist jedoch auch keine Loesung, da Floating Point Zahlen in der Regel mit einer Ziffer beginnen und daher die long_ Regel saemtliche Ziffern bis zum Dezimalpunkt konsumieren wuerde, waehrend die uebrigen Zeichen bis zum abschliessenden Semikolon nicht mehr in die Grammatik passen und somit einen Parser Fehler ausloesen wuerden. Wir koennen jedoch mit nur einer Zeile Code einen modifizierten double Parser definieren, der zwingend einen Dezimalpunkt voraussetzt:
namespace qi = boost::spirit::qi; qi::real_parser<double, qi::strict_real_policies<double> > strict_double;
Fehlen noch die Symbole “groupDefinition”, “listDefinition” und “arrayDefinition”. Diese sind umfangreicher, weshalb wir ihnen einen separaten Teil widmen wollen (2b).



Mit dem E63 stellte Nokia im November 2008 das Budget Modell des erfolreichen E71 vor. Seit Anfang diesen Jahres ist das E63 nun endlich in den meisten Shops verfügbar, und vor zwei Wochen erhielt auch ich mein lange erwartetes Exemplar.