Angeregt durch die “Reifeprüfung fürs Web” im Linux-Magazin 11/13 stellt dieser Artikel das Webframework Tntnet vor. Mit ihm lassen sich in C++ Webanwendungen mit MVC-Architektur programmieren.
Im Magazin-Schwerpunkt11/13 [1] durften Contao, Rails, Django und Magnolia CMS zeigen, wie sie eine Programmieraufgabe lösen. Das inspirierte die Autoren, die Beispielanwendung für diesen Artikel in C++ umzusetzen. Der Online-Veranstaltungskalender (Abbildung 1) zapft eine Open-Data-Schnittstelle an, um Straßenfeste anzuzeigen, die in einem vom Anwender gewählten Zeitraum stattfinden.
DELUG-DVD
Auf der DELUG-DVD zu dieser Ausgabe finden Sie die Aufgabenstellung sowie die Auswertung aus dem Schwerpunkt “Reifeprüfung fürs Web” im Magazin 11/13.

Abbildung 1: Dank Tntnet lässt sich die Website für die Veranstaltungssuche auch in C++ programmieren.
Als Grundlage dient nun Tntnet [2], Tommi Mäkitalos C++-Framework für Webanwendungen, das unter LGPLv2.1 steht. Im Jahr 2003 veröffentlichte er die erste Version, derzeit ist die Release 2.2 aktuell. Tntnet-Anwendungen laufen etwa bei der Deutschen Börse AG, Tommis Arbeitgeber. Dieser Artikel verwendet die jüngste Version aus dem Entwickler-Repository, die der Kasten “Tntnet installieren” einrichtet.
Tntnet installieren
Das Webframework Tntnet [2] setzt die Bibliothek Cxx-Tools voraus, die ebenfalls vom Entwickler Tommi Mäkitalo stammt. Zur Installation klont man deren Code von einem Github-Repository [4]. Im entstandenen Verzeichnis konfigurieren und übersetzen die folgenden Kommandos den Code:
autoreconf -i ./configure make
Das Kommando »su -c ‘make install’« installiert die Tools. Ebenso lässt sich Tntnet von [5] klonen und mit den gleichen Kommandos einsatzbereit machen.
Ob Tntnet funktioniert, lässt sich mit einer einfachen Beispielanwendung testen. In einem beliebigen Verzeichnis erzeugt folgender Befehl die Grundlage für die Webanwendung »myfirstproject« :
tntnet-config --project=myfirstproject
Im Verzeichnis »myfirstproject/« stößt »make« die Übersetzung an. Anschließend startet das Kommando »tntnet« die Anwendung samt eingebautem Webserver, der sich mit Logmeldungen bemerkbar macht (Abbildung 2). Im Browser ist das Projekt unter »http://localhost:8000/myfirstproject« zu erreichen.
C++ als Technologie für Webanwendungen ist im Vergleich zu Ruby on Rails und anderen Skriptsprachen sehr Ressourcen-schonend. So lassen sich damit auch Weboberflächen für die schwächeren CPUs von Embedded-Geräten verwirklichen. Am anderen Ende des Spektrums ist es für hohe Lasten skalierbar und unterstützt Multithreading. Zudem ist C++ seit etwa drei Jahrzehnten ein fester Bestandteil der IT-Welt und international standardisiert. Das macht Tntnet zukunftssicher: Anwender können darauf vertrauen, dass der Code wartbar bleibt.
Flexibel
Tntnet unterscheidet sich in einem Punkt wesentlich von verbreiteten Webframeworks wie Rails oder Django: Es propagiert nicht das Prinzip “Konvention vor Konfiguration”. Es möchte dem Anwender nicht vorschreiben, wie er sein Projekt zu organisieren und seine Dateien zu benennen hat, sondern versucht über dokumentierte Best Practice und Howtos einen Leitfaden für die Realisierung guter Software zu geben [3].
Tntnet-Applikationen lassen sich grundsätzlich auf zwei Arten umsetzen. Entweder kommt ein Tntnet-Application-Server zum Einsatz, um Shared Libraries und Objektdateien zu laden und auszuführen, oder man erzeugt ein einziges lauffähiges Binary. Im ersten Fall besteht die Applikation aus einer Shared Library und dem Tntnet-Application-Server. Im zweiten Fall lässt sich alles zu einer einzigen ausführbaren Datei zusammenfassen, was das Ausrollen vereinfacht, etwa auf Embedded-Geräten. Dieser Artikel verwendet die zweite Variante.
Zur Ansicht
Das Tntnet-Framework nutzt die verbreitete Anwendungsarchitektur Model View Controller (MVC). Für die View-Komponente, die für Ansichten zuständig ist, sieht es die projekteigene Auszeichnungssprache ECPP vor, die C++ über spezielle Tags in HTML einbettet. Beim Übersetzen wandelt ein Präprozessor sie in C++-Code um, den ein herkömmlicher C++-Compiler zu einer ausführbaren Datei kompilieren kann. Es wäre möglich, die gesamte Logik als C++-Code in HTML einzubetten. Bei kleineren Aufgaben führt das rasch zu einem Resultat, schon bei etwas umfangreicheren Projekten ist es aber nicht ratsam.
Getreu dem MVC-Prinzip ist in der Suchseite (Listing 1) kaum Logik zu finden, denn sie dient ausschließlich der Ansicht. Die Datei »strassenfeste.ecpp« deklariert zu Beginn mit »<%args>« die Anfrage-Parameter der Veranstaltungssuche. Die unterschiedlichen »scope« -Tags klären die Geltungsbereiche von Variablen innerhalb der Anwendung, einer Session und eines HTTP-Requests. Daneben befüllt die Datei das Suchformular mit den Bezirken und gibt Ergebnisse aus, sofern die Suche welche ergab.
Listing 1
strassenfeste.ecpp (gekürzt)
01 <%args>
02 q;
03 bezirk;
04 [...]
05 </%args>
06 <%application scope="shared">
07 std::vector<std::string> bezirke;
08 </%application>
09 <%request scope="shared" include="strassenfestresult.h">
10 StrassenfestResult strassenfestResult;
11 </%request>
12 <%session scope="shared" include="suchesession.h">
13 SucheSession suche;
14 </%session>
15 [...]
16 <h1>Suche</h1>
17
18 <form>
19 <table class="form">
20 <tr>
21 <th>Stichwortsuche:</th>
22 <td><input type="text" name="q" value="<$q$>"></td>
23 </tr>
24 <tr>
25 <th>Bezirk:</th>
26 <td>
27 <select name="bezirk">
28 % for (unsigned n = 0; n < bezirke.size(); ++n) {
29 <option value="<$ bezirke[n] $>"<? bezirk == bezirke[n] ? " selected"?>><$
30 bezirke[n] $></option>
31 % }
32 </select>
33 </td>
34 </tr>
35 [...]
36 </table>
37
38 % if (strassenfeste.empty()) {
39 Keine Ergebnisse
40 % } else {
41 [...]
42 % }
Im Session-Scope referenziert die ECPP-Datei zudem den Header »suchesession.h« , der die Klassen der Anwendung bekannt macht. Daneben deklariert sie die gemeinsame Variable »suche« , die die Such-Session für alle Komponenten speichert. Damit verknüpft sie auch die View mit dem Controller, der die eigentliche Arbeit macht.
Die Funktion des Controllers übernimmt der Code in »controller/suche.cpp« , gewöhnlicher C++-Code ohne besondere Tags (Listing 2). Es gibt allerdings keine Headerdatei, sodass der Anwendungsentwickler die Instanzierung indirekt durch eine Tntnet-Factory-Klasse erledigt (Zeile 21). Der Controller erbt von der Klasse »tnt::Component« , die grundlegende Funktionen eines Controllers bereitstellt.
Listing 2
Controller controller/suche.cpp
01 #include <sstream>
02 #include <tnt/component.h>
03 #include <tnt/componentfactory.h>
04 #include <tnt/httprequest.h>
05 #include <tnt/httpreply.h>
06 #include <cxxtools/log.h>
07 #include <strassenfestresult.h>
08 #include <strassenfestmanager.h>
09 #include <suchesession.h>
10
11 log_define("suche.controller")
12
13 namespace
14 {
15 class sucheController : public tnt::Component
16 {
17 public:
18 unsigned operator() (tnt::HttpRequest& request, tnt::HttpReply& reply, tnt::QueryParams& qparam);
19 };
20
21 static tnt::ComponentFactoryImpl<sucheController> factory("controller/suche");
22
23 unsigned sucheController::operator() (tnt::HttpRequest& request, tnt::HttpReply& reply, tnt::QueryParams& qparam)
24 {
25 TNT_APPLICATION_SHARED_VAR(std::vector<std::string>, bezirke, ());
26 TNT_REQUEST_SHARED_VAR(StrassenfestResult, strassenfestResult, ());
27
28 TNT_SESSION_SHARED_VAR(SucheSession, suche, ());
29
30 log_debug("sucheController; q=" << qparam.getUrl());
31
32 StrassenfestManager manager;
33
34 if (bezirke.empty())
35 {
36 bezirke = manager.getBezirke();
37 }
38
39 if (qparam.arg<bool>("suchen"))
40 {
41 log_debug("suchen");
42
43 suche.q = qparam.arg<std::string>("q");
44 suche.bezirk = qparam.arg<std::string>("bezirk");
45
46 suche.von_from = qparam.arg<std::string>("von_from");
47 suche.von_to = qparam.arg<std::string>("von_to");
48
49 if (!suche.von_from.empty())
50 suche.from = cxxtools::Date(suche.von_from, "%d.%m.%Y");
51
52 if (!suche.von_to.empty())
53 suche.to = cxxtools::Date(suche.von_to, "%d.%m.%Y");
54
55 suche.pageNo = 1;
56 strassenfestResult = manager.search(suche.q, suche.bezirk, suche.from, suche.to,
57 cxxtools::Date(), suche.itemsPerPage, suche.pageNo);
58 }
59 else if (qparam.arg<bool>("previousPage"))
60 {
61 --suche.pageNo;
62 strassenfestResult = manager.search(suche.q, suche.bezirk, suche.from, suche.to,
63 cxxtools::Date(), suche.itemsPerPage, suche.pageNo);
64 }
65 else if (qparam.arg<bool>("nextPage"))
66 {
67 ++suche.pageNo;
68 strassenfestResult = manager.search(suche.q, suche.bezirk, suche.from, suche.to,
69 cxxtools::Date(), suche.itemsPerPage, suche.pageNo);
70 }
71
72 // Return DECLINED to tell tntnet to continue processing with next
73 // mapping. The next mapping will be the corresponding view.
74
75 return DECLINED;
76 }
77
78 }
Controller
Die eigentliche Arbeit des Controllers findet dann in der Methode »operator()« statt, die HTTP-Request und -Reply sowie die Parameter der Anfrage entgegennimmt (Zeile 23). Die Methode ist ein Erbstück der Klasse »Component« und wird bei jedem eingehenden Request ausgeführt, egal ob es sich um eine GET- oder POST-Anfrage handelt. Darüber hinaus lässt sich noch eine ganze Reihe anderer Informationen über die Klasse »HttpRequest« abfragen.
Die Anwendung benutzt ein Formular, um Werte entgegenzunehmen und zu übergeben. Sie sind im Parameter des Typs »tnt::QueryParams« repräsentiert. Ein einzelnes Feld wie »bezirk« lässt sich wie folgt auslesen:
sessionShared.bezirk =qparam.arg<std::string>("bezirk");
Unerlässlich für interaktive Websites ist das Session-Handling. Tntnet-Controller bilden es mit Makros ab, beispielsweise in Zeile 28:
TNT_SESSION_SHARED_VAR(SucheSession,suche, ());
Es handelt sich hierbei um die Daten, die der Controller gemeinsam mit der View nutzt. Das Makro gibt der Instanz von »SucheSession« eine Lebenszeit über den einzelnen Request hinaus für die Dauer der Session. Jeder User erhält eine eigene Session, um deren Verwaltung sich Tntnet automatisch kümmert.
Für Werte, die nur die Lebensdauer eines Requests benötigen, gibt es das Makro »TNT_REQUEST_SHARED_VAR()« . Diese Variante genügt, um die Kooperation verschiedener Komponenten während eines Requests zu erlauben. Das MVC-Konzept von Tntnet basiert darauf, dass mehrere Komponenten oder Controller in Serie geschaltet eine Anfrage verarbeiten.
Fertig!
Das letzte Element in der Operator-Methode heißt »return DECLINED;« . Dieser Return-Wert teilt dem Tntnet-Application-Server mit: Ich bin fertig, ich konnte meine Arbeit erfolgreich erledigen. Sollte noch eine weitere Komponente den Request weiter bearbeiten wollen, kann sie das jetzt tun. Das ermöglicht mehrstufige Bearbeitung.
Model
Die letzte Komponente des MVC-Gespanns findet ihren Platz im Verzeichnis »model« . Es gibt unterschiedliche Ansichten darüber, wie viel Logik das Model enthalten sollte. Im vorliegenden Beispiel sind die Klassen sehr schlank und bestehen fast nur aus Getter-Methoden. Trotzdem sei hier auf die folgenden Zeilen in »strassenfestresult.h« hingewiesen:
friend void operator>>= (
const cxxtools::SerializationInfo& si,
StrassenfestResult& strassenfestResult);
Sie überladen den Shift-Operator, um die Klasse deserialisierbar zu machen. Die Implementierung in die Datei »strassenfestresult.cpp« (Listing 3) befüllt die Klasse mit Werten.
Listing 3
model/strassenfestresult.cpp
01 void operator>>= (
02 const cxxtools::SerializationInfo& si,
03 StrassenfestResult& strassenfestResult)
04 {
05 const cxxtools::SerializationInfo& siMessages = si.getMember("messages");
06 siMessages.getMember("messages") >>= strassenfestResult._messages;
07 siMessages.getMember("success") >>= strassenfestResult._success;
08
09 const cxxtools::SerializationInfo& siResults = si.getMember("results");
10 siResults.getMember("count") >>= strassenfestResult._resultCount;
11 siResults.getMember("items_per_page") >>= strassenfestResult._itemsPerPage;
12
13 si.getMember("index") >>= strassenfestResult._strassenfeste;
14 }
Es ist in Tntnet prinzipiell möglich, beliebige Datentypen zu verwenden, also auch primitive Typen wie etwa Integer. Davon ist aber abzuraten, denn dann gibt es keine Namensräume, die einzelne Komponenten voneinander trennen. Bei sehr großen Projekten kann es dann geschehen, dass ein Programmierer aus Versehen einen Variablennamen verwendet, der bereits an anderer Stelle vergeben wurde. Kapselt aber jede Komponente wie in der Beispielanwendung ihre Session-Daten in einer eigenen Klasse, können verschiedene Komponenten dieselben Variablennamen verwenden. Ist im Controller der Komponente A zum Beispiel folgender Code zu finden
TNT_SESSION_SHARED_VAR(CompA::SessionShared, sessionInfo, ());
bleiben die Variablen vom Namensraum »CompB« der Komponente B getrennt.
Das noch fehlende Glied in der Verarbeitungskette ist der Manager. Die Manager-Klasse »StrassenfestManager« baut die Verbindung zum Berliner Open-Data-Server auf, holt die Daten im Json-Format, deserialisiert sie und wandelt sie in Model-Klassen um. Nun ist die Kette vollständig: Die Manager-Klasse generiert die Model-Klasse und übergibt sie dem Controller, der die Model-Klasse wiederum der View zur Verfügung stellt.
Deserialisiert
Die Deserialisierung findet in »manager/StrassenfestManager.cpp« statt. Die Hauptarbeit leistet eine Klasse aus der Bibliothek »cxxtools« . Die Library stellt fundamentale Funktionen zur Verfügung, die Tntnet benötigt.
In Listing 4 ist ein gekürzter Teil des Codes zu sehen, der die Json-Daten vom Open-Data-Server holt und aufbereitet. Die Klasse »Configuration« ist dafür zuständig, die Konfiguration auszulesen und deren Daten für das Programm vorzuhalten. In diesem Fall lässt sich der Anwendungsentwickler die URL zum Server in Berlin zurückgeben.
Listing 4
manager/strassenfest-manager.cpp (Auszug)
01 cxxtools::net::Uri uri = Configuration::it().berlinUrl();
02 cxxtools::QueryParams q;
03 q.add("q", keyword);
04 [...]
05 std::string results = _client.get(uri.path() + '?' + q.getUrl());
06 std::istringstream in(results);
07 StrassenfestResult r;
08 in >> cxxtools::Json(r);
Die Klasse »cxxtools::QueryParams« dient dazu, elegant den Query-Teil einer URL zu generieren. Die darauf folgende Zeile füllt die Klasse mit Werten. »_client« ist eine Instanz vom Typ »cxxtools::http::Client« , mit der das Programm die HTTP-Verbindung aufbaut. Das Resultat der Abfrage wird in einen String-Stream umgewandelt, von dem der Deserialisierer »cxxtools::Json()« liest und die Klasse »StrassenfestResult« befüllt.
Routing
Das Routing legt fest, über welche URLs sich die Komponenten der Tntnet-Anwendung aufrufen lassen. Es ist in der Datei »main.cpp« festgelegt (Listing 5). Die Funktion »mapUrl()« teilt dem Application-Server »tnt::Tntnet« die Routen mit. Der erste Parameter ist ein regulärer Ausdruck. Die erste Route legt also fest, dass für »/« die Komponente »webmain« aufgerufen wird. Die zweite Route ruft alle Controller auf, für die der reguläre Ausdruck passt. Konkretes Beispiel: Ruft ein Benutzer die URL »http://example.com/suche« auf, kommt der Controller »controller/suche« ins Spiel.
Listing 5
Routing in main.cpp
01 tnt::Tntnet app;
02 [...]
03
04 // index page
05 app.mapUrl("^/$", "webmain")
06 .setArg("next", "index");
07
08 // controller
09 app.mapUrl("^/(.*)$", "controller/$1");
10
11 // view
12 app.mapUrl("^/(.*)$", "webmain")
13 .setArg("next", "view/$1");
14
15 app.run();
Findet sich kein passender Controller, passiert nichts. Das gilt auch für die View. In der Reihenfolge, in der die Routen gesetzt sind, arbeitet Tntnet sie auch beim Matching ab – zunächst die Controller und dann die Views. Das ist wichtig, da die Controller erst die Daten für die Views aufbereiten müssen.
Alternativen
Der Ansatz von Tntnet mag ungewohnt erscheinen, wie auch der Kasten “Bewertung der Redaktion” anmerkt. Es gibt aber keinen Grund, die Programmiersprache C++ für die Webentwicklung grundsätzlich zu verwerfen. Mit Tntdb [6] existiert zudem eine flexible und robuste Möglichkeit, die Datenbanken MySQL, PostgreSQL, SQlite oder Oracle anzubinden.
Bewertung der Redaktion
Mit Tntnet hat Tommi Mäkitalo eine korrekte Lösung der Aufgabenstellung auf die Beine gestellt [9]. Als Extras bietet sie sogar die Auswahl unter den Bezirken sowie Freitextsuche in den Veranstaltungsdaten. Die Architektur folgt dem MVC-Modell, das vielen Entwicklern von Webanwendungen vertraut ist [10].
Tntnet erlaubt daneben die Arbeitsteilung zwischen einem Webdesigner und dem Anwendungsprogrammierer, weil sich der Quelltext in ECCP-Templates mit HTML und Steuerungstags einerseits und reinen C++-Programmcode andererseits aufteilt. Jegliche Änderung, und sei es nur ein korrigierter Rechtschreibfehler, verlangt allerdings das Neukompilieren der Webanwendung.
Fortgeschrittene C++-Entwickler finden im Framework Tntnet eine vertraute Welt vor, Umsteiger von Skriptsprachen wie PHP und Ruby dürften sich mit manchen fortgeschrittenen C++-Programmierkonzepten aber schwertun. (Mathias Huber)
Tntnet ist auch nicht das einzige Projekt, das ein Webframework für C++ bereitstellt. Daneben existiert beispielsweise das Web-Toolkit Wt [7]. Dessen erklärtes Ziel ist es, die Webprogrammierung möglichst der GUI-Programmierung mit Qt nachzuempfinden. Das macht es vor allem für Single-page Web Applications interessant, die dem Desktop-Feeling möglichst nahe kommen sollen.
Wer nicht auf MVC verzichten möchte und nach einer Art “C++ on Rails” sucht, sollte sich das Treefrog-Projekt [8] ansehen. Es ist das jüngste Projekt der drei. Das Framework bietet sogar einen C++-Laufzeit-Interpreter an. Daneben setzt es auf die Philosophie “Konvention vor Konfiguration”, die sich in den letzten Jahren großer Beliebtheit unter den Webentwicklern erfreut.
Infos
- Mathias Huber, “Reifeprüfung fürs Web”: Linux-Magazin 11/13, S. 21
- Tntnet: http://www.tntnet.org
- Tntnet-Howtos: http://www.tntnet.org/howto.html
- Code der Cxx-Tools: https://github.com/maekitalo/cxxtools
- Code von Tntnet: https://github.com/maekitalo/tntnet
- Tntdb: http://www.tntnet.org/tntdb.html
- Wt: http://www.webtoolkit.eu
- Treefrog: http://www.treefrogframework.org
- Code zur Programmieraufgabe: https://github.com/maekitalo/bbstrassenfest
- Mathias Huber, “Sieg programmiert”: Linux-Magazin 11/13, S. 46






