\usepackage{todonotes} \usepackage{minitoc} \usepackage{makeidx} \usepackage{sidenotes} \usepackage{tikz} \makeindex \setcounter{secnumdepth}{3}
\dominitoc

1 Wittgenstein Advanced Search Tools

1.1 Abstract des Kurses

1.2 Softwarearchitektur und Projektmanagement am Beispiel von WAST – theoretische Grundlagen und praktische Übungen

Dieses Hauptseminar erklärt Grundlagen der Softwarearchitektur und des Projektmanagements am konkreten Beispiel von WAST, den Wittgenstein Advanced Search Tools, das am CIS in Kooperation mit dem Wittgenstein Archiv in Bergen (WAB) aktiv entwickelt wird.

Dabei richtet sich der Fokus zunächst auf die Architektur dieser Plattform, und die Erklärung der einzelnen Komponenten sowie ihres Zusammenspiels. Desweiteren werden die Schnittstellen und die Projektinfrastruktur an diesem konreten Projekt erläutert und die Prinzipien dahinter vorgestellt.

Begleitet wird das Seminar von praktischen (kleinen) Beispiel-Übungen, die helfen, die wesentlichen Schritte tatsächlich selbst nachzuvollziehen und moderne Technologien und Frameworks kennenzulernen.

Besprochen und praktisch probiert werden in diesem Seminar: die Versionsverwaltung git und das am IFI angebotene gitlab als Web-Oberfläche zur Softwareverwaltung sowie Kollaboration mit anderen Entwicklern; das Wrappen von Libraries, die in C geschreiben wurden in C++; das Erweitern derselben C-Libraries mit neuen Funktionalitäten; das Einbinden der entstandenen C++-Libraries in Javascript, und damit in einem nächsten Schritt in den Browser; die Architektur von modernen Webseiten (auch als Single Page Applications – SPA – bekannt) in Form von Server/Client-Kommunikation; die Verwendung des sogenannten MEAN-Stacks zur Umsetzung von Single Page Applications, d.h. das Zusammenspiel von MongoDB + ExpressJS + AngularJS + NodeJS.

Erwartet werden nicht zwingend Kenntnisse in allen vorgestellten, o.g. Technologien und Frameworks, dafür aber ein hohes Maß an Willen, Belastbarkeit, Spielfreude, generellen, gut-bis-sehr-guten Programmierkenntnissen und, nicht zuletzt, die spielerische Neugierde, die leidenschaftliche Software-Entwickler antreibt (nicht zuletzt diejenigen, deren Technologien wir verwenden). Dazu gehört auch das exakte Lesen von Aufgaben-Spezifikationen und Programm-Dokumentation.

Als Lohn des dicht gepackten, umfangreichen Programms des Seminars mit, z.T. bedeutenden Sprüngen von Woche zu Woche, erhält man einen praktischen und einen theoretischen Einblick in konkrete, existierende Frameworks und Strukturen, sowie die Antwort auf die Frage: ich habe eine Kommandozeilen-Applikation in C++ – wie kann ich diese nun in den Browser transportieren und der Welt zugänglich machen?

1.3 Projektstruktur

1.4 Projektstruktur extern: beteiligte Institutionen und Personen

1.4.1 Das CIS und die Wittgenstein Advanced Search Tools

Die Wittgenstein Advanced Search Tools (WAST) sind eine Kollektion von verschiedenen Anwendungen des CIS, die spezifisch auf das Korpus des Archivs in Bergen, Norwegen (WAB) entwickelt wurde.

WAST ist entstanden aus einer zunehmend gewachsenen Kooperation mit dem des WAB Archivs in Bergen (vertreten durch Alois Pichler) und Max Hadersbeck (CIS).

Für die Zusammenarbeit spielen folgende Aspekte eine wesentliche Rolle:

  • begrenztes Material
  • sehr reichhaltig aufbereitetes, sehr sauberes (kontrolliertes Material)

Wittgenstein wiederum ist ein Subteil des ehemaligen(?) europäischen Projekts unter der Leitung von Paolo D’Iorio (darunter, u.a. )

WAST Projektstruktur

WAST Projektstruktur

1.4.2 WAB – Wittgenstein Archive Bergen

1.5 WAST Projektstruktur intern

Die Tool-Landschaft von WAST besteht aus zwei Dingen:

  1. Komponenten
  2. Tools und Projektstruktur

Zu den Komponenten zählen alle Dinge, die …, darunter:

  • wittfind
  • SIS: Symmetric Index Structures
  • feedback
  • wab2cis
  • WIndex

Zu den Tools zählen alle Dinge, die …, darunter:

  • wast-infrastructure
  • bug tracker
  • Versionierung
  • git
  • wiki
Die Landschaft von WAST

Die Landschaft von WAST

Die WAST-Infrastruktur (siehe ab Seite ) erlaubt es, den deploy-Prozess und den Versionierungs-Prozess zu abstrahieren, indem alle Komponenten unter einem Dach versammelt sind.

Zum Beispiel kompiliert und startet man dann die feedback-Komponente folgendermaßen:

./wast build wast-feedback-app && ./wast start wast-feedback-app

Die weiteren Komponenten funktionieren dann alle gleich und es ist wesentlich einfacher, den Überblick zu behalten.

Alle von der WAST-Infrastruktur verwalteten Komponenten findet man ganz einfach heraus, indem man

./wast

ohne Argumente startet. Hierbei werden sowohl alle Komponenten als auch Aktionen angezeigt.

1.5.1 Verwalten von Komponenten in WAST

Komponenten-Maintainer können ihre Komponente in die Gruppe cis(Gitlab cis group 2014) überführen lassen – dazu ist dann eine Mail an Thomas notwendig (glaube ich). Vorteile daraus:

  1. das Projekt wird im gitlab nicht mehr gegen das persönliche Projektlimit bei der RBG gezählt

  2. das Projekt ist weniger “personenbezogen” und kann besser von Nachfolgern “geerbt” werden

Komponenten-Maintainer, die ihre Komponente in die WAST-Infrastruktur hinzufügen wollen, machen folgendes:

  1. Zunächst, die Komponente als git-submodule[link: http://git-scm.com/book/en/Git-Tools-Submodules] adden:

git submodule add git@gitlab.cip.ifi.lmu.de:cis/wf.git components/wf

  1. Am besten direkt die Remote-Bezeichnung[link: http://git-scm.com/book/en/Git-Basics-Working-with-Remotes] origin in upstream umbenennen (der Einheitlichkeit halber)

    git remote rename origin upstream

  2. Im eigenen Projekt die wesentlichen Dateien anlegen (oder die, die man für nötig hält):

    • wast.build

    • wast.check

    • wast.start

    • wast.stop

    • wast.restart

    Beispiele hierzu finden sich hier:

    https://gitlab.cip.ifi.lmu.de/cis/wast-feedback/tree/master

1.6 Komponenten

In den folgenden Teilen werden die wichtigsten Komponenten wittfind ( ab Seite ), SIS ( ab Seite ), feedback ( ab Seite ), wab2cis ( ab Seite ) und WIndex ( ab Seite ) besprochen.


2 wittfind

2.1 Zweck

2.2 Kurzvorstellung

2.3 Funktionen

2.4 Beschreibung der verwendeten Technologien

2.5 Umfang, Wert und Größe der Codebase

sloccount [link: http://www.dwheeler.com/sloccount/]

2.6 Mitwirkende

2.7 Import/Export aus Paper

The web-based frontend WiTTFind consists of several web programs which offer an easy to use interface to query Wittgenstein’s Nachlass and presents all search results as HTML-transformation of the edited text together with an excerpt from the original facsimile of Wittgenstein’s remark. For the texts available on Wittgenstein Source1, every result is linked to Wittgenstein Source as well as to the Semantic web annotation tool Pundit Grassi et al. (2013 ^7) (see Figure [fig:search])

2.7.1 Resource Text: TEI P5 conform XML

The XML-Transcription of Wittgenstein’s Nachlass Pichler (2010), which was developed at WAB in Bergen, annotates the texts in greatest detail. All deletions and substitutions etc. of Wittgenstein are annotated in the XML-texts. The latter is too detailed in order to be used by WAST and thus a new TEI P5 TEI Consortium (2009) compatible and reduced XML-Format (CISWAB) was defined. This text format is an optimal base for the cooperation between the Wittgenstein researchers and programmers in the field of computational linguistic. The CISWAB texts are extracted via XSLT-transformation from WAB’s XML-transcriptions of Wittgenstein’s Nachlass Pichler et al. (2009).

2.7.2 Resource Lexicon: Electronic full-form Lexicon

Successful text searches crucially rely on the availability of an electronic full-form lexicon. For the work on Wittgenstein’s Nachlass, we used CISLEX Guenthner and Maier (1994), one of the biggest electronic German full-form lexica, which has been developed at CIS over the last 20 years. We extracted all words from the texts available on Wittgenstein Source, the word information from CISLEX and extended it to the special lexicon, called WiTTLex. Each word-entry in WiTTLex is formatted according to the DELA format, defined at the Laboratoire d’Automatique Documentaire et Linguistique (LADL), Paris Gaston (1991). The lexicon entries contain the word’s full-form, lemma, lexicographical word form, inflection and semantic information. Search queries to WiTTFind can be grammatically processed with the help of WiTTLex. Figure [fig:dic] shows a short excerpt from the lexicon.

[H]

Advokat, Advokat.N+HUM:neM
gesagt, sagen.V:OZ
gesamte, gesamt.ADJ+NUM:aeFxp:aeFyp:aeFzp:aeNyp:\
 amUxp:neFxp:neFyp:neFzp:neMyp:neNyp:nmUxp
dagestanden, dastehen.V+#2
glänzende, glänzend.ADJ+ER+COL+Glanz
Fermat, .EN
Fermatsche, Fermat'sch.ADJ+EN
rot, rot.ADJ+COL+Grundfarbe
rötlicher, rötlich.ADJ+COL+Zwischenfarbe+KOMP:\
 deFxp:geFxp:gmUxp:neMxp:neMzp

[fig:dic]

2.7.3 The finder Application suite ``

In order to provide extended linguistic search capabilities for explorations from non-technical and non-linguistic backgrounds, a suite of tools was developed at CIS Seebauer (2012; Krey 2013; Fink 2013; Volos 2013). With a mixture of implicit and explicit conversions we are able to provide advanced linguistic search capabilities to researchers from different fields of the humanities. The next section provides a bottom up overview on the program suite ``, one section out of WAST, that is used to find text, i.e. to find utterances of Wittgenstein.

Every query to `` is internally transformed to a local-grammar-search-graph of the search terms Maurice (1997). In its simplest form these graphs are a chain of the search terms. But in general search-graphs can contain an arbitrary number of loops and branches to express complex search patterns Fink (2013). The system uses various strategies to match tokens in the text. It is able to use i.a. string-based matching, regular expressions, semantic and morphological information from dictionaries and arbitrary annotations from the searched text itself.

In order to provide a simple interface to the user and hide much of the complexities involved, queries from the user are automatically converted to local-grammar-search-graphs. A simple query language enables the user to search for sequences of arbitrary length or combine tokens with boolean operators Fink (2013). Furthermore, the system applies implicit conversions on the queries to generate more natural search results.

2.7.4 Implementation aspects of

The tool is implemented in a client-server architecture. There are three main tools involved in the processing of queries: A server-, a client- and a displaytool. The client merely takes queries from the user and applies some very basic transformation to them before sending the actual query to the server. These transformations mostly convert convenient wildcard expressions to valid more complex regular expressions.

On the server side each query is parsed and then transformed to a local-grammar, that is applied asynchronously to the different documents the query wishes to search. One running server instance is able to provide parallel searches over a various set of different documents. All results are then send back to the client.

The third tool is a simple displaying tool, that is able to output the result of the queries in different formats2. Together these three tools provide the backend to present query results, that are further used by the WiTTFind front end to present the final results of queries.

[H]

[tab:wfbenchmark]

Query type Median (seconds)
strict 0.082
word phrase 0.080
lemma-based 0.088
inverted lemma-based 0.088
sentence 0.169
local grammar (particle verbs) 0.214

Table [tab:wfbenchmark] lists a view benchmarking results of different query types to a server instance running on a modern desktop PC3. The numbers each show the median of 10 different queries to the text of the Big Typescript All queries where limited to a maximum of 100 hits4. It should be noted, that the last two lines show the results of complex queries, that generate very few hits. These queries take a lot more time to finish (more than twice as long), since the whole document has to be processed and local grammars are used to disambiguate.

2.7.6 Word-Form Search and Part-of-Speech Tagging

In the full-form lexicon, WiTTLex, every word is stored together with its lexical word form as it is defined in the German CISLEX (see Table [tab:tags]).

[H]

[tab:tags]

Name Tag Translation
Nomen <N> noun
Adjektiv <ADJ> adjective
Verb <V> verb
Determinativ <DET> determiner
Adverb <ADV> adverb
Partikel <PART> particle
Präposition <PREP> preposition
Präposition + Artikel <PDET> preposition + article
Konjunktion <KONJ> conjunction
Interjektion <INTJ> interjection
Verbpartikel <VPART> verb particle
Eigenname <EN> proper name

The finder allows for the use of word forms as placeholders for words in a query. For example, the query Die <ADJ> Farbe finds all sentences that contain the nominative feminine definite article Die, followed by an adjective, which precedes the noun Farbe (colour) as in sentence ([ex:farbe]).

[ex:farbe]

[ex:farbe:q] Die <ADJ> Farbe [ex:farbe:a] Ich könnte Dir die genaue Farbe der Tapete zeigen, wenn hier etwas wäre was diese Farbe hat.5

To further reduce the syntactic ambiguity of the word forms in the text, we additionally tagged the data with the Part-of-Speech (POS) treetagger Schmid (1994). The finder permits POS-tags to be used as placeholders for the selection of syntactic word forms, as defined in the Stuttgart-Tübingen-Tagset Schiller, Thielen, and Teufel (1999). The user can decide to search for a lexical word form (e.g. <ADJ>) or the syntactic word-form within the sentence (e.g. [ADJ*]).

2.7.7 Semantic Lexical Classification

In WiTTLex we classified nouns Strutynska (2012) and adjectives Krey (2013) semantically. According to the work by Langer and Schnorbusch Langer and (Hrsg.) (2005) we defined eleven classes for nouns (see: Table [tab:semnoun]) and eleven classes for adjectives (see: Table [tab:semadj]).

[H]

[tab:semnoun]

Name Tag Translation Occurrences
Menschen <HUM> humans 140
Tiere <T> animals 96
Pflanzen <PF> plants 26
Objekte <OBJ> objects 1402
Ereignisse <ER> events 589
Zustände <ZU> states 51
Eigenschaften <EIG> p properties 236
Temporalia <TEMP> time 49
Eigennamen <EN> proper names 60
Numeralia <NUM> number 47
Diversa <SONST> other 713
Name Tag Translation Occurrences
Farben <COL> colour 974
Numeralia <NUM> number 1258
Relation <REL> relation 2517
Eigennamen <EN> proper names 17
Temporalia <TEMP> time 619
Evaluation <EVAL> evaluation 1732
Zustände <ZU> states 6629
Komparativa <KOMP> compariative 2080
Stilistika <STIL> style 1917
Eigenschaft <EIG> property 382
Ereignisse <ER> proppperty 187

In our investigations in Wittgenstein’s Big Typescript (BT)6, we found that there are about 1800 nouns out of all 46000 words in the data (see: Table [tab:semnoun] and [tab:semadj]). All the Tags from the Tables can be used in WiTTFind. For example, the query <EN> und <EN> returns the sentence ([ex:frege]) in which the two proper names Fregeschen and Russellschen are joined by the coordinating conjunction and.

[ex:frege]

[ex:frege:q] <EN> und <EN> [ex:frege:a]Unzulänglichkeit der Fregeschen und Russellschen Allgemeinheitsbezeichnung.7

In cooperation with the Faculty of Philosophy, Philosophy of Science and the Study of Religion, at the University of Munich (Rothhaupt Rothhaupt (1996)), we implemented a first semantic classification of Wittgenstein’s colour vocabulary together with a special HTML-interface for querying colours in the Nachlass. We found, that five different categories for colours are optimal for Wittgenstein’s Nachlass: Grundfarbe (primary colour), Zwischenfarbe (intermediate colour), Transparenz (transparency), Glanz (gloss) and Farbigkeit (colourness) Krey (2013). Table [tab:sem] shows the different labels for colours and the number of their occurrences in the text.

In the web-frontend WiTTFind, the user can select between different colour categories (see: Table [tab:sem]), view statistics and query Wittgenstein’s Nachlass.

2.7.8 Sentence structure and Wildcards

For the Wittgenstein researchers it is very important to take the sentence structure into account. To enable users to specify the sentence structure within queries, we introduced sentence structuring tags in queries, such as <BOS> (the beginning of a senctence), <EOS> (the end of a sentence) and <PUNCT> for punctuation characters. The wildcard operator * can be used as placeholder for arbitrary character sequences. The query: <BOS> Ich meine nur <PUNCT> * * * <PUNCT> <EOS> would return all sentences that consist of six words starting with the sequence of three tokens Ich meine nur followed by a punctuation character as in example ([ex:sage])

[ex:sage]

[ex:sage:q] <BOS> Ich meine nur <PUNCT> * * * <PUNCT> <EOS> [ex:sage:a] Ich meine nur, was ich sage.8

[tab:sem]

|c|c|p2.0cm|c| Name & Tag & Translation & Occurrences
Grundfarbe & <Grundfarbe> & basic colour & 454
Zwischenfarbe & <Zwischenfarbe> & intermediate colour & 301
Transparenz & <Transparenz> & transparency & 105
Glanz & <Glanz> & gloss & 2
Farbigkeit & <Farbigkeit> & colourness & 29

2.7.9 Rule-based linguistic search with Part-of-Speech Tagging (POS-tagging)

To enrich the number of found verbs concerning a search query, we implemented an automatic particle-verbs detection and distinction. Particle-verbs are marked in their lexical entry and divided into verb and particle. In the Big Typescript we found almost 750 verbs with separated particles. To disambiguate the separated particles from prepositions, we use Part-of-Speech tagging and local grammars. For example, the query with the particle verb dastehen would extract instances as: steht klar da … in which the verb particle da, which may occur in German separately from the verb stehen, is recognized as such and not as a preposition (see: Figure [fig:dastehen]).

2.7.10 Search without Alternatives

One characteristic feature of Wittgenstein’s Nachlass is, that Wittgenstein changed his texts very often and as a result the Nachlass offers a lot of different readings. To enable the finding of remarks in all of the latter, in a background process of the finder application, we generate all different readings, which are processed simultaneously Seebauer (2012).

2.7.11 Search Result Layout with the Facsimile

Ludwig Wittgenstein’s Nachlass is highly heterogeneous. It consists of a large number of texts, some of them handwritten, with numerous passages edited from the author himself. The researchers can only appreciate the found remarks to the fullest with all its editions, errors and overall form, if they see not only the HTML-transformation of the edition, but as well their original facsimile which should be displayed simultaneously Rothhaupt (2006). Only this combined view provides the possibility for comparison and analysis of the original material, which carries the complete set of information. In order to implement this layout, it was necessary to OCR all facsimile of Wittgenstein’s Nachlass, extract the coordinates of his remarks and link them to the search result according to their siglum Gotscharek et al. (2011). All the work was done with ABBYY FineReader and additionally developed PERL programs. The highlighting in the display of the facsimile is done with CSS techniques within the HTML Page Lindinger (2013).


3 SIS: Symmetric Index Structures

SIS (und sein Web-Teil) besteht aus mehreren Schichten und dem ineinandergreifen verschiedener Technologien und Frameworks.

Alle diese Teile werden in den nächsten Kapiteln besprochen:

3.1 Einführung

SIS, die Symmetrischen Index Strukturen sind Endliche Automaten in Form von SCDAWGs (Symmetric Compacted Directed Acyclic Word Graphs), welche es erlauben, Texte zu indexieren und während des Lookups Vorschläge und Vervollständigungen sowohl klassisch nach-rechts als auch nach links zu geben.

Diese werden detailliert in (Daniel Bruder 2012) besproche, welches aufbaut auf (Gerdjikov et al. 2013), und dieses im Wesentlichen mit (Blumer et al. 1987) verknüpft.

Die Grundidee hinter SIS ist folgende: Man nehme zwei nicht-symmetrische CDAWG-Automaten – in linearer Zeit und auf linearem Raum direkt, on-line erstellt (Inenaga et al. 2001) –, und befülle einen mit einem Text in “normaler”, links-nach-rechts-Richtung (left to right, LTR), und den anderen in umgekehrter Richtung (RTL). Dann identifiziere man die identischen States dieser beiden Automaten (Gerdjikov et al. 2013). Daraufhin annotiere man die Automaten mit den Hinweisen auf die Dokumente, für welche sie stehen ((Daniel Bruder 2012))

3.2 Zweck

3.3 Kurzvorstellung

3.4 Verwendung auf der Kommandozeile

Die ausführbare Datei sis (im Verzeichnis build/bin/sis) erlaubt zwei verschiedene Modi: einmal zum Indexieren eines vorhandenen Korpus (sis index) und zum anderen zur Suche auf einem bestehenden Index (sis context).

Ein erster Versuch auf Kommandozeile ergibt folgendes Ergebnis:

> bin/sis
This is SIS. Please see `sis --help' for available commands and options

You can get additional help using `sis <COMMAND> --help'

Available COMMANDs are:
context
index

Und der Aufruf der Hilfe ergibt folgendes:

> bin/sis --help

sis
---

Usage:
    sis [GENERAL OPTIONS] COMMAND [COMMAND OPTIONS] ...

Allowed general options:
  -h [ --help ]         show help message
  -v [ --version ]      show version
  --commands            show available commands
  -i [ --input ] arg    input files

SIS funktioniert also ähnlich wie git mit dem Hauptbefehl sis und den Unter-Kommandos index und context.

Zu jedem Unterkommando gibt es nun noch eine weitere Hilfe, welche die speziellen Argumente erklärt – hier für sis index:

> bin/sis index --help
Options for command 'index':
  -o [ --output ] arg   output name
  -f [ --thefile ] arg  file listing input files

Und hier für sis context:

> bin/sis context --help
Options for command 'context':
  -w [ --width ] arg (=10) lr-context width
  -q [ --query ] arg       query string
  -p [ --print-filename ]  print the filenames

Beide Sub-Kommandos erwarten also (laut den general options) mindestens ein Inputfile mit --input bzw. -i:

bin/sis index -i INPUTFILE
bin/sis context -i INPUTFILE

Für das Sub-Kommando index wird als Inputfile entweder

  1. mit -i eine Auflistung von Dateien erwartet:
    • bin/sis index -i file1 -i file2
  2. mit -f eine Datei, welche die Input-Dateien auflistet (falls die Input-Dateien mit -i zu viele werden):
    • bin/sis index -f file.list

Die Input-Dateien für sis index müssen folgendermaßen beschaffen sein:

  1. entweder alle Dateien sind XML-Dateien (nach dem CISWAB-TEI-Standard)
  2. alle Dateien sind plain-text-Dateien

(Um hier mehr Flexibilität anbieten zu können müssen noch mehr features für einen Importer geschrieben werden, siehe https://gitlab.cip.ifi.lmu.de/bruder/sis3/issues/93; desweiteren werden bei plain-text Dateien die Dateinamen als Siglum interpretiert).

Solange die Importer noch nicht komplett vollendet sind, gibt es eine Zwischenlösung, plain text Dateien herzustellen:

Man kann die Testdaten aus src/features/data/Ts-213 und den Extractor unter src/features/data/Ts-213/extract.sh verwenden um plain-text Dateien herzustellen.

Der Extractor liefert die Dateien in das Verzeichnis work/ aus, man ist aber selbst dafür zuständig, dieses frei zu halten (also gegebenenfalls zu löschen vor der nächsten Extraktion).

> ./extract.sh                                                                                                                                             
Usage:                            
./extract.sh [7bit|utf8]                    

(results will be written to work/)
(make sure you erase work/ before)

Der Extractor ist, wie gesagt, nur eine Zwischenlösung lässt sich aber verwenden. Der Befehl …

> ./extract.sh utf8

… wird plain text Dateien nach work/ extrahieren, welche sich für bin/sis index -i file1 -i file2 verwenden lassen.

Idealerweise legt man eine Datei allfiles.list o.ä. an …

> find ../src/features/data/Ts-213/work/utf8 \
        -type f \
        -iname '*.satz' \
        -exec echo `pwd`/{} \; \
        > Ts-213.allfiles.list

… um diese in einem Stück füttern zu können:

> bin/sis index -f Ts-213.allfiles.list -o allfiles.index

Letzterer Befehl erstellt einen Index für alle Dateien, die in Ts-213.allfiles.list gelistet sind und legt – wegen der zweistufigen Serialisierung (siehe das zugehörige Kapitel) –, zwei Serialisierungsdateien an: allfiles.index für den C++-Teil des Automaten und allfiles.index.c für den C-Teil des Automaten (es sei an dieser Stelle kurz darauf hingewiesen, dass sich allfiles.index.c auch mit der executable der C-Schicht, unter bin/caut verwenden lässt, ohne den C++-Teil zu benötigen):

> ls -lh allfiles.index*
-rw-r--r--  1 dbruder  staff   6,1M 11 Mär 13:26 allfiles.index
-rw-r--r--  1 dbruder  staff    12M 11 Mär 13:26 allfiles.index.c

Nachdem also der Index angelegt ist, lässt sich auf diesem Suchen – hierbei fungiert der vorhergehende Output (-o allfiles.index) diesmal als Input für bin/sis context:

> bin/sis context -i allfiles.index -q erst 
Automaton loaded
size is 5548
results.size() = 599
'oder erinnerst Du Dich a'
'Erinnerst Du Dich d'
'oran erinnerst Du Dich? '
'e verifizierst Du das? ?'
' daß man erst ahnungslo'
' ich doch erst aufbauen;'
[...]

Laut der Hilfe zu sis context (bin/sis context --help) lässt sich unter anderem die Breite der ausgeschnittenen Ergebnisse einstellen (-w WIDTH):

>  bin/sis context -i allfiles.index -q erst -w 20
Automaton loaded
size is 5548
results.size() = 599
's gesagt; oder erinnerst Du Dich an die Stim'
'Erinnerst Du Dich daran, daß'
'“ Woran erinnerst Du Dich? — '
'Wie verifizierst Du das? — dann we'
' nicht so, daß man erst ahnungslos ist, und'
'rt müßte ich doch erst aufbauen; oder: von'
'gen: Ein Satz folgt erst aus ihm, wenn er da'
'ch mir vielmehr nun erst ausrechnen. '
' an, die deren Sinn erst bestimmen; was nich'
[...]

3.5 Überblick

Im Folgenden ein Überblick über die Projektstruktur von SIS, die Schichten, aus welchen SIS sich zusammensetzt, und die wichtigsten Klassen innerhalb von SIS.

3.5.1 Projektstruktur

Hier ein Überblick und eine Beschreibung der wichtigsten Teile der Projektstruktur:

.
|-- build                   # Out-of-source dir für cmake / Kompilierung
|   |-- bin                 # Platz für die gebildeten exeucatables
|   |-- lib                 #            --- " ---     libraries
|   `-- src                 #            --- " ---     *.o
|-- cmake                   # cmake-module für Makefile-Generation
|-- etc                     # Test-Daten und anderes
|-- src                     # C++-Sources
|   |-- adapter             # Alle Adapter-Klassen (wrappen vendor/cautomata)
|   |   |-- VoidSequence
|   |   `-- inenagaCDAWG
|   |-- features            # Tests
|   |   `-- data            # Test-Daten
|   `-- indexer             # Indexer-Klassen
|-- vendor                  # 3rdparty libs (eingebunden als git submodules)
|   |-- cautomata           # C-Schicht von Petar Mitankin, Sofia
|   |-- make_unique         # wurde im C++11-Standard schlicht vergessen
|   |-- pugixml             # XML-Modul
|   |-- serialization       # ...
|   `-- testing
`-- web                     # Web-Schicht: MEAN-Stack
    |-- bower_components    # Client-seitige Javascript-Bibliotheken
    |-- build               # hier wird das node-addon gebildet
    |-- cpp                 # Hier liegen die Klassen, die src/ nach JS wrappen
    |-- node_modules        # Server-Seitige Module
    |-- public              # Client-Seite
    |   |-- javascripts
    |   `-- stylesheets
    |-- routes              # Server-Seite: Routes
    `-- views               # Client-Seite: Views
        `-- partials

3.5.2 Schichten

  • Schicht 1: C9

    • Bereitstellung von SCDAWG-Automaten für einzelne Wörter/Wörterbücher
  • Schicht 2: C++(11)10

    • Erweiterung der SCDAWGs aus der C-Schicht, um diese für die Suche innerhalb von Dokumenten (statt Wörtern) verwendbar zu machen
  • Schicht 3: Web11 mit:

    • Serverseite

      • Nodejs+ExpressJS

      • node addon als Wrapper der C++-Automaten (ermöglicht das Einbinden von C++-Objekten in Javascript code)12

    • Client-Seite

      • AngularJS13
SIS Schichten

SIS Schichten

3.5.3 Klassen

Zunächst ein Überblick:

Klassenarchitektur von SIS

Klassenarchitektur von SIS

Im Folgenden eine Kurz-Besprechung der (wichtigsten) Klassen, ihres Zwecks und ihrer grundlegenden Eigenschaften aus der C++-Schicht, welche die structs aus der C-Schicht wrappen und dokumentindexierende Eigenschaften zur Verfügung stellen:

3.5.3.1 *Adapter

Jedem *Adapter liegt ein C-Struct zugrunde, vergleiche das entsprechende Kapitel zum grundlegenden Wrapping-Pattern

3.5.3.2 Document

Ein sis::Document stellt die grundsätzliche Basis dessen dar, was letztendlich von einem DocumentIndexingAutomaton verarbeitet und indexiert wird.

Es hat zwei wesentliche members:

  • filename_ und
  • contents_

wobei filename das Siglum darstellt und contents den tatsächlichen Inhalt.

In einem Stück wie diesem etwa…

<s n="Ms-115,3[2]_1" ana="facs:Ms-115,3 abnr:7 satznr:22">
  “Was heißt es: ‘dieser Gegenstand ist mir<lb/> wohlbekannt?’”
</s>

entspräche:

  • filename == Ms-115,3[2]_1
  • und contents == “Was heißt es: ‘dieser Gegenstand ist mir wohlbekannt?’”
3.5.3.2.1 Hinweise
  • Document ist hinsichtlich des string-Typs templatisiert: damit lässt sich besser mit dem unterliegenden string-Typ der C-Schicht, VoidSequence umgehen.

  • Die C-Schicht verlangt unbedingt, dass zu einem gegebenen Zeitpunkt einmal das Makro mSetEndianness() aufgerufen wird. Anonsten passieren können sehr böse Dinge während der Serialisierung geschehen (bytes können falsch herum eingelesen werden, da die Endianness nicht richtig definiert ist) und damit nur sehr schwer nachvollziehbare Fehler entstehen. Daher ruft der Konstruktor von Document dieses Makro auf.

  • Die contents eines Document werden aus Platzgründen nicht serialisiert. Schlussendlich sind diese Inhalte in automaton()->data_ verfügbar (welches serialisiert wird) und über entsprechende VoidSequenceAdapter::iterators rekonstruierbar (in Zusammenarbeit mit DocumentIndex )

3.5.3.3 DocumentCollection

DocumentCollection ist im wesentlichen eine Liste von Documents.

Ihm können auch XML-Dateien als Input gegeben werden und er erstellt daraus “virtuelle” Dokumente (durch die Methode import()), als wären die einzelnen Documents in tatsächlichen Dateien vorgelegen (s.o. das Beispiel mit Siglum/Contents im Kapitel Document).

Die Methode import() verwendet pugixml(Arseny Kapoulkine 2014a), ein externes Modul aus dem vendor-Teil des Projekts (als git submodule von (Arseny Kapoulkine 2014b)).

3.5.3.4 DocumentIndex

Ein DocumentIndex ist eine std::map die Documents und ihre Positionen verwaltet:

typedef Document<StringType>            key_type;
typedef DocumentPos<StringType>         value_type;
typedef std::map<key_type, value_type>  docinfo_t;
/// @todo rename: value_type -> mapped_type (do this everywhere)

Document und ihre DocumentPos werden im index-ing-Schritt des DocumentIndexingAutomaton angelegt:

  • zu jedem Document gehört ein sinkstate im Automaten, d.h. ein Final-State
3.5.3.4.1 Hinweise
  • Es finden sich z.T. noch veraltete APIs innerhalb dieser Klasse, die für den Vorgänger von DocumentPos, DocumentPositions gedacht waren. Diese werden Stück für Stück entfernt werden, sind aber mit deprecated gekennzeichnet.

3.5.3.5 DocumentIndexingAutomaton

Der DocumentIndexingAutomaton (Abbildung auf Seite ist die komplexeste Klasse hinsichtlich seiner Member. Er ist die zentrale Stelle, an der alle Informationen zusammenlaufen:

  • Hinzufügen von Documents zu den C-Automaten und entsprechendes
  • Indexing dieser Dokumente, d.h.
    • verwalten der sinkstates
    • und der Documents
  • Retrieval und Suche auf den Automaten, und dabei
    • “Ausschneiden” von passenden Ergebnissen
    • und Auslieferung derselben an die Clients
DocumentIndexingAutomaton UML

DocumentIndexingAutomaton UML

3.5.3.6 DocumentIndexingAutomatonAbstract

Die Basis-Klasse für DocumentIndexingAutomaton (Abbildung auf Seite )

DocumentIndexingAutomatonAbstract UML

DocumentIndexingAutomatonAbstract UML

3.5.3.7 DocumentInformation

3.5.3.8 DocumentPositions

3.5.3.9 FindResult

3.5.3.10 TEIReader

3.6 Ebenen

SIS setzt sich aus mehreren Ebenen bzw. Schichten zusammen, welche im folgenden besprochen werden:

  • C-Ebene
  • C++-Ebene
  • Web-Ebene

3.6.1 C++-Ebene

  • TDD: Test Driven Development [link: http://www.agiledata.org/essays/tdd.html] bzw. [link: späteres tdd kapitel]
    • lest (Martin Moene 2013)
  • make_unique (C++1y Standard Committee 2014)
  • Git Submodules (“Git - Submodules” 2014): git submodules in cmake (Daniel Bruder 2014)
  • andere vendor-submodules aufzählen
  • Template Meta-Programmierung14
    • [TMP vorstellen]
  • Serialization (siehe mehr zum Thema Serialisierung allgemein im Kapitel Serialisierung und wie Serialisierung in SIS gemacht wird im Kapitel Zweistufige Serialisierung)
    • cereal (uscilab 2014)
    • [zwei Serialization Module vorstellen]
    • [link: cereal]
    • [link: boost serialization]

3.6.1.1 Detaillierter Walk-through durch spezielle Teile

3.6.1.1.1 Automatentheorie: SCDAWGS, Bijektive Abbildung
3.6.1.1.2 Automatenkonstruktion
3.6.1.1.3 Dokumenterweiterung
3.6.1.1.4 Wrapping von C-Strukturen in C++-Klassen
3.6.1.1.4.1 Warum C Strukturen Wrappen?
3.6.1.1.4.2 Beispiele (Code Walk Through)

Hier ein Beispiel für ein C-Header-File, das ein struct beschreibt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef _HEADER_FILE_COMPRESSED_AUTOMATON_
#define _HEADER_FILE_COMPRESSED_AUTOMATON_

#define mCompressedAutomatonGetStatesStored( aut ) ( (aut)->statesTransitions->seqStored )

#define mCompressedAutomatonGetTransitionsStored( aut ) ( (aut)->transitionsStart->seqStored )

typedef struct tCompressedAutomaton{
    UINT initialState;
    UINTSequence * statesTransitions;
    UINTSequence * statesNumberOfTransitions;
    UINTSequence * statesEnd;
    UINTSequence * transitionsStart;
    UINTSequence * transitionsTo;
    VoidSequence * data;
    TarjanTable * tt;
} CompressedAutomaton;

extern CompressedAutomaton * CompressedAutomatonInit( UINT symbolSize );
extern void CompressedAutomatonAddState( CompressedAutomaton * aut, UINT state );
extern void CompressedAutomatonAddTransition( CompressedAutomaton * aut, UINT stateFrom, UINT start, UINT stateTo );

// ...

Und hier die wrappende C++-Klasse mit “sauberen” Konstruktoren (CTORs) und (automatischen) Desktruktoren (DTORs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/// @brief Adapter for CompressedAutomaton
template<class StringType>
struct CompressedAutomatonAdapter
  : virtual public CompressedAutomatonAbstract<StringType>
{

    using CompressedAutomatonAbstract<StringType>::traits::string_type;
    using CompressedAutomatonAbstract<StringType>::traits::symbol_size;

protected:
    using adaptee_handle = 
        std::unique_ptr< CompressedAutomaton, decltype(&::CompressedAutomatonFree)>;
    adaptee_handle adaptee_;

public:
    explicit CompressedAutomatonAdapter()
    : adaptee_ { ::CompressedAutomatonInit(symbol_size), &::CompressedAutomatonFree }{}
    virtual ~CompressedAutomatonAdapter() {}


//——————————————————————————————————————————————————————————————————————————//
//     M A P P I N G   I N T E R F A C E              (AutomatonAbstract)   //
//——————————————————————————————————————————————————————————————————————————//
public:
    virtual void shrink() override;
    virtual void write(FILE * fp) const override;

// ....

//——————————————————————————————————————————————————————————————————————————//
//     M A P P I N G   I N T E R F A C E            (CompressedAutomaton)   //
//——————————————————————————————————————————————————————————————————————————//
public:
    virtual void add_state(UINT state) override;
    virtual void add_transition(UINT stateFrom, UINT start, UINT stateTo) override;

3.6.1.1.5 Template Meta Programmierung in C++-Automaten

(Vergleiche auch das Kapitel Techniken/TMP)

Wie im Kapitel [Techniken/TMP] ausgeführt, erlaubt es Template Meta Programmierung, oder kurz: TMP, bestimmte Entscheidungen und Berechnungen zur Compile-Zeit auszuführen, bzw. diese in die Compile-Zeit zu verlagern.

Das kann einerseits erhöhte Typ-Sicherheit gewährleisten (bei weniger Code-Aufwand), schlicht Zeit ersparen, oder, wie in diesem Fall, die Konstruktion, oder besser: die “Zusammenstellung” von Klassen übernehmen.

Konkret verstanden werden soll unter “Zusammenstellung” in diesem Fall, dass durch die Technik der TMP eine kohärente Klasse geschrieben wird, die sich je nach Instantiation unterschiedlich “verhält”.

Ganz gezielt wird diese Technik in Form der Automaten – kurz gesagt –, auf folgende Weise genutzt:

Das C-struct wird, wie im vorhergehenden Beispiel mit einer symbolSize konstruiert. Die UINT symbolSize wird anderswo definiert und kann (bzw: soll) zwei unterschiedliche unsigned int-Werte annehmen:

  • 1, um 7-bit Strings zu verarbeiten
  • 4, um UTF-8 zu verarbeiten

Vergleiche folgende Code-Teile aus der C-Schicht:

1
2
3
4
5
6
7
8
9
10
11
12
13
// typical.h
typedef unsigned char U8;
typedef unsigned short int U16;
typedef unsigned int U32;
typedef unsigned long long U64;

typedef char S8;
typedef short int S16;
typedef int S32;
typedef long long S64;

typedef U32 UINT;
typedef S32 SINT;

und folgenden Teil aus voidsequence.h:

1
2
3
// voidSequence.h
#define ENCODING_PLAIN (0)
#define ENCODING_UTF8 (1)

und – nicht zuletzt –, folgenden Teil aus compressedAutomaton.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// compressedAutomaton.c
CompressedAutomaton * CompressedAutomatonInit( UINT symbolSize ){
    CompressedAutomaton * aut;

    aut = Malloc( 1, sizeof(CompressedAutomaton) );
    aut->initialState = NO;
    aut->statesTransitions = UINTSequenceInit2( 1024, 1024 );
    aut->statesNumberOfTransitions = UINTSequenceInit2( 1024, 1024 );
    aut->statesEnd = UINTSequenceInit2( 1024, 1024 );
    aut->transitionsStart = UINTSequenceInit2( 1024, 1024 );
    aut->transitionsTo = UINTSequenceInit2( 1024, 1024 );
    aut->data = VoidSequenceInit2( symbolSize, 1024, 1024 );
    aut->tt = NULL;
    return aut;
}

Das Problem mit diesem Code nun gestaltet sich folgendermaßen:

  1. Zum einen muss der Anwender der Klasse wissen, was mit UINT symbolSize gemeint sein soll
  2. Der Anwender der Klasse muss wissen, welche Werte UINT symbolSize annehmen darf. Es ist nicht definiert, was passiert, sollte ein Anwender den Wert 3 hineingeben

Anders ausgedrückt verstößt dieser Code gegen mehrere Richtlinien für “sauberen” Code:

  1. Scott Meyers (Effective C++): Machen Sie es dem Anwender so einfach wie möglich, Ihre Klasse richtig zu verwenden und so schwer wie möglich, sie falsch zu verwenden (Meyers 2005)
  2. Stroustrup (“Day 1 Keynote - Bjarne Stroustrup: C++11 Style” 2012): “Strong typing”. Als Apollo 11 (oder 13?), ein millionen- (oder milliarden-?) schweres Projekt hart abgestürzt. Warum? Weil eine Methode einen Wert float übernehmen sollte, die die Verzögerungsgeschwindigkeit anzeigen sollte. Dabei war nicht klar, ob in km/h oder in meile/h angegeben sollte. Mit strong typing und Verwendung von Klassen, die SI-Einheiten darstellen, wäre so etwas nicht passiert, da ein Objekt km/h nicht in eine Methode hineingegeben werden kann, das meile/h verlangt. Vergleiche hierzu unbedingt das Kapitel über SI-Einheiten in Stroustrups “the C++-Language” für C++11

Zurück zu unserem Code:

Um es so einfach wie möglich zu machen die Wrapper-Klassen richtig zu benutzen, und so schwer wie möglich, diese falsch zu benutzen (und dabei auch noch Platz und Größe zu sparen), benutzen die Wrapper-Klassen Template-Meta-Programmierung, die folgendes erreicht:

  1. Eine class CompressedAutomatonAdapter mit der Definition template<class StringType> struct CompressedAutomatonAdapter ist templatisiert, wobei ein Template-Parameter erwartet wird, der:
    1. entweder ein std::string ist
    2. ein std::wstring ist
    3. oder eine (egal welche) von std::string abgeleitete Klasse darstellt
  2. Dabei erreicht die template class CompressedAutomatonAdapter, dass:
    1. die unterliegende, adaptierte Klasse CompressedAutomaton mit genau dem richtigen Wert für UINT symbolSize aufgerufen wird
    2. keine “falsche” Instantiierung vorgenommen werden kann (strong typing)
    3. man die Klasse nicht falsch verwenden kann (durch static_assert), bzw. schon zur Compile-Zeit über eine falsche Verwendung (mit entsprechender Fehlermeldung) aufgeklärt wird – und der Code gar nicht erst kompiliert (statt später ohne Warnung in “undefined behavior” hineinzurennen)

Konkret wird dies erreicht und umgesetzt durch folgenden Code:

Zunächst noch einmal die Konstruktion der adaptierten Klasse:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// @brief Adapter for CompressedAutomaton
template<class StringType>
struct CompressedAutomatonAdapter
  : virtual public CompressedAutomatonAbstract<StringType>
{

    using CompressedAutomatonAbstract<StringType>::traits::string_type;
    using CompressedAutomatonAbstract<StringType>::traits::symbol_size;

protected:
    using adaptee_handle = 
        std::unique_ptr< CompressedAutomaton, decltype(&::CompressedAutomatonFree)>;
    adaptee_handle adaptee_;

public:
    explicit CompressedAutomatonAdapter()
    : adaptee_ { ::CompressedAutomatonInit(symbol_size), &::CompressedAutomatonFree }{}
    virtual ~CompressedAutomatonAdapter() {}

Bemerkungen:

  • StringType in Zeile 2 ist lediglich deskriptiv, aber nicht präskritiptiv: sie ist lediglich dazu geeignet, dem Leser zu sagen, was hier erwartet wird, prüft dies aber nicht (oder kann dies ohne Concepts, die erst in C++14 kommen werden, gar nicht)
  • konstruiert wird der adaptee_ mit einer symbol_size, die von CompressedAutomatonAbstract<StringType>::traits::symbol_size geliefert wird
  • CompressedAutomatonAbstract<StringType> wiederum ist ein gemeinsames Interface, das alle Automaten teilen werden

Hiermit also zur Betrachtung des Interfaces CompressedAutomatonAbstract<StringType>

1
2
3
4
5
6
7
8
9
10
11
/// @brief interface for CompressedAutomaton
template<class StringType>
struct CompressedAutomatonAbstract
  : virtual public AutomatonAbstract<StringType>
{

    using AutomatonAbstract<StringType>::traits::string_type;
    using AutomatonAbstract<StringType>::traits::symbol_size;  

    virtual UINT get_symbol_size() const = 0;
    virtual CompressedAutomaton * read( FILE * fp ) = 0;

Dinge, die man hier erkennen kann:

  • Das Interface für CompressedAutomaton beruft sich auf ein noch grundlegenderes Interface: AutomatonAbstract<StringType> und “erbt” dessen class traits
  • Das Interface für CompressedAutomaton deklariert pure virtuals für alle erbenden Klassen und zeichnet sich damit durch folgendes aus:
    • sie ist eine abstrakte, nicht instantieerbare Klasse
    • verlangt von allen erbenden Klassen, diese Methoden zu implementieren, andernfalls, bleiben diese ebenfalls abstrakt
    • definiert dadurch ein gemeinsames Interfaces aller erbenden (nicht-abstrakten) Klassen, auf das man sich berufen kann.

Nun ist aber nocht nicht die Herkunft der traits geklärt.

Traits erlauben es, “kostenlos”, d.h. ohne Speicher zu belegen, Informationen in Klassen zu halten, die während der Compile-Zeit genutzt werden können.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
namespace sis {

template <class StringType>
struct string_traits {
public:
    typedef typename ElementSize<StringType>::type  string_type;
    static constexpr unsigned                       symbol_size = ElementSize<string_type>::value;
};

/// @brief  Defines an Automaton in general
/// @todo   rename: AutomatonAbstract -> AdaptedAutomaton (?)
template<class StringType>
struct AutomatonAbstract : boost::noncopyable, string_traits<StringType> {

    using string_type = typename            string_traits<StringType>::string_type;
    static constexpr unsigned symbol_size = string_traits<StringType>::symbol_size;

//——————————————————————————————————————————————————————————————————————————//
//     C T O R  /  D T O R                                                  //
//——————————————————————————————————————————————————————————————————————————//
public:
    AutomatonAbstract() { mSetEndianness() } // this is important.
    virtual ~AutomatonAbstract() {}


//——————————————————————————————————————————————————————————————————————————//
//     M A P P I N G   I N T E R F A C E                                    //
//——————————————————————————————————————————————————————————————————————————//
public:
    virtual void shrink() = 0;
    virtual void write(FILE * fp) const = 0;

// ...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
namespace sis {

/// @brief meta-class to handle StringType type traits (using static assertions)
template<class T>
struct ElementSize {
    static constexpr bool T_is_string_type =
                std::is_base_of< std::string, T>::value
             || std::is_base_of<std::wstring, T>::value ;

     static_assert(T_is_string_type,
         "\n\n\tYou must instantiate this class in question with std::string,"
         "\n\tstd::wstring, or any class derived thereof.\n"
     );


    typedef T                               type;
    typedef T                        string_type;
    typedef typename T::value_type    value_type;

    typedef typename std::conditional<
        std::is_same<std::string,T>::value, // if StringType is std::string
        std::fstream,                       // then stream_type is std::fstream
        std::wfstream                       // else it is std::wfstream
        >::type                             stream_type;

    static constexpr unsigned int value =
                            sizeof(typename string_type::value_type);
};

} /* sis */

3.6.1.1.5.1 Besprechung des Codes

Die Ziele, die dieser (C++11-)Code erreicht, sind mehrere:

  1. Es wird geprüft, ob der Template-Paramter template<class T>
    1. ein std::string,
    2. ein std::wstring
    3. oder eine von einer der beiden Klassen abgeleitete Klasse ist (std::is_base_of)
  2. Diese Überprüfung findet zur Compile-Zeit (!) statt (constexpr) und das Ergebnis wird in einem bool ::value abgespeichert

  3. Es werden sog. traits definiert, die wiederum an anderer Stelle abfragbar sind:
    1. ElementSize<mystring>::type würde den Typ mystring zurückgeben
    2. ElementSize<mystring>::string_type würde das selbe Ergebnis liefern
    3. ElementSize<mystring>::value_type würde den value_type der Klasse zurückgeben mit der ElementSize instantiiert ist; im Falle von std::string also char
  4. Der wichtigste traits jedoch ist static constexpr unsigned int value: an dieser Stelle wird, je nach Instantiierung ein numerischer Wert von 1 oder 4 zurückgegeben, nämlich die exakte Größe des value_types der Klasse template<class T>:

Bespiel:

  1. für ElementSize<std::string>::value der Wert 1, da der value_type von std::string ein char ist, und char (zwar plattform-abhängig) aber in den meisten Fällen die “Breite” 1 hat (also ein 1 Byte “breit” ist)
  2. für ElementSize<std::wstring>::value der Wert 4, da der value_type von std::wstring ein wchar_t ist, und wchar_t (zwar plattform-abhängig) aber in den meisten Fällen die “Breite” von 4 hat (also 4 Byte “breit” ist)

Passenderweise sind dies genau die Werte, die unser adaptierter Automatot CompressedAutomaton in seinem Konstruktor erwartet! Allerdings kann die Adapter-Klasse CompressedAutomatonAdapter nun nicht mehr “falsch” verwendet werden!

Nicht zuletzt ist noch das trait stream_type interessant:

Je nachdem, ob die Automaten mit std::string oder std::wstring instantiiert wurden, werden Ausgaben auf std::cout oder std::wcout gelenkt werden müssen. Um auch dieses “wegzuabstarhieren” wird – wiederum zur Compile-Zeit –, entschieden, was der passende Typ von Streams sein wird.

Hierfür bietet sich std::conditional aus dem Header <type_traits> an, das stream_type für eine Instantiierung mit std::string auf den stream_type-Typ auf std::fstream festlegt und mutatis mutandis das selbe für std::wstring erledigt.

Wenn nun eine Automaten-Klasse also Ausgaben vornehmen wird, so kann sie dies abstrakt erledigen, indem Sie – in etwa – folgendes tut:

#include "ElementSize.hpp"

template<class T>
class Foo {
  using stream_type = ElementSize<T>::stream_type;

  stream_type& operator<<(const T&);
}

Sind hiermit alle Anforderungen erfüllt?

  • Es ist schwerer geworden, die Klasse “falsch” zu benutzen, und leichter geworden, sie “richtig” zu benutzen
  • “Strong Typing” verhindert falsch gesetzte Parameter, da bei einem einfachen UINT oder unsigned int nicht klar ist, welche konkreten Werte an dieser Stelle erwartet werden.
  • Darüber hinaus sind die Klassen, die auf diese class ElementSize aufbauen, wesentlich flexibler, da sie gleichzeitig unabhänigig hinsichtlich des stream_types operieren können.

Letztendlich also bietet sich folgendes Szenario:

Der Nutzer verwendet eine Klasse CompressedAutomatonAdapter<std::string> und bekommt – zur Compile-Zeit – die für ihn “passende” (d.h. kleinstmögliche, platzsparendste, sicherste) Klasse “zusammengebaut” und hat dabei auch noch Sicherheit für die Verwendung gewonnen. Sollte er die Klasse “falsch” (d.h. mit einem “nicht-string”) instantiieren wollen, würde ihm dies beim kompilieren durch das static_assert inklusive einer Fehlermeldung mitgeteilt werden.

Zum Erlernen von Techniken zur Template Meta Programmierung, verwenden Sie bitte die Übungen, die im Kapitel TMP auf Seite angegeben werden.

3.6.1.1.6 Template Meta Programmierung: Member Dispatch

Eine weitere Technik, die innerhalb der SIS code base zu finden ist, ist ein Member Dispatching Pattern. Obwohl man das selbe Ziel auch über Template-Spezialisierung erreichen könnte, ist es dennoch ratsam, dieses Pattern zu kennen um es im Falle des Falles auch tatsächlich zu erkennen.

Wie mit allen Patterns, sehen diese am Anfang komplett unverständlich aus, kennt man sie aber, so sieht man sofort, was geschieht, ohne lange nachdenken zu müssen.

Das “Member Dispatch”-Pattern funktioniert so:

Angenommen, ich habe eine Template-Klasse Foo mit einer Methode bar(). Nun möchte ich komplett unterschiedliche Methoden bar() ausführen, je nachdem womit die Klasse Foo instanziiert ist – also z.B. als Foo<std::string> und Foo<int>.

Der member-dispatch funktioniert dann so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <type_traits>
#include <cassert>

// C++11 Proof of concept of different implementations of
// member methods for different types of class instances.

template<class T> struct tag {};

template<class T>
struct Foo {

    // obviously, you can play with where you actually put these attributes
    // (at least with gcc)
    [[ dispatched_to ]]
    std::string bar(tag<std::string>) {
        return std::string{"foo"};
    }

    int bar(tag<int>) [[ dispatched_to ]] {
        return 5;
    }

    auto bar() [[ dispatcher ]]
    ->decltype(this->bar(tag<T>{})) // 'this' seems to be needed here.
    {
        return bar(tag<T>{});
    }
};

3.6.1.1.6.1 Zur Funktionsweise des member-dispatch

Es gibt eine zentrale Methode auto bar() [[ dispatcher ]], welche dann einfach weiterreicht an die eigentlichen Methoden, int bar(tag<int>) [[ dispatched_to ]] oder [[ dispatched_to ]] std::string bar(tag<std::string>).

Das struct tag dient dabei lediglich einer “Markierung” und wird vom Optimizer im Laufe der Kompilierung entfernt.

3.6.1.1.7 Test Driven Development: TDD

Fast die gesamte Funktionalität der SIS-Komponente wurde mit dem Verfahren des TDD, Test Driven Developments entwickelt.

Mehr zum Thema TDD findet sich im Kapitel TDD – Test Driven Development ab Seite .

Kurz zur Struktur der Tests innerhalb von SIS:

  • Die tests verwenden die Bibliothek lest(Martin Moene 2013) für die tests.
  • Alle tests finden sich im Verzeichnis src/features
  • Jeder Klasse innerhalb von src/ sollte ein Test entsprechen:
    • Eine Klasse Document, z.B. in Document.hpp, sollte eine Fixture und ein Feature besitzen:
    • src/features/DocumentFixture.hpp und src/features/DocumentFeature.hpp

Zur tieferen Beschreibung von Fixtures und Features, siehe nochmal das Kapitel TDD – Test Driven Development ab Seite ; an dieser Stelle nur so viel:

  • Fixtures beschreiben sowohl die Input-Daten als auch die erwarteten Output-Daten
  • Features sind spezielle Test-Klassen/Dateien, welche die Spezifikation der Klasse beschreiben, auf die sie sich beziehen und die Fixtures verwenden um die Funktionalität der getesteten Klasse mit den erwarteten Ergebnissen abzugleichen
3.6.1.1.7.1 Beispiel

In SIS findet sich eine Klasse Document, mit den folgenden Fixtures/Features um die Spezifikation zu prüfen:

#ifndef DOCUMENTFIXTURE_HPP_8JMP5XLF
#define DOCUMENTFIXTURE_HPP_8JMP5XLF

#include "sis_config.hpp"

#include <vector>
#include <string>

typedef std::string file_t;                                                     /// @todo change this to fs::path

struct DocumentFixture
{
    std::vector<file_t> filenames = {
        sis_TEST_DATA_DIR"ascii/1.txt",
        sis_TEST_DATA_DIR"ascii/2.txt",
        sis_TEST_DATA_DIR"ascii/3.txt",
    };

    std::vector<file_t> wfilenames = {
        sis_TEST_DATA_DIR"utf8/1.txt",
        sis_TEST_DATA_DIR"utf8/2.txt",
        sis_TEST_DATA_DIR"utf8/3.txt",
    };

    std::vector<std::string> contents = {
        "Das Verstehen, die Meinung, faellt aus unsrer Betrachtung heraus.",
        "Also: Kann man Etwas anders, als als Satz verstehen?",
        "Oder aber: Ist es nicht erst ein Satz, wenn man es versteht."
    };

    std::vector<std::wstring> wcontents = {
        L"Das Verstehen, die Meinung, fällt aus unsrer Betrachtung heraus.",
        L"Also: Kann man Etwas anders, als als Satz verstehen? Ääääh...",
        L"Oder aber: Ist es nicht erßt ein Satz, wenn man es versteht."
    };
};

#endif /* end of include guard: DOCUMENTFIXTURE_HPP_8JMP5XLF */

#include "sis_config.hpp"

#include "testing/lest.hpp"
#include "DocumentFixture.hpp"

#include "indexer/Document.hpp"

#include <fstream>
#include <locale>
#include <cereal/types/vector.hpp>
#include <cereal/types/string.hpp>
#include <cereal/access.hpp> // For LoadAndAllocate
#include <cereal/types/memory.hpp>
#include <cereal/archives/binary.hpp>
#include <cereal/archives/json.hpp>
#include <cereal/archives/xml.hpp>

#include <cwchar>

DocumentFixture f;

const lest::test specification[] =
{
    "standard CTOR callable",[]
    {
        sis::Document<std::string> d;
        EXPECT( d.filename() == "" );
    },

    "CTOR with filename", []
    {
        sis::Document<std::string> d{ f.filenames.front() };
        EXPECT( d.filename() == f.filenames.front() );
    },

    "can serialize (automaton with added document)", []
    {
        sis::Document<std::string>  d{ f.filenames.front() };
        std::ofstream               ofs{ "document.bin" };
        cereal::BinaryOutputArchive oarchive{ ofs };

        oarchive( d ); // Write the data to the archive
    },

    "can deserialize (automaton with added document)", []
    {
        sis::Document<std::string>  d{ f.filenames.front() };
        std::ifstream               ifs{ "document.bin" };
        cereal::BinaryInputArchive  iarchive{ ifs };

        iarchive( d ); // Read the data
        EXPECT( d.filename() == f.filenames.front() );
        EXPECT( d.contents() == f.contents. front() );
    },

    "can serialize (automaton without added document)", []
    {
        sis::Document<std::string>  d;
        std::ofstream               ofs{ "document.bin" };
        cereal::BinaryOutputArchive oarchive{ ofs };

        oarchive( d ); // Write the data to the archive
    },

    "can deserialize (automaton without added document)", []
    {
        sis::Document<std::string>  d;
        std::ifstream               ifs{ "document.bin" };
        cereal::BinaryInputArchive  iarchive{ ifs };

        iarchive( d ); // Read the data
        EXPECT( d.filename() == "" );
        EXPECT( d.contents() == "" );
    },

    "can access content", []
    {
        sis::Document<std::string>  d{ f.filenames.front() };
        EXPECT( d.contents() == f.contents.front() );
    },

    "can access wide content", []
    {
        sis::Document<std::wstring>  d{ f.wfilenames.front() };

        /// Make sure to properly imbue!
        EXPECT( d.contents() == f.wcontents.front() );
    },

    "can access content (for default CTOR)", []
    {
        sis::Document<std::string>  d;
        EXPECT( d.contents() == "" );
    },

    "can access filename (for default CTOR)", []
    {
        sis::Document<std::string>  d;
        EXPECT( d.filename() == "" );
    },

    "can access filename (with given filename)", []
    {
        sis::Document<std::string>  d{ f.filenames.front() };
        EXPECT( d.filename() == f.filenames.front() );
    },

    "can serialize (automaton with added document) [as std::wstring]", []
    {
        sis::Document<std::wstring> d{ f.wfilenames.front() };
        std::ofstream               ofs{ "document.json.w" };
        cereal::JSONOutputArchive oarchive{ ofs };

        oarchive( d ); // Write the data to the archive
    },

    "can deserialize (automaton with added document) [as std::wstring]", []
    {
        sis::Document<std::wstring> d{ f.wfilenames.front() };
        std::ifstream               ifs{ "document.json.w" };
        cereal::JSONInputArchive  iarchive{ ifs };

        iarchive( d ); // Read the data

        std::wstring expected = L"Das Verstehen, die Meinung, fällt aus unsrer Betrachtung heraus.";

        /// Make sure to properly imbue!
        std::wcout << L"[" << d.contents() << L"]" << std::endl;
        std::wcout << L"[" << expected     << L"]" << std::endl;

        EXPECT( d.filename() == f.wfilenames.front() );
        EXPECT( d.contents() == expected );
    },

    "can serialize (automaton with added document) [as std::wstring]", []
    {
        sis::Document<std::wstring> d{ f.wfilenames.front() };
        std::ofstream               ofs{ "document.bin" };
        cereal::BinaryOutputArchive oarchive{ ofs };

        oarchive( d ); // Write the data to the archive
    },

    "can deserialize (automaton with added document) [as std::wstring]", []
    {
        sis::Document<std::wstring> d{ f.wfilenames.front() };
        std::ifstream               ifs{ "document.bin" };
        cereal::BinaryInputArchive  iarchive{ ifs };

        iarchive( d ); // Read the data

        std::wstring expected = {L"Das Verstehen, die Meinung, fällt aus unsrer Betrachtung heraus."};

        /// Make sure to properly imbue!
        // std::wcout << L"[" << d.contents() << L"]" << std::endl;
        // std::wcout << L"[" << expected     << L"]" << std::endl;

        EXPECT( d.filename() == f.wfilenames.front() );
        EXPECT( d.contents() == expected );
    },

};

int main() {
    std::setlocale(LC_ALL, "");
    return lest::run( specification );
}

3.6.1.1.8 Zweistufige Serialisierung

Das Thema Serialisierung wird allgemein beschrieben im Kapitel Serialisierung.

In diesem Kapitel wird die zweistufige Serialisierung innerhalb von SIS beschrieben.

Warum die Serialisierung innerhalb von SIS zweistufig läuft ist durch das Wrapping-Pattern zu erklären:

Da die C-Schicht ihre eigene Serialisierung hat, kann diese selbst angesprochen werden. Um die zusätzlichen Datenstrukturen aus der C++-Schicht zu serialisieren, ist jedoch noch weitere Serialisierung notwendig.

Hierbei spielen folgende Überlegungen eine Rolle:

  • Die Serialisierung der C-Schicht speichert und liest ein kompaktes Binärformat (mehr Informationen zu Binär-Serialiseirung und Byte-Layout in Kapitel Methoden zur Serialisierung)
  • Die Beibehaltung der Serialisierung der C-Schicht bietet sich aus 2 Gründen besonders an:
    • diese ist bereits implementiert und stellt ihre Funktionalität bereits zur Verfügung
    • Die entstehende Datei kann von der executable der C-Schicht – unabhängig von allem was innerhalb der C++-Schicht passiert –, gelesen und geschrieben werden. Eine Beibehaltung dieser Serialisierung garantiert also eine gewisse Übertragbarkeit der serialisierten Daten zwischen den Schichten
  • Trotz Beibehaltung der bestehenden Funktionalität zur Serialisierung der C-Schicht ist die Entwicklung einer eigenen Serialisierung der C++-Schicht notwendig.

Abgesehen von den generellen Überlegungen zur Serialisierung wie im Kapitel Methoden zur Serialisierung beschrieben, bieten sich zur Serialisierung in ein Binärformat vor allem folgende Optionen an:

  • boost serialization: eine Standardbibliothek für C++, kann (virtuelle und polymorphe) Klassenhierarchien speichern und laden, hat extras, die es erlauben raw pointer zu (de)serialisieren, die Dokumentation und die Verwendung sind allerdings nicht die einfachsten
  • cereal (uscilab 2014). Eine moderne, schlanke Bibliothek, die vor allem auf C++11 features der Template Meta Programmierung setzt. Die Dokumentation ist leichter zu verdauen als die von Boost Serialization und die API leichter zu bedienen und in vorhandene Strukturen einzubauen
  • cpp-generic-serialize (Daniel Bruder, Florian Fink 2014): eine kleine Bibliothek, die das Binär-Speichern und -laden für STL-Container erleichtert, ebenfalls umgesetzt durch die Nutzung von Template Meta Programmierung. Erlaubt vor allem das detaillierte heraus-schreiben und einlesen von einzelnen Elementen eines Containers, wenn nicht ganze Klassen-hierarchien serialisiert sondern nur einzelne Elemente oder Container einer Klasse serialiseirt werden sollen.

Im Falle von SIS fiel die Wahl auf cereal, da die Funktionalität den Bedürfnissen entspricht und die Verwendung als einfach bezeichnet werden kann.

Im besonderen Falle von SIS hinsichtlich des Wrappings und der Beibehaltung der Serialisierungs-Funktionalität der C-Schicht ist jedoch im besonderen zu beachten, dass die C++-Sicht beim Speichern die Serialisierung der C-Schicht anstoßen muss und beim Laden die Ergebnisse der C-Deserialisierung entsprechend berücksichtigen (und möglicherweise in die Klassenhierarchie entsprechend integrieren muss).

Hierzu stehen für einen generischen(!) template<AutomatonType> struct DocumentIndexingAutomaton<AutomatonType<StringType>> folgende Schnittstellen zur Verfügung:

template <class F = std::ofstream, class A = cereal::BinaryOutputArchive>
void save(std::string & fn /* fs::path & p */);

template <class F = std::ifstream, class A = cereal::BinaryInputArchive>
static
DocumentIndexingAutomaton<AutomatonType>
load(std::string & fn);

Mit diesen Methoden lassen sich alle Kombinationen der folgenden Dimensionen von DocumentIndexingAutomaton speichern und lesen:

Serialisierungsmöglichkeiten für DocumentIndexingAutomaton
Automaten StringType Formate Ziele
SCDAWGAdapter std::string JSON std::fstream,
CDAWGAdapter std::wstring XML d.h. std::cout,
CompressedAutomatonAdapter Binär std::ofstream (Datei), …

Um zu verdeutlichen, wie die (De-)Serialisierung und dabei das Anstoßen der Serialisierungsfunktionen der C-Schicht funktioniert, hier die Implementierung:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
namespace sis {

/*****************************************************************************/
template <class AutomatonType>
template <class FileType, class ArchiveType>
inline
void
DocumentIndexingAutomaton<AutomatonType>::
save(std::string & fn /* fs::path & p */)
{
    FileType        ofs{ fn };
    ArchiveType     oarchive{ ofs };

    index();
    automaton_->save(fn + ".c");     // Write automaton information to archive
    oarchive( *this );               // Write the document data to the archive
}


/*****************************************************************************/
template<class AutomatonType>
template<class FileType, class ArchiveType>
inline
DocumentIndexingAutomaton<AutomatonType>
DocumentIndexingAutomaton<AutomatonType>::
load(std::string & fn)
{
    FileType            ifs{ fn };
    ArchiveType         iarchive{ ifs };

    auto aut = Factory::create<AutomatonType>();
    aut->load(fn + ".c");

    DocumentIndexingAutomaton<AutomatonType> a;
    iarchive( a );
    // a.automaton_ = std::shared_ptr<AutomatonAbstract<typename AutomatonType::string_type>>(aut);
    a.automaton_ = aut;
    for (auto& index: *(a.index_)) {
        index.second.set_voidsequence(a.automaton()->data());
    }
    return std::move(a);
}

/* .... */

Zur Verdeutlichung der Kombinations-Möglichkeiten hier ein Auszug aus den dazugehörigen Tests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
"Can write to disk in JSON format to std::ofstream", []
{
    std::cout << "saving to json" << std::endl;
    refresh_indexer();
    idx->save<std::ofstream,cereal::JSONOutputArchive>(f.filename.json);
},

"Can read back from disk in JSON format from std::ofstream", []
{
    std::cout << "loading from json" << std::endl;
    auto index_load = sis::DocumentIndexingAutomaton<sis::SCDAWGAdapter<std::string>>::load<std::ifstream,cereal::JSONInputArchive>(f.filename.json);
},

"Can write to disk in binary format to std::ofstream", []
{
    std::cout << "saving to binary" << std::endl;
    refresh_indexer();
    idx->save<std::ofstream,cereal::BinaryOutputArchive>(f.filename.binary);
},

"Can read back from disk in binary format from std::ofstream", []
{
    std::cout << "loading from binary" << std::endl;
    auto index_load = sis::DocumentIndexingAutomaton<sis::SCDAWGAdapter<std::string>>::load<std::ifstream,cereal::BinaryInputArchive>(f.filename.binary);
    // assert(aut.automaton_ != nullptr);
},

"Can write to disk in binary format to std::ofstream (with default template arguments)", []
{
    refresh_indexer();
    idx->save<>(f.filename.binary);
},

"Can read back from disk in binary format from std::ofstream (with default template arguments)", []
{
    auto aut = sis::DocumentIndexingAutomaton<sis::SCDAWGAdapter<std::string>>::load<>(f.filename.binary);
    // assert(aut.automaton_ != nullptr);
},

"Can write to disk in binary format to std::ofstream (with default template arguments -- and without using `<>')", []
{
    refresh_indexer();
    idx->save(f.filename.binary);
},

/* ... */
3.6.1.1.8.1 Die API von cereal

Um ein Beispiel von der API von cereal zu geben, hier ein Ausschnitt aus DocumentIndexingAutomaton.

Dabei geht es im wesentlichen darum, eine member-Methode template <class Archive> void serialize( Archive & archive ) für die Klasse zu definieren und dieser mitzuteilen, welche member-Variablen serialisiert werden sollen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

template<class AutomatonType>
struct DocumentIndexingAutomaton
{

    /* .... typedefs für die member-Typen AutomatonAbstract_t, etc ...*/

    AutomatonAbstract_t                     automaton_ = nullptr;
    std::unique_ptr<DocumentCollection_t>   documents_ = nullptr;
    std::unique_ptr<DocumentIndex_t>        index_     = nullptr;
    std::map<UINT, std::vector<Document_t>> states_    = nullptr;
    FindResult_t                            results_   = nullptr;
    size_t                                  total_length_ = { 0 };
    bool                                    indexed_   = false;
    string_type                             query_;

    template <class Archive> void serialize( Archive & archive ) {
        archive(
                CEREAL_NVP(documents_),
                CEREAL_NVP(index_),
                CEREAL_NVP(states_),
                CEREAL_NVP(indexed_)
        );
    }

Hinzuweisen ist noch auf CEREAL_NVP: hiermit wird festgelegt, dass, wenn nach json oder XML serialisiert wird, die Daten in der Serialisierung benannt werden (NVP = named value pair).

3.6.2 Web-Ebene

In diesem Kapitel werden die wesentlichen Bausteine der Web-Ebene von SIS besprochen. Der Web-Teil ist im wesentlichen eine SPA (Singe Page App) und baut hierbei weitestgehend auf den sogenannten MEAN-Stack15 auf, lässt aber u.a. die Datenbank weg, da die Daten von den Automaten in SIS geliefert werden.

3.6.2.1 Wesentliche Technologien und ihr Zweck

  • Node: stellt den (eigenen) Webserver dar
  • Express: Ein Web-Framework für Node
  • Angular: Client-seitige Logik
  • Jade: HTML-Templating Engine
  • Bootstrap: Client-seitiges CSS und mehr
  • Bootstrap UI: Client-seitige UI-Module (Dropdowns, etc.)
  • v8: Javascript-Engine in C++

Für Detail-Besprechungen dieser Technologien siehe auch das eigene Buch “Teilbesprechungen”.

3.6.2.2 Überblick

Wie im Kapitel Wesentliche Technologien und ihr Zweck angedeutet handelt es sich bei der Web-Schicht um eine SPA in Form des MEAN-Stack.

Dabei spielt der Node-Webserver eine wesentliche Rolle. Mit Express, dem Web-Framework für node lassen sich routen definieren und Antworten an den Client zurücksenden (vergleiche hierzu auf Seite ).

Das Zusammenspiel von Node und den C++-Automaten von SIS funktioniert nun auf folgende Art und Weise:

Im Ordner web/ befindet sich eine weitere Schicht, welche die nun in C++ vorliegenden Automaten derart wrapped, dass diese direkt in Javascript verwendet werden können:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## --[ app.coffee ]-- ##

# import
indexer = require './build/Release/sis'
log.info('index imported', {})

# deserialize
indexerObj = indexer.load '../etc/allfiles'
log.info('index loaded', {})

# size
size = indexerObj.size()
log.info('index size', {size: size})


## --[ query.coffee ]-- ##
# query
found = app.get('indexer').find(query, width)

Konkret bedeutet dies, dass das C++-Objekt an dieser Stelle in Javascript nutzbar gemacht worden ist, und zwar als “node-addon” über v8!

Die Erklärung, wie dies bewerkstelligt werden kann, folgt in diesem Kapitel.

3.6.2.3 Zweck des Wrapping

Selbstverständlich stellt sich die Frage, warum überhaupt man C++-Objekte so wrappen sollte, dass man mit diesen in javascript arbeiten kann. Die Antwort lautet

  1. weil man es kann!
  2. weil es Vorteile bei der Geschwindigkeit liefert:

Die Überlegung ist, nur 1x zu deserialisieren.

3.6.2.4 Installation, Kompilierung und Benutzung der Web-Schicht

Zunächst einmal aber zum Lauffähig-machen der Web-Componente (auch hier gilt: bitte vergleichen Sie auch die einschlägigen Kapitel in [Teilbesprechungen][]).

Im Normalfall sollte der Kompilierungs- und Installationsprozess für die Web-Komponente durch den cmake-Workflow automatisch übernommen werden, dennoch an dieser Stelle ein paar Hinweise im Speziellen und zum Vorgang allgemein.

Der cmake-Workflow bildet die Web-Komponente (aus Zeitgründen) nicht (!) per default und muss gesondert aktiviert werden:

17
18
19
20
21
if($ENV{sis_BUILD_WEB_COMPONENT})
    set(sis_BUILD_WEB_COMPONENT      ON)
else()
    set(sis_BUILD_WEB_COMPONENT     OFF)
endif()

Das heisst, es gibt zwei Möglichkeiten, den Build der Web-Komponente anzustossen:

  1. den default in der obersten CMakeLists.txt-Datei auszutauschen, oder
  2. eine environment-Variable auf ON zu setzen, womit der “volle” cmake-Befehl folgendermaßen aussieht:

    sis/build> SIS_BUILD_WEB_COMPONENT=ON cmake ..

(Der cmake-Befehl wird entsprechend eine Ausgabe liefern, die bestätigt oder verneint, ob die Web-Komponente gebaut wird):

[...]
-- [WEB COMPONENT]
--   Will NOT build web component
[...]

bzw.

[...]
-- [WEB COMPONENT]
--   Will build web component
[...]

Die Teile der Build-Kette für die Web-Komponente, die cmake automatisch anstößt, umfassen:

  • das Installieren der server-seitigen Dependencies
  • das Installieren der client-seitigen Dependencies
  • das Builden des node addon
3.6.2.4.1 server-seitige Dependencies installieren

die server-seitigen Dependencies werden in node immer mit dem node package manager npm installiert. Dieser liest die Datei package.json im obersten Verzeichnis aus und installiert die dort aufgeführten Dependencies (und rekursiv deren Sub-Dependencies).

sis/web> npm install

Hinweis: npm installiert seine projekt-spezifischen Pakete nach node_modules/.

3.6.2.4.2 client-seitige Dependencies installieren

Die client-seitigen Dependencies werden mit bower installiert. bower kümmert sich ebenfalls um das Auflösen von Abhängigkeiten von Javascript-Paketen – nur eben, im Gegensatz zu npmˆ–, für die Client-Seite, also den Browser.

sis/web> bower install

Hinweis: bower installiert seine projekt-spezifischen Pakete nach bower_components/ oder app/bower_components/

Vergleiche auch noch einmal die Beschreibung der Projektstruktur im Kapitel Projektstruktur!

3.6.2.4.3 node addon builden

Nicht zuletzt muss noch das node addon kompiliert werden, dass die C++-Objekte in Javascript zur Verfügung stellt. Dafür wird klassischerweise node-gyp16 als Build-Tool verwendet.

node-gyp liest die Datei binding.gyp aus und orientiert daran seinen build-Prozess.

node-gyp umfasst wiederum mehrere sub-befehle, im wesentlichen aber reicht ein…

web/sis> node-gyp rebuild

… um (implizit) ein node-gyp build anzustossen.

3.6.2.4.3.1 binding.gyp

Eine minimale binding.gyp-Datei sieht folgendermaßen aus:

{
  "targets": [
    {
      "target_name": "hello",
      "sources": [ "hello.cc" ]
    }
  ]
}

An dieser Stelle sollen die wichtigsten Ausschnitte der binding.gyp von SIS in aller Kürze besprochen werden:

{
    'targets': [
    {
        'target_name': 'sis',
        'sources': [
            'cpp/sis.cpp',
            'cpp/DocumentIndexingAutomaton.cpp'
        ],
        'include_dirs': [
            '/usr/include',
            '<!@(echo `pwd`/../src/)',
            '<!@(echo `pwd`/../vendor)',
            '<!@(echo `pwd`/../vendor/serialization/include)',
            '<!@(echo `pwd`/../vendor/make_unique)',
            '<!@(echo `pwd`/../vendor/pugixml/src)'

        ],
        # global flags go here
        # `cflags_cc!` is supposed to mean: remove these flags
        'cflags_cc!': [ '-fno-rtti' ], # '-fno-exceptions'
        # `cflags_cc` is supposed to mean: add these flags
        'cflags_cc':  [ '-fPIC', '-std=c++11', '-fexceptions', '-Wall', '-Wextra', '-Wno-attributes', '-Wno-pointer-arith' ],
        'conditions': [
    /* [......] */ 

Zu beachten sind folgende Punkte:

  • include_dirs verlangt vollständige Pfade, daher der “Hack” über die commandline
  • cflags_cc! nimmt vorhandene Compiler-Flags weg
  • cflags_cc fügt Compiler-Flags an die bereits vorhandenen hinzu
3.6.2.4.3.2 Hinweise
  • Hinweis für Server omega bzw. 64-bit-Systeme: Omega hat 2 verschiedene Verzeichnisse, in denen libs liegen können: /usr/lib und /usr/lib64! Im Falle von fehlenden shared-objects auch dort suchen. Beim Build-Prozess

  • Hinweis für spezielle Compiler: um SIS kompilieren zu können ist ein GNU gcc Compiler >= 4.7.0 nötig. Ein solcher ist zwar möglicherweise installiert, aber mit einem Namenssuffix versehen, d.h. liegt als g++-4.8 oder ähnlich vor. Daher unbedingt den Standard-Compiler des akutellen Systems genauer unter die Lupe nehmen:
    1. which gcc; gcc --version;
    2. einen anderen compiler suchen – dabei die “einschlägigen” Pfade durchsuchen, z.B.: ls /usr/bin/g++*
    3. Diesen speziellen compiler nötigenfalls noch node-gyp “unterzuschmuggeln”: um den Compiler zu ermitteln, liest node-gyp die ENV-Variable CXX aus. Diese lässt sich am besten gleich auf den vollen Pfad von gcc setzen, der Befehl lautet also ungefähr:

    CXX=which g++-4.8 node-gyp rebuild

  • Manchmal kann es vorkommen, dass beim Start des node Servers die shared objects aus dem node wrapping nicht gefunden werden; da hilft es die Variable LD_LIBRARY_PATH entsprechend anzupassen: LD_LIBRARY_PATH=/srv/www/sis/sis3/build/lib:$LD_LIBRARY_PATH coffee app.coffee

3.6.2.4.4 Web Server starten

um den Web-Server zu starten, reicht es mit node die entsprechende javascript-Datei zu starten.

Da für SIS coffeescript statt javascript verwendet wird, stattdessen den server also mit dem Befehl coffee statt node starten: coffee app.coffee.

3.6.2.4.5 Deploy

Mit den o.g. Hinweisen im Gepäck lässt sich die Applikation nun auch auf den Server deployen (beachte hierzu bitte auch das Kapitel[Der ausführende user wastd][]).

Um im Falle eines Crashs den Web-Server sofort wieder neu zu starten (speziell dieses Addon mit seiner zugrundliegenden C-Schichit ist sehr gefährdet für SEGFAULTs…), lässt sich das node-module forever17 verwenden. Siehe die Datei web/wast.start für nähere Details.

3.6.3 Node Addon

3.6.4 Nächste Schritte

  • Alternativ zu einem Node-Addon gibt es die Möglichkeit, durch die Verwendung von emscripten (Emscripten Authors 2013), C++ nach Javascript zu kompilieren. Allerdings müssten dabei die kompletten Index-Dateien in den Browser des Client übertragen werden, weshalb hier nur der Hinweis auf emscripten gegeben werden soll.

4 feedback

Die Applikation Feedback18 dient als Web-Front-end für das reporting von bugs, feature requests und aller anderen Dinge, die relevant sind für den Bug tracker und die dahinterliegenden Konzepte der “Bug Tracking Best Practices”.

Bugs und feature requests submitten zu können kann interessant sein für

Als Web-Applikation ist Feedback konzipiert, da generell jeder, der einen Bug im Bug Tracker eintragen möchte, eine Registrierung auf der Web-Oberfläche des gitlab19 benötigen würde, dieses aber eine zu hohe Hürde darstellt und Bug Reporter aufgrund des Aufwands davon abhalten könnte, wichtige Informationen bereitzustellen.

Darüber hinaus garantiert Feedback eine geordenete Weise, in der Bugs abgelegt werden, indem es bugs automatisch nach Komponenten und Maintainern sortiert (durch tags) direkt in die Bug-Liste einträgt.

4.1 Aufbau

Der Aufbau von Feedback gestaltet sich in Form ein MEAN-Stack Web-Applikation, welche das Frontend und Backend bildet.

Im Backend wird im wesentlichen die API des Gitlab genutzt, um Bugs eintzutragen.

Um mit der API von Gitlab sprechen zu können bedarf eines eines sogenannten “Private Token” zur Authentifizierung. Hierfür wurde ein gesonderter User (“FeedbackBot”) angelegt, dessen Token verwendet wird, um Bugs zu submitten (dieses geheimzuhaltende Token wird auf der Serverseite bereitgehalten und nicht in das Frontend, zum Client übertragen). Um innerhalb von Wittgenstein-Issues20 bugs submitten zu können, muss FeedbackBot selbstverständlich als Member im Projekt vorhanden sein.

4.1.1 Dateistruktur

.
|-- Makefile                      # targets zum starten, stoppen, restart
|-- README.md
|-- bower.json                    # ini-file für client-seitige dependencies
|-- bower_components              # Ordner für client-seitige dependencies
|-- database                      # MongoDB-Anbindung
|-- node_modules                  # Ordner für Server-seitige dependencies
|-- npm-shrinkwrap.json           # ini-file für server-seitige dependencies (unwichtig)
|-- package.json                  # ini-file für server-seitige dependencies (wichtig)
|-- public                        # client-seitiger Applikationsteil: Application-Logic (AngularJS)
|-- routes.js                     # Server-seitige Routes
|-- views                         # client-seitiger Teil: Views (Jade)
`-- wast-feedback-server.js       # Der Web-Server selbst (node)

4.2 Application-Logic

Der operationale Ablauf der Web-Applikation gestaltet sich folgendermaßen:

  1. User geht auf
  2. Er ist noch nicht eingeloggt (wittgenstein-issues soll nicht von Bots gespammed werden)
  3. User logged sich ein (CIS/lmu)
  4. User füllt Formular aus:
    1. Das Formular wird permanent auf Vollständigkeit geprüft (ansonsten wird live eine Warnung/Hilfe angezeigt)
    2. User beschreibt seinen Bug genau (nach den Regeln des WAST-Workflow: wie/wann/wo erscheint dieser Fehler, wie kann man ihn reproduzieren, etc.) und hinterlässt seine email-Adresse: dadurch kann er am Ende eines resolveten Bugs verify-en ob der Bug in seinem Sinne gelöst wurde
  5. User schickt Formular ab:
    1. ein POST-Request auf eine bestimmte Server-Route wird mit den Daten des Formulars getätigt
    2. Der Web-Server hat eine bestimmte Logik implementiert, was passieren soll, wenn er einen POST-Request auf dieser Route entgegennimmt:
    3. Der Web-Server nimmt den privaten Token des FeedbackBot und schickt mit einem curl-Request eine Bug submission an die API von gitlab.
    4. Der Server sagt der Client-Seite, ob er erfolgreich mit der gitlab-API sprechen konnte
    5. Die Client-Seite gibt dem User eine Rückmeldung, ob der Bug erfolgreich eingetragen werden konnte.
  6. Dem User steht es frei, einen weiteren Bug-Report zu senden (also zurück zu 1.) oder fertig zu sein.

Hinweis: Für eine genauere Beschreibung von Client-Seite/Server-Seite siehe das Kapitel Web-Applikationen, für eine Beschreibung der einzelnen Komponenten siehe das Kapitel MEAN-Stack und für eine Beschreibung der Applikations-Logik hinsichtlich Controller, View, etc., vergleiche das Kapitel Design Patterns.


5 wab2cis

Verwendung von TEI, [siehe auch Sonderkapitel TEI]

5.1 Einbindung als git-submodule

Zur Verwendung von git, siehe Kapitel git submodules.

6 WIndex

7 Patrick’s Teil: Ausformulierung von Alternativen


8 gitlab

Im Rahmen der Entwicklung von WAST spielt das selbst-gehostete gitlab des CIS unter eine ganz entscheidende Rolle.

Zu den wesentlichen Aufgaben, die über gitlab laufen, zählt:

9 Continuous Integration

9.1 Definition

Unter Continuous Integration (CI) versteht man:

Continuous Integration is a software development practice where members of a team integrate their work frequently, usually each person integrates at least daily - leading to multiple integrations per day. Each integration is verified by an automated build (including test) to detect integration errors as quickly as possible. Many teams find that this approach leads to significantly reduced integration problems and allows a team to develop cohesive software more rapidly. This article is a quick overview of Continuous Integration summarizing the technique and its current usage.21

auf Seite verdeutlicht den Workflow einer CI.

Continuous Integration

Continuous Integration

9.2 CI@CIS

Die Webseite für die CI am CIS findet sich unter https://gitlabci.cis.lmu.de/22.

Innerhalb der CI lassen sich dann Projekte, die im gitlab verfügbar sind, zu automatisierten Tests hinzufügen.

Dafür gibt man im wesentlichen an, wie die Software zu testen ist und erhält dann nach jedem push von Änderungen in sein Repository automatisch dieses getestet.

Damit ergibt sich eine gute Rückversicherung – und nicht zuletzt eine gut nach außen hin sichtbare From, dass sämtliche Software sich in einem “ship-baren”, also gesunden Zustand befindet. Das schafft Vertrauen und ist auch innerhalb des Teams höchst praktisch.

Eine ausführliche Anleitung wie die CI zu benutzen ist findet man hier: https://gitlab.cis.lmu.de/schweter/cis-ci/tree/master (erreichbar über https://gitlab.cis.lmu.de/public).

Der Maintainer der CI ist Stefan Schweter (schweter@cip.ifi.lmu.de) und die CI umfasst derzeit etwa 4 virtuelle Maschinen (ein bunter Mix aus Debian, Ubuntu, Suse, etc.) welche zufällig ausgewählt werden und die Tests ausführen. Damit wird sogar noch ein bisschen “Smoke-Testing” verhalten emuliert und sichergestellt, dass die innerhalb von WAST entwickelte Software auch auf anderen Betriebssystem installierbar ist und die Tests bestehen.

9.3 Referenzen

Das Thema CI hängt eng zusammen mit den Themen:

10 Build Systeme

Makefiles und das dazugehörige Programm make werden in der Software-Entwicklung seit jeher als Build-Systeme verwendet.

Build-Systeme beschreiben jeden einzlnen Schritt eines Build-Prozesses und führen diese auf intelligente Art und Weise so aus, dass möglichst wenige Schritte ausreichen, um einen funktionierenden build zu garantieren.

Da Build-Prozesse bei größeren Projekten einige Zeit beanspruchen können (mehrere Stunden sind hier nicht ausgeschlossen) und es beim debuggen sehr unpraktisch wäre, nach jeder Änderung einen kompletten Build-Prozess abzuwarten, macht man sich an dieser Stelle sowohl Eigenschaften von C und C++ (Object-Files, Trennung von Header und Implementation in unterschiedliche Dateien, shared und static libs, …) als auch das intelligente Vorgehen von Makefiles zu Nutze, um die entsprechenden Warte-Zeiten abzukürzen.

Makefiles bilden stets einen Pfad von Abhängigkeiten, welche schrittweise aufzulösen/abzuarbeiten sind, um zum nächsten Ziel kommen zu können.

Dabei dienen die sog. targets als die Ziele und die Definition dieser targets als die zu absolvierenden Schritte (häufig “recipes” genannt).

Hinzu kommt, dass sich die Bearbeitungszeiten der Dateien nutzen lassen, um festszustellen, ob – zum Beispiel – ein Object-File neu gebildet werden musss, oder nicht.

10.1 make

make ist, wie bereits erwähnt, der Platzhirsch und der Standard unter den Build-Systemen. Allerdings können Makefiles schnell komplex und unübersichtlich werden, weshalb man sog. “Makefile-Generatoren”, wie z.B. cmake oder gyp verwendet, um sich die entsprechenden Makefiles generieren zu lassen und diese nicht selbst schreiben und verwalten zu müssen.

Bis zu einer gewissen Größe und für eine breite Palette von Aufgaben lassen sich Makefiles ganz hervorragend und gewinnbringend einsetzen – nicht zuletzt, da sie auf so gut wie jedem System vorhanden sind.23

Eine Einführung zu GNU make findet sich unter https://www.gnu.org/software/make/manual/make.pdf; vergleiche u.a. auch das Makefile zum Erstellen dieser Dokumentation.

10.2 cmake

cmake24 ist ein Betriebssystem-unabhängiger Makefile-Generator für C- und C++-Projekte. Das heisst, man benutzt cmake, um ein Makefile zu generieren. cmake kann als “Industriestandard” für C- und C++-Projekte betrachtet werden.

Der Workflow mit cmake funktioniert also folgendermaßen:

cmake DIR
make

Dazu verteilt man (mindestens eine) CMakeLists.txt-Datei (potentiell rekursiv) im Projekt, welche die Deklarationen zu Targets, Libraries, etc. enthält und teilt cmake durch die Angabe DIR mit, wo sich diese Datei befindet. Danach wird ein Makefile zur Verfügung stehen, welches man mit make aufrufen kann.

10.2.1 Motivation

cmake zu verwenden bringt mehrere Vorteile:

  1. die Syntax von cmake ist einfacher als die von “klassischen” Makefiles
  2. dadurch, dass die CMakeLists.txt-Dateien rekursiv aufeinander aufbauen und jeweils an geeigneten Stellen im Projekt untergebracht werden, bleiben diese stets übersichtlich

10.2.2 vs. autotools (autotool-hell)

cmake wird vor allem deshalb gerne verwendet, weil der “klassische” autotools-Weg sehr umständlich, fehleranfällig und schlicht “altbacken” sein kann – siehe hierzu den Ablaufgraph “Autotools”, welcher den autotools-Weg nachzeichnet.

Autotools

Autotools

autotools fasst wiederum den bis dahin verwendeten Weg und die dazu benötigten tools zusammen, die benutzt wurden um Makefiles generieren zu lassen. Den autotools-Weg erkennt man immer daran, dass eine Software den “klasssischen Dreischritt” ./configure && make && make install verwendet.

10.2.3 out-of-source-builds

Klassischerweise verwendet man bei cmake in sog. “out-of-source builds”. Einfach gesagt, heisst dies nur, dass man seinen Build nicht direkt in der source macht, sondern dafür ein eigenes Verzeichnis (meist build/) anlegt, und fortan von dort arbeitet.

Damit vermeidet man das “Vermüllen” des source-directories mit Object-Files und dergleichen und hält so die Quellen stets sauber.

Der Workflow ist also:

~/project/foo > mkdir -p build && cd $_   # Hier liegt u.a. src/ mit den Quellen und (mindestens) einer CMakeLists.txt
~/project/foo/build > cmake ..            # `..` ist übergeordnetes Verzeichnis!
~/project/foo/build > make                # build-files werden out-of-source angelegt!

10.2.4 Wichtige optionen für cmake

Die beiden witchtigsten Optionen für cmake sind:

  • -DCMAKE_CXX_COMPILER=/opt/local/bin/g++ # Verwende speziellen Compiler
  • -DCMAKE_BUILD_TYPE=Debug # Build type festlegen (andere Switches für den Compiler)

Diese gibt man cmake mit, das dann entsprechend die Makefiles generiert:

cmake -DCMAKE_CXX_COMPILER=/Users/dbruder/bin/g++ -DCMAKE_BUILD_TYPE=Debug ..

10.2.5 Beispiel

Hier ein Auszug aus den CMakeLists.txt-Dateien des Projekts sis:

project(sis)
cmake_minimum_required(VERSION 2.8.3)
set(sis_AUTHORS "\"Daniel Bruder <bruder@cip.ifi.lmu.de>\"")

set(sis_NAME     "\"SIS\"")
set(sis_SUBNAME  "\"Symmetric Index Structures\"")
set(sis_FULLNAME "\"SIS -- Symmetric Index Structures\"")

# version numbers: lassen sich gut in doxygen etc verwenden
set(sis_VERSION_MAJOR 3)
set(sis_VERSION_MINOR 8)
set(sis_VERSION_PATCH 0)

set(CMAKE_INSTALL_PREFIX            /usr/local)
set(FIND_LIBRARY_USE_LIB64_PATHS)

# diese Variablen können auf der Commandline mit -D gesetzt werden
set(sis_BUILD_TESTS                  ON)
set(sis_TEST_STATIC_ASSERTIONS        0) # 0 == 'OFF' / 0 == 'ON'
set(GIT_SUBMODULES_CHECKOUT_QUIET    ON)

set(CONF_DIR ${PROJECT_SOURCE_DIR}/cmake)

include(${CONF_DIR}/config.cmake)
include(${CONF_DIR}/compilerdetect.cmake)
include(${CONF_DIR}/processorcount.cmake)
include(${CONF_DIR}/buildflags.cmake)
include(${CONF_DIR}/doxygen.cmake)
include(${CONF_DIR}/boost.cmake)
include(${CONF_DIR}/ctest.cmake)
include(${CONF_DIR}/cpack.cmake)
include(${CONF_DIR}/host.cmake)
include(${CONF_DIR}/checklocale.cmake)
include(${CONF_DIR}/submodules.cmake)
include(${CONF_DIR}/githooks.cmake)
include(${CONF_DIR}/nodewrap.cmake)

add_subdirectory(src/)
add_subdirectory(src/features)
add_subdirectory(vendor/)
add_subdirectory(web/)

Im Verzeichnis src/ findet sich folgendes cmake-file:

# Diese Verzeichnisse mit -I an den Compiler als Suchpfade mitgeben
include_directories(${CMAKE_SOURCE_DIR}/src/)

# Diese Verzeichnisse mit -isystem (oder nach OS abhängig) an den Compiler als Suchpfade mitgeben. -isystem bindet "externe" Header so ein, dass diese keine Warnings werfen.
include_directories(SYSTEM
    ${CMAKE_SOURCE_DIR}/vendor/
    ${CMAKE_SOURCE_DIR}/vendor/cautomata
    ${CMAKE_SOURCE_DIR}/vendor/make_unique
    ${CMAKE_SOURCE_DIR}/vendor/serialization/include
)

# Shared lib bauen
add_library(cppautomataadapter SHARED
    adapter/CompressedAutomatonAdapter.cpp
    adapter/SCDAWGAdapter.cpp
    adapter/VoidSequenceAdapter.cpp
)
target_link_libraries(cppautomataadapter cautomata)


add_library(cppindexer SHARED
    indexer/DocumentIndexingAutomaton.cpp
)
target_link_libraries(cppindexer cppautomataadapter cautomata)

add_executable       (sis main.cpp)
target_link_libraries(sis cppautomataadapter cppindexer cautomata ${Boost_LIBRARIES})

10.3 rake

rake25 ist ein kleines aber feines Build-Tool, das hier als eine Alternative zu make kurz vorgestellt werden soll. rake ist und wird in Ruby geschrieben und ist auf den meisten Systemen, die ruby mitliefern, enthalten.

rake ist aber nur eine von sehr vielen Alternativen zu make! Es gibt viele weitere, ähnliche Tools, doch nur eines, nämlich make kann als echter “Standard” angesehen werden. Nicht zuletzt aber lohnt sich ein Blick auf make: unter Umständen lässt sich make durch diesen kleinen Umweg schneller und einfacher verstehen…

Rake ist – im Gegensatz zu cmake – aber kein Makefile-Generator und kann folglich (leider) keine makefiles generieren!

10.4 gyp

gyp (Generate Your Projects)26 ist ein Makefile-Generator, der von Google für den Build-Prozess von Chrome entwickelt wurde. Er wird des weiteren auch für den build von Node.js verwendet (was an anderer Stelle wichtig wird – hier werden wir gyp wieder begegnen). gyp ist also ein build-automation tool ähnlich zu cmake, gegenüber cmake aber werden die Targets in gyp-files als JSON-Struktur beschrieben – hier ein Beispiel für ein gyp-file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
  'targets': [
    {
      'target_name': 'foo',
      'type': 'executable',
      'msvs_guid': '5ECEC9E5-8F23-47B6-93E0-C3B328B3BE65',
      'dependencies': [
        'xyzzy',
        '../bar/bar.gyp:bar',
      ],
      'defines': [
        'DEFINE_FOO',
        'DEFINE_A_VALUE=value',
      ],
      'include_dirs': [
        '..',
      ],
      'sources': [
        'file1.cc',
        'file2.cc',
      ],
      'conditions': [
        ['OS=="linux"', {
          'defines': [
            'LINUX_DEFINE',
          ],
          'include_dirs': [
            'include/linux',
          ],
        }],
        ['OS=="win"', {
          'defines': [
            'WINDOWS_SPECIFIC_DEFINE',
          ],
        }, { # OS != "win",
          'defines': [
            'NON_WINDOWS_DEFINE',
          ],
        }]
      ],
    },
  ],
}

11 Web

In diesem Kapitel werden die wesentlichen Züge von Web-Applikationen vorgestellt (dazu zählen Client-Server-Konfigurationen und Kommunikation), und am Beispiel des [MEAN-Stack][Der MEAN-Stack] ein konkretes, sehr modernes Framework zum Erstellen von SPAs (Single Page Applications), bzw. Rich Client Web Applications vorgestellt. Wesentlich bei diesen ist das dynamische Laden von Seiten-Bestandteilen, statt dem kompletten Reload der gesamten Seite.

11.1 Web-Applikationen

Moderne Web-Applikationen funktionieren im Allgemeinen nach wenigen, wesentlichen Prinzipien. In welcher Art und Weise diese konkret ausgestaltet werden, ist eine andere Frage – die Basics jedoch bleiben die selben.

11.1.1 Schematische Darstellung

Web Apps Basics

Web Apps Basics

11.1.1.1 Ablauf der Aktionen

  • Anfrage (1) vom Client (B) an den Server (A)
  • Anfrage (2) des Servers (A) an die Datenbank (C)

11.1.1.2 Aktionsarten

Die genannten Aktionen können unterschiedlicher Art sein:

Anfrage (1) kann – nach dem HTTP-Protokoll unterschiedliche Methoden haben:

  • GET
  • POST
  • PUT
  • UPDATE
  • DELETE

Die Anfragen vom Typ (1) vom Client (B) an den Server (A) richten sich immer nach einer bestimmten Routen.

Der Server bestimmt, wie seine API gestaltet ist, und reagiert entsprechend auf Anfragen an bestimmte Routen.

Anfragen können sein:

Der Server (A) kann auf Anfrage (1) im Schritt (4) mit unterschiedlichen Typen von Ressourcen antworten (selbstverständlich wird (B) diesen “richtig” anfragen, bzw. genau diese Ergebnisse erwarten):

  • GET-Anfrage nach einer statischen HTML-Seite auf einem bestimmten Pfad, server kann diese direkt ausliefern
  • GET-Anfrage nach einer Ressource (möglicherweise von einer Datenbank, oder aus anderen Quellen), server kann JSON liefern
  • POST auf eine bestimmte Route: Server soll die im POST gesendeten Daten irgendwo hinpacken

DER HTTP-Standard unterstützt theoretisch noch mehr Anfrage-Methoden, allerdings sind diese de facto aber kaum bis gar nicht umgesetzt (die nicht umgesetzten Methoden lassen sich gut GET und POST emulieren)

Die Schritte (3) und (4) sind optional. Der Typ der Datenbank ist auch nicht festgelegt und es gibt im wesentlichen 2 Typen von Datenbanken:

  • Relationale (SQL-Datenbanken)
  • Nicht-Relationale (NOSQL-Datenbanken), Dokumentorientert, meist eigentlich sehr JSON-artig

In Schritt (5) sendet der Server seine Antwort auf Anfrage (1), diese kann, wie gesehen, eine HTML-Seite sein, JSON, oder etwas anderes. An diesem Punkt ist es Aufgabe des Client, diese Antwort entsprechend zu verarbeiten.

11.1.2 Unterschiedliche Kombinationsmöglichkeiten

Hier eine kleine Liste, was an welchen Stellen möglicherweise vorkommen könnte – diese Liste ist bei weitem nicht exhaustiv!

11.1.2.1 Server

Als Server können unterschiedliche Software-Komponenten in Betracht kommen. Ein Server liefert im wesentlichen Daten aus (er “serve-d”):

  • Apache
  • IIS
  • nginx
  • lighttpd
  • unicorn
  • Node

11.1.2.2 Client

  • Browser: Firefox, Chrome, Chromium, Safari, IE, …
  • curl, wget, …

11.1.2.3 Datenbank

Wie bereits erwähnt gibt es unterschiedliche Typen von Datenbanken und im wesentlichen SQL-orientierte und NO-SQL-orientierte:

  • SQL-orientierte Datenbanken
    • MySQL
    • PostgreSQL
  • NoSQL-orientierte DB:
    • MongoDB
    • CouchDB
    • Redis
  • Graph-orientierte Datenbanken:
    • Neo4j
    • viele andere…

11.2 MEAN-Stack

Der MEAN-Stack ist ein Framework für die Entwicklung von modernen Webseiten – ähnlich wie das bekanntere “Ruby on Rails”. Diese neue Generation von Webseiten ist auch als “SPA” (Single Page Web-Applications) oder “Rich Client Web Applications” bekannt und unterscheidet sich von “herkömmlichen” Webseiten dadurch, dass stets nur einzelne Teilbereiche einer Webseite neu geladen werden anstatt durch eine Reihe von Seiten “vorwärts und rückwärts” zu gehen und diese jeweils komplett zu laden. Rich Client Web Applications fühlen sich wesentlich “applikationshafter” an als “herkömmliche” Webseiten und zeichnen sich nicht zuletzt dadurch aus, dass mehr Arbeit, die klassicherweise auf dem Server gemacht wurde auf den Client – also den Browser des Beutzers ausgelagert wird.

Genau genommen ist der MEAN-Stack nicht ein Framework, sondern die Kombination mehrerer, kleinerer Frameworks, Schichten und Module zu einem sogenannten stack.

11.2.1 Vorteile

Entscheidender Vorteil des MEAN-Stacks gegenüber Ruby on Rails sind u.a. folgende

  • man kann durch alle Teile hinweg stets nur in Javascript bleiben
  • die Webseiten sind wesentlich performanter und die Load auf dem Server bleibt wesentlich geringer
  • die Sub-Komponenten (die gleich besprochen werden) sind jeweils gegen andere austauschbar: in diesem Sinne handelt es sich um einen Stack: man kann für die jeweiligen Aufgaben durchaus auch andere Module zum Einsatz bringen
  • es gibt weniger Konventionen und einen freieren Aufbau (dies kann sowohl ein Vor- als auch ein Nachteil sein)

Hierbei stehen die Initialen für folgende Teile, welche – ganz grob umrissen –, folgende Aufgaben übernehmen:

  • MongoDB27 – Datenbank
  • Express28 – Framework aufbauend auf Node, für die Server-Seite
  • Angular29 – für die Client-Seite
  • Node30 – der Webserver (ähnlich wie, aber statt Apache, nginx, unicorn, …)

Wie oben angedeutet ließen sich die einzelnen Komponenten des Stack auch durch andere Komponenten austauschen – dazu im folgenden eine kleine Aufzählung, welche Alternativen (unter vielen vielen mehr) potentiell bestehen könnten – hierbei werden vielleicht auch die Aufgaben der einzelnen Komponenten deutlicher:

  • MongoDB: mysql, postgresql, redis, couchDB, …
  • Express: koa, pures Node, …
  • Angular: jQuery + php, ember.js, sails.js, …
  • Node: Apache, nginx, unicorn, foreman, …

11.2.2 Node

Node31 wird verwendet um einen eigenen Webserver selbst zu schreiben. Node ist ein unglaublich schneller Webserver und ein funktionsfähiger eigener Webserver lässt sich in Node bereits mit wenigen Zeilen schreiben.

Da am Ende Webseiten über eine normale WWW-Adresse ausgeliefert werden sollen und meistens ein Apache auf dem Server bereits läuft und auf dem HTTP-Port 80 “listened”, kann dieser Port nicht erneut belegt werden. Node macht daher seinen eigenen Port auf, z.B. 6688, was aber wiederum bedeuten würde, dass der user die seite auf diesem Port ansteuern müsste, z.B. webseite.com:6688.

Für diesen Fall wird zumeist ein sog. “Reverse Proxy” im Apache eingerichtet, der alle unterschiedlichen Services und Webserver die möglicherweise sonst noch auf dem Server laufen zusammenfasst und eine einheitliche Oberfläche nach außen bietet (d.h. keine expliziten Port-Angaben vom User verlangt, die ihn ohnehin nur verwirren würden). Bei einem Reverse-Proxy wird Apache mitgeteilt bestimmte Pfade auf bestimmte Ports einfach weiterzuleiten und nicht selbst zu bearbeiten.

11.2.3 Angular

Angular erlaubt es, vieles an Logik bereits in der View zu lösen, d.h. an der Stelle, an der es in einem MVC-Pattern32 häufig gebraucht wird. (Der andere, gegenteilige Ansatz nennt sich “Fat Controller”).

Angular erlaubt das ganz hervorragende “double data-binding”, welches man sich lieber live in einem Beispiel anschaut um es zu verstehen. Damit erlaubt es Angular unglaublich schnell schöne Web-Applikationen zu entwickeln.

11.2.4 Express

Express ist ein Mini-Framework, das auf Node aufbaut, und es deutlich einfacher macht, einen Server mit Routes, Middleware, etc auf die Beine zu stellen. Dabei arbeitet es sehr eng mit dem Node-Webserver zusammen.

11.2.5 MongoDB

MongoDB (von “humounguous”, riesig) ist eine sogenannte “NoSQL”-, bzw. Dokumentenorientierte Datenbank.

Dabei speichert es im Gegensatz zu SQL-artigen Datenbanken JSON-Objekte (“Dokumente”) anstatt von Tabellen und ist wesentlich einfacher zu bedienen.

Auch passt es sich besser an sich schnell und häufig wechselnde Datenbank-Schemata an und ist dadurch wesentlich flexibler als eine SQL-Basierte Datenbank.

11.3 Jade

11.3.1 Bootstrap

11.3.2 Bootstrap UI

11.3.3 jQuery

12 XML

12.1 TEI

12.2 XPath

13 Dokumentation

13.1 pipeline mit gpp, ppp & pandoc

Dieses Dokument wurde erstellt mit einer elaborierten pipeline, die mehrere Tools umfasst.

Dabei wird von pandoc33 der wesentliche Teil übernommen, und dieses von gpp34, dem “general preprocessor” und ppp35, dem “pandoc preprocessor” begleitet.

Mit pandoc lassen sich eine breite Vielfalt von Dokumenten in Markdown erstellen und in verschiedene Formate exportieren, wie z.B.:


  • HTML
  • ebooks
  • Präsentationen
  • u.v.w.m.

Um dieses Dokument voll setzen zu können sind mindestens notwendig:

  • pandoc36
  • gpp37
  • ppp38
    • dot39
    • neato40
    • rdfdot41
    • ditaa42
    • Image::Magick43
    • rsvg-convert44
    • yuml45. (als python-modul installieren mit pip install https://github.com/wandernauta/yuml/zipball/master oder easy_install https://github.com/wandernauta/yuml/zipball/master)
    • plantuml46
  • TeXLive / MikTex / MacTex47

Empfohlen werden dazu noch für das Bibliographieren:

  • zotero
  • chrome zotero plugin
  • zotero plugin auto export bibtex

Desweiteren sei hingewiesen auf das von David Kaumanns mit Stefan Schweter entwickelte Template zur Gestaltung von Bachelor- und Hausarbeiten, welches auf dieser pipeline aufbaut:

  • BA Thesis Template48

Der Workflow eines vollen Typeset gestaltet sich folgendermaßen:

  • Vorverarbeitung mit gpp: Datei-includes, Teile Ein- und Ausblenden je nach #defines
  • Vorverarbeitung mit ppp: alle “code”-Teile die Graphiken beschreiben zu Graphiken rendern
  • Zentrale: pandoc: Export in verschiedene Formate: pdf, html, tex, ebook, etc.
Der Typesetting Process

Der Typesetting Process

Eine genauere Beschreibung und Anleitung findet sich in ppp-Documentation.pdf im Repository von ppp49.

13.1.1 Hinweise zur Installation der dependencies

Für Hinweise zur Installation der dependencies bezüglich der Rechner am CIP-Pool, vergleiche die README.md in BA Thesis Template50.

13.1.2 Hinweise zur Benutzung des Makefiles

Für Hinweise zur Benutzung des Makefiles, vergleiche ebenfalls die README.md in BA Thesis Template51.

14 Unified Deployment

14.1 wast-infrastructure

Durch den unified deployment process in WAST werden alle Komponenten (unabhängig von ihrem Aufbau, ihren verwendeten Modulen, backends, frontends, …) abstrahiert unter einem Dach zusammengefassst, und mit nur einem Befehl verwaltet, administriert, und deployed.

Dieses Vorgehen hat den entscheidenden Vorteil, dass spätere Generationen alle Komponenten in gewohnter Weise bilden, kompilieren und deployen können – ohne sich speziell mit dem einzelnen Modul im Detail auskennen zu müssen – und, letztendlich, dabei einzelne Befehle in der richigen Reihenfolge mit den richtigen Argumenten auf diesen aufrufen zu müssen.

Der unified deployment process wird nach und nach immer stärker eingesetzt werden um die gesamte Tool- und Komponenten-Landschaft von WAST zu verwalten, d.h. – unter anderem –, Gesamt-Releases anzulegen (nach dem Semantic Versioning-Schema), den Development und den Master-Branch zu verwalten (siehe [git branching model][]), einzelne Komponenten upzudaten, neue annotierte Daten aus Bergen mit einem Befehl in alle Suchmaschinen zu verteilen und dergleichen.

14.1.1 Bündelung der Komponenten unter einem Dach

Die zentrale Stelle an der alle Komponenten der WAST-Landschaft zusammenlaufen ist das repository wittfind-web.

wittfind-web stellt nach außen hin die zentrale Webseite als Einstieg für die Nutzer dar; nach innen hin ist dies der Schnittpunkt an dem alle Module zusammenlaufen52.

Das zentrale Makefile, welches alle diese administrativen Aufgaben übernimmt (und dabei u.a. an komponentenspezifische Makefiles weiterleitet ist hier).

+++ BEGIN DEPRECATED +++

Ab hier ist dieses Kapitel ein bisschen out-dated, vermittelt aber trotz allem den richtigen Eindruck von unserem neueren Makefile-Ansatz, bitte die folgenden Teile Updaten!

Um die Komponenten und Tools unter einem Dach zu bündeln bietet wast-infrastructure den Befehl wast.

Unter wast werden die verschiedenen Komponenten registriert und sind damit für den “unified deployment”-Prozess verfügbar:

> ./wast
WAST, version 0.9.5

This is the help section

Usage:

  ./wast <command>

Available COMMANDS:
init                    : initialize (all) components
build <component>       : build component <component>
start <component>       : start component <component>
stop  <component>       : stop component <component>
restart <component>     : restart component <component>

Available COMPONENTS:
wast-feedback-app
wast-website
wf
sis

Die Komponenten werden unter Available COMPONENTS gelistet und sind somit für die Befehle, die unter COMMANDS gelistet sind, verfügbar.

Beispielsweise lässt sich somit also die Komponente wf mit folgendem Befehl bilden und starten:

> ./wast init wf  && \
  ./wast build wf && \
  ./wast start wf

Selbstverständlich lässt es sich darüber nachdenken, welche weiteren Sub-Befehle für wast noch denkbar sind (z.B. ist wast release <version> noch nicht implementiert, und denkbar sind auch Befehle wie wast test <component>, wast deploym <component> um die vorhergehenden drei Befehle zusammenzufassen). Dieser Teil befindet sich bisher noch in der beta- und Evaluations-Phase, daher sollen zunächst die wichtigsten Befehle robust implementiert sein, bevor neue Funktionalität hinzugefügt wird.

Um wast neue Funktionalität hinzuzufügen oder eine Komponente Teil des unified deployment-Prozesses zu machen ist gar nicht viel nötig, wie im nächsten Teil gezeigt wird.

14.1.2 Abstraktion des deployment-Prozesses

wast erwartet von einer Komponente im wesentlichen 2 Dinge:

  • die Komponente woll in wast registriert sein
  • die Komponente soll für alle wast-Sub-Befehle, die unterstützt werden sollen, ein entsprechendes Skript liefern. Konkret muss wf, wenn es die Befehle wast build und wast start anbieten will, in seinem Verzeichnis die Dateien wast.build und wast.start anbieten.

14.1.2.1 Beispiele

In diesem Beispiel soll die Komponente sis und ihre Implementation im Rahmen der wast-infrastructure betrachtet werden.

Zunächst also die Registrierung der Komponente in der wast infrastructure in der Datei wast:

14
COMPONENTS=(wast-feedback-app wast-website wf sis)

Als nächstes die entsprechenden Skripte in der Komponente selbst:

Hier also die Datei wast.build innerhalb der Komponente “sis”:

#!/bin/sh

set -x  # Show the commands that are executed

# try "Creating build directory"      by 
mkdir -p build

# try "Switching to build directory"  by 
cd build

# try "Running cmake"                 by 
cmake ..

# try "Running make"                  by 
make VERBOSE=1

# try "Switching to web component"    by 
cd ../web

# try "Installing web dependendices"  by 
CXX=/usr/bin/g++-4.8 npm install

# try "Installing web dependendices"  by 
bower install

Und hier die Datei wast.start innerhalb der Komponente “sis”, um den Befehl ./wast start sis zu unterstützen:

#!/bin/sh
set -x
# try "Starting feedback app" by 
cd web
LD_LIBRARY_PATH=/srv/www/vhosts/wast/components/sis/build/lib \
sudo -u wastd forever start -a app.coffee

+++ END DEPRECATED +++

14.2 user wastd

Alle Skripte und Komponenten, die einen Webserver starten und lange laufen, sollen unter dem “Pseudo-Superuser” genannt wastd gestartet werden.

Dieser “Pseudo-Superuser” hat den Vorteil, dass Webserver, die von jemand anderem gestartet wurden, von einer weiteren Person – im Falle des Falles – auch wieder ge-kill-t und gestoppt werden können. Normale Prozesse, die von einem bestimmten User gestartet wurden, erlauben es nicht, von einem anderen User ge-kill-t zu werden.

Der User wastd ist aber nicht im echten Sinne ein Super-User mit root-Rechten, sondern lediglich ein entsprechend ausgestatter User, der eben diese gemeinsame Nutzung von lange laufenden Prozessen über verschiedene User hinweg erlaubt.


15 C++

In diesem Kapitel werden entscheidende Techniken vorgestellt, die besonders in fortgeschrittenen C++-Programmen eine große Rolle spielen können. Die folgenden Techniken kommen sehr viel zum Einsatz in wittfind und SIS.

15.1 Wrapping von C nach C++

In manchen Situationen bietet es sich an, C structs nach C++ zu wrappen.

15.1.1 Vorteile

Die entscheidenden Vorteile des Wrappens von C nach C++ sind:

  • automatische und saubere Allokation / Deallokation von Klassen bzw. structs (dieses ist auch bekannt unter dem Namen “RAII” – zu diesem siehe auch das nächste Kapitel sowie Definitionen)
  • einfacheres “Handling” der Instantiierung von Klassen, also Konstruktion und, v.a. sauberere, sicherere Destruktion

Selbstverständlich lassen sich nach dem Wrapping auch alle Vorteile von C++ bzw. C++11 (und, später C++14) nutzen. Darunter fallen u.a.:

  • Die Smart Pointers shared_ptr und unique_ptr: Diese legen u.a. feste Ownership zu Grunde. Damit wird verhindert, dass man vergisst ein struct wieder zu deallokieren und die Zuständigkeit wird festgelegt: wer soll den pointer am Ende wieder deallokieren?

  • Templatisierung

  • Vererbung und Interfaces und damit klare Strukturen: Mit C++ lässt sich viel stärker auf der Problem-Ebene arbeiten denn auf der reinen Sprach-Ebene

15.1.2 Patterns

Grundlegend funktioniert das Adapter/Wrapper-Pattern immer auf die gleiche Art und Weise: Es gibt eine Klasse die gewrapped werden soll, den Adaptee und einen Wrapper, den Adaptor bzw. Wrapper.

Adapter Pattern

Adapter Pattern

Der Wrapper hält eine Verbindung zum Adaptee und bildet die API nach. Der Client wird nur noch einen Adaptor bzw. Wrapper instantiieren und der Adaptor kümmert sich darum, den entsprechenden, eigentlichen Adapee zu konstruieren.

Wenn der Client Methoden des Adaptors verwendet, so wird dieser lediglich die Parameter an den Adaptee weiterleiten und die Ergebnisse, die vom Adaptee kommen wieder an den Client zurückleiten.

Im Fall von C++ bietet es sich natürlich an, den Adaptee als Pointer im Adaptor zu halten.

Ein mögliches, elegantes, sicheres und eigentlich simples Pattern um C structs nach C++ zu wrappen beschreibt (R. Martinho Fernandes 2013) mit seiner “Rule of Zero”. Eine konkrete Anwendung wird im Kapitel ab Seite sowie hier im Detail beschrieben.

Um dieses Pattern besser verstehen zu können soll an dieser Stelle noch einmal ein genuerer Blick darauf geworfen werden. Als Beispiel dient in diesesm Fall die Klasse VoidSequenceAdapter, welche das C-Struct VoidSequence wrapped.

VoidSequence stellt in der C-Schicht eine Art std::vector dar, indem es eine Liste von Elementen führt, und, falls der allokierte Speicher nicht ausreicht, neuen besorgt, um weitere Elemente in seinen Members einfügen zu können. Dabei speichert es beliebige Objekte, indem es sich als void* verwaltet – letzteres setzt natürlich voraus, dass diese pointer später wieder entsprechend “zurückgecastet” werden.

Wie auch immer das struct im Detail aussieht, das Wrapping-Pattern bleibt stets identisch. Zunächst soll ein naiver Ansatz vorgestellt werden, welcher dann kontrastiert wird von dem “Rule of Zero”-Ansatz.

1
2
3
4
5
6
7
#include "Adaptee.h"

class Adaptor {
  Adaptee*   adaptee_;
  Adaptor(): adaptee_{ new Adaptee() } {};
  ~Adaptee() { delete adaptee_; }
};

Dieser naive Ansatz zeigt das Wrapping-Pattern in klassischer Weise in C++: es wird ein Pointer zum Adaptee gehalten. Der Pointer wird im Konstruktor hergestellt und im Destruktor deallokiert. Problematisch wird es, wenn der “Konstruktor” des Adaptee eine exception wirft: Konstruktoren sollen keine Exceptions werfen, da es sonst schwierig wird, bereits gelaufene Allokationen rückgängig zu machen.

Ein fortgeschrittenerer Ansatz ist der “Rule of Zero”-Ansatz von (R. Martinho Fernandes 2013): In diesem Fall ist auch die Ownership zur Adaptee-Ressource geregelt, da es sich hier um einen unique_ptr handelt.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "voidSequence.h"

class VoidSequenceAdapter {
  using adaptee_handle = std::unique_ptr<VoidSequence, decltype(&::VoidSequenceFree)>;
  
private:
  adaptee_handle adaptee_;
  
public:
   VoidSequenceAdapter()
   : adaptee_{ ::VoidSequenceInit(1), &::VoidSequenceFree } {}
  ~VoidSequenceAdapter() {}
};

15.1.2.1 Besprechung des Codes

  • Zeile 1 bindet die Definitionen des C structs ein
  • Zeile 3 definiert eine Adapter-Klasse VoidSequenceAdapter, die das C struct VoidSequence wrappen wird
  • Zeile 4 definiert einen geeigneten Daten-Typ für den Adaptee:
    • Der Typ des unique_ptr ist unique_ptr<VoidSequence> mit einem custom-Desktruktor. Letzterer wird aufgerufen, sobald der unique_ptr destruiert wird.
  • die gewöhnungsbedürftige Schreibweise &::VoidSequenceFree bedeutet folgendes:
    • unique_ptr verlangt, wenn man einen custom-DTOR angeben will, als zweites Template-Argument den Typ einer Funktion. Dieses wird durch decltype generiert.
    • decltype wiederum wird den Typ der Funktion VoidSequenceFree zurückliefern
    • & nimmt die Adresse der Funktion (liefert also einen Pointer zurück)
    • ::VoidSequenceFree gibt an, dass es sich hier um eine namespace-lose Funktion handelt. (Also das Gegenstück von, z.B. std::)
  • adaptee_handle adaptee_ legt das member adaptee_ vom Typ adaptee_handle an. Damit ist sichergestellt, dass zu dem Zeitpunkt, an dem die Destruktion des Objekts VoidSequenceAdapter stattfindet, alle seine member destruiert werden. Damit wiederum wird der custom-DTOR des adaptee_handle angestoßen und dieser ist ein VoidSequenceFree, also ist die Rückbindung an die C-Schicht gegeben.
  • Zeile 10-11: Der CTOR von VoidSequenceAdapter nun wird das member konkret initialisieren: Als erstes Argument wird die Konstruktionsfunktion des C-structs mitgegeben, als zweites Argument die Adresse der Destruktionsfunktion des C-structs.
  • Zeile 12: Der DTOR des VoidSequenceAdapters braucht nichts weiter – das member adaptee_handle wird nach den Standard-Regeln von C++ destruiert werden.

15.2 Policy based Design

Policy-based design erlaubt es, eine Klasse, die verschiedene Policies kennt, auf viele Arten und Weisen zu konfigurieren und somit ein deutlich unterschiedliches Verhalten der Klasse erzeugen zu können.

Policies decken dabei unterschiedliche, orthogonale Konzepte ab und lassen sich miteinander kombinieren um eine Vielzahl von Funktionalitäten und Verhaltensweisen zu bewirken.

Vergleiche vor allem die Implementation der ersten Generation von Smart Pointern (auto_ptr) welche zu den heutigen Standard-Smart-Pointern (shared_ptr und unique_ptr) geführt haben in (Alexandrescu 2000).

15.2.1 Beispiel

Um das Beispiel aus dem vorherigen Kapitel erneut aufzugreifen, soll an dieser Stelle auf die tatsächliche Implementation des adaptee_handle eingegangen werden:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Managed : std::true_type {};
class Unmanaged : std::false_type {};

template<class StringType = std::string, class ManagedAdaptee = Managed> class VoidSequenceAdapter;

template<class StringType, class ManagedAdaptee>
class VoidSequenceAdapter {

using adaptee_handle = typename
    std::conditional<
      ManagedAdaptee::value == true                                 // if adaptee is to be managed by this adapter
    , std::unique_ptr<VoidSequence, decltype(&::VoidSequenceFree)>  // then handle is a unique_ptr w/ custom DTOR
    , VoidSequence*                                                 // otherwise: bare ptr, no automatic deletion.
>::type;                                                            // finally: don't forget to choose this type
adaptee_handle adaptee_;

/* ... */
};

Abegesehen von den Template Meta Programmierungs-Anteilen (std::conditional) und den Type Traits-Anteilen (ManagedAdaptee::value) in diesem Beispiel soll an dieser Stelle der Policy-Based Ansatz (ManagedAdaptee) genau betrachtet werden.

Kurz zur Wiederholung: Nach der Defintion in Zeile 4, kann ein VoidSequenceAdapter auf folgende Arten und Weisen instantiiert werden und dabei ein sehr unterschiedliches Verhalten aufweisen:

1
2
3
4
VoidSequenceAdapter<>                        v1; /* std::string, managed     */
VoidSequenceAdapter<std::wstring>            v2; /* std::wstring, managed    */
VoidSequenceAdapter<std::wstring, Managed>   v3; /* same                     */
VoidSequenceAdapter<std::wstring, Unmanaged> v4; /* std::wstring, unmanaged! */

Wie im vorherigen Kapitel besprochen, wird die Klasse VoidSequenceAdapter – im Standardfall – den Speicher des Structs adaptee_ automatisch am Ende seiner eigenen Lifetime deleten, indem es die Funktion VoidSequenceFree aus der C-Schicht aufrufen wird.

Nun kann es aber ganz bestimmte Situationen geben, in welchen dieses Verhalten nicht nur unerwünscht sondern fehlerhaft wäre. In diesen Fällen lässt sich ein weiteres Template-Argument, Unmanaged mitgeben, um die Klasse derart anders zu gestalten, dass sie kein (!) automatisches delete von VoidSequence ausführen wird.

Dabei spielen folgende Teile zusammen:

  • class Managed : std::true_type {}; ist eine bereits fertige Klasse ohne weitere Implementation. Sie stellt einen “sprechenden” strong type dar, der sich als Template-Argument nutzen lässt. std::true_type stellt einen Typ aus C++11 dar, der folgendem Typ entspricht: std::integral_constant<bool, true>. Das heisst, es handelt sich hier um einen “strong type” vom Typ bool mit der Belegung true, dessen einfachste Implementierung folgende sein könnte (der Standard übernimmt dieses aber):

    template<class T, T v>
    struct integral_constant {
        static constexpr T value = v;
    };
  • Hiermit wiederum lassen sich entscheidende Weichen (zur Compile-Zeit!) stellen, um VoidSequenceAdapter entweder Managed oder Unmanaged zu machen. Um zur Compile-Zeit die Klasse VoidSequenceAdapter anders zu konfigurieren lässt sich Managed::value abfragen, um zu sehen, wie die Klasse konfiguriert werden soll (vergleiche hierzu das “Type Traits”- sowie das “Template Meta Programmierungs”-Kapitel).

  • In diesem elaborierteren Fall wird also der Typ des adaptee_handle zur Compile-Zeit entschieden und kann – je nach Typ von ManagedAdaptee – eines der beiden folgenden werden:

    std::unique_ptr<VoidSequence, decltype(&::VoidSequenceFree)>

    oder schlicht:

    VoidSequence*

Mit diesem Policy-Based Ansatz ist es also möglich, eine Klasse zu schreiben, die mehreren Ansprüchen – je nach Bedarf – gerecht werden kann: Im Standardfall ist VoidSequenceAdapter eine Adapter-Klasse, welche den RAII-Prinzipen folgt; sollte jedoch ein anderes, möglicherweise orthogonales, Verhalten benötigt werden, so lässt sich dieses ebenso umsetzen und der restliche Code, der VoidSequenceAdapter nutzt, kann diesen auch weiterhin wie gewohnt nutzen.

Zu diesem Kapitel wird empfohlen auch die anderen Kapitel Template Meta Programmierung, SFINAE, Type traits, und Silent Degredation genau zu beachten; vor allem aber (Alexandrescu 2000), (Di Gennaro 2012) und (Abrahams and Gurtovoy 2005) genau zu studieren.

15.3 Type traits

Type traits sind im wesentlichen schlicht typedefs in einer Klasse, die man von außen abfragen kann.

Ein kleines Beispiel soll dies kurz darstellen:

Anstatt folgendes zu schreiben:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <vector>
#include <string>
#include <cctype>
#include <algorithm>
#include <iostream>

#include <boost/algorithm/string.hpp>

int main() {
    typedef std::vector<std::string> string_vector_t;

    string_vector_t vec = { "foo", "bar", "baz" };

    for (std::vector<std::string>::iterator it = vec.begin();
        it != vec.end();
        ++it)
    {
        std::cout << *it << std::endl;
    }
    
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        boost::to_upper(*it);
    }

    for (const auto & elem: vec) {
        std::cout << elem << std::endl;
    }

    return 0;
}

läßt sich auf einfach der typedef value_type von std::vector nutzen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <vector>
#include <string>
#include <cctype>
#include <algorithm>
#include <iostream>

#include <boost/algorithm/string.hpp>

int main() {
    typedef std::vector<std::string> string_vector_t;

    string_vector_t vec = { "foo", "bar", "baz" };

    std::for_each(begin(vec), end(vec), [&](string_vector_t::value_type & elem) {
        std::cout << elem << std::endl;
    });

    std::for_each(begin(vec), end(vec), [&](decltype(vec)::value_type & elem) {
        boost::to_upper(elem);
    });

    std::for_each(begin(vec), end(vec), [&](std::vector<std::string>::value_type & elem) {
        std::cout << elem << std::endl;
    });


    auto output_function = [&](string_vector_t::value_type & elem) {
        std::cout << elem << std::endl;
    };
    std::for_each(begin(vec), end(vec), output_function);

    return 0;
}

Type traits lassen sich insbesondere im Zusammenspiel von verschiedenen Klassen nutzen und ermöglichen es, unterschiedliche Klassen unabhängiger voneinander zu halten, dabei aber für mehr Typsicherheit zu sorgen.

15.3.1 Beispiel

/// @brief meta-class to handle StringType type traits (using static assertions)
template<class T>
struct ElementSize {
    static constexpr bool T_is_string_type =
                std::is_base_of< std::string, T>::value
             || std::is_base_of<std::wstring, T>::value ;

     static_assert(T_is_string_type,
         "\n\n\tYou must instantiate this class in question with std::string,"
         "\n\tstd::wstring, or any class derived thereof.\n"
     );


    typedef T                               type;
    typedef T                        string_type;
    typedef typename T::value_type    value_type;

    typedef typename std::conditional<
        std::is_same<std::string,T>::value, // if StringType is std::string
        std::fstream,                       // then stream_type is std::fstream
        std::wfstream                       // else it is std::wfstream
        >::type                             stream_type;

    static constexpr unsigned int value =
                            sizeof(typename string_type::value_type);
};

15.4 Template Meta Programmierung

Template Meta Programmierung besteht aus mehreren Teilen die in den nächsten Kapiteln besprochen werden

15.5 SFINAE

Eine in “Metaprogrammierungskreisen” häufig gesehene Abkürzung ist SFINAE – wobei dieses Monstrum an Abkürzung für “Substitution failure is not an error” steht (“Substitution Failure Is Not an Error” 2014).

Bei SFINAE geht es um folgendes Prinzip:

Man macht sich die Garantie des Standards zu nutze, dass, für den Fall, in dem ein Template-Argument ungültig ist, kein Fehler ausgelöst, sondern dieser schlicht übergangen wird.

Ganz grundsätzlich geht es bei SFINAE um folgendes:

If an invalid argument or return type is formed when a function template is instantiated during overload resolution, the function template instantiation is removed from the overload resolution set and does not result in a compilation error.

Kurz gesagt:

  • während des Einsetzens von Tempalte-Argumenten in die Template-Klassen werden potentiell viele template-Klassen vom Compiler angelegt
  • diese “ausgefüllten” Template-Klassen landen im sog. “Overloaod Resolution Set”.
  • bei dieser Operation können Fehler entstehen (in form von “ill-formed” classes) – diese werden schlicht übergangen.
  • Voilà! Dieses Aussortieren des Compilers von ill-formed classes bewusst zu nutzen ist SFINAE.

In der Standard-Library von C++11 gibt es z.B. eine Meta-Funktion std::enable_if<>.

Ihre Definition wird in etwa so aussehen:

template <bool Condition, typename T = void>
struct enable_if {}; // no members

template <typename T>
struct enable_if<true, T> { using type = T; }; // type member

Beispiel-Aufruf:

typename std::enable_if<std::is_floating_point<T>::value>

Wenn nun T von einem is_floating_point-Typ ist, so ist ein Type Trait ::type definiert, ansonsten nicht.

Dieses Konstrukt nun lässt sich an verschiedenen Stellen einsetzen um z.B. unter bestimmten Bedingungen Funktionen ein- und auszuschalten, oder unter bestimmten Bedingungen andere Überladungen anzubieten:

  • std::enable_if kann als zusätzliches Argument einer Funktion verwendet werden (aber nicht bei Operatoren-Überladungen)

  • als Rückgabewert (daher aber nicht Konstruktoren und Destruktiren)

  • oder als Klassen-Template oder Funktionstemplateparameter

Als Hintergrundlektüre siehe auch:

15.6 Silent Degredation

Unter silent degredation bzw. “incomplete instantiation” versteht man folgendes Prinzip: Wenn eine Member-Methode einer Template-Klasse nie verwendet wird, so wird diese auch niemals instanziiert. (Alexandrescu 2000, S.13)

Damit lassen sich also höher-geordnete Klassen schreiben, die nach wie vor kompilierfähig bleiben, obwohl einige ihrer Member-Methoden eigentlich ill-formed wären – aber nur, falls diese benutzt werden würden!

15.6.1 Beispiel

Angenommen man hat eine Klasse DoItAll mit folgender implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <class Type>
class DoItAll {
  Type member_;

public:

  /* CTOR, DTOR */
  DoItAll(Type t): member_{t} {}
  ~DoItAll() {}

  /* Methods */
  inline Type plus(Type other) const { return member_ + other; }
  inline Type div(Type other) const { return member_ / other; }
};

Angenommen, DoItAll würde folgendermaßen genutzt werden:

1
2
3
4
DoItAll<int>  doitall_int{8};
  
std::cout << doitall_int.plus(34) << std::endl; // 42
std::cout << doitall_int.div(4) << std::endl;   // 2

Oder auch so:

1
2
3
DoItAll<std::string>  doitall_str{"this is"};
  
std::cout << doitall_str.plus(" a string") << std::endl; // "this is a string"

…so wäre bis hierhin alles in Ordnung und würde sauber kompilieren – trotz dessen, dass std::string / std::string ill-formed und kein gütliger Aufruf wäre!

Das Geheimnis dahinter ist, dass Methoden von Template Klassen erst in dem Moment konkret instantiiert werden, in dem sie auch tatsächlich genutzt werden, d.h. wenn an keiner Stelle doitall_str.div("some string") aufgerufen wird, wird diese Methode auch nicht instantiiert und die Klasse bleibt kompilierfähig.

Dies ist das Geheimnis hinter silent degredation bzw. “incomplete instantiation” und erlaubt wiederum sehr flexible Klassen zu gestalten.

15.6.2 Bemerkung

Mit C++11 gibt es eine Möglichkeit, Kompilierzeit zu sparen, indem man den Compiler mit extern template anweisen kann, sich darauf zu verlassen, dass eine volle Instantiierung/Spezialisierung einer Klasse an anderer Stelle vorgenommen wird und an dieser Stelle nicht zu instantiieren.

Im Normalfall wird nämlich die template class X überall dort, wo sie verwendet wird, vom Compiler (u.U. auch viele Male) instantiiert nur um am Schluss alle diese – bis auf eine – wieder zu verwerfen.

C++11 erlaubt eine einmalige Spezialisierung auf die sich andere Code-Teile stützen können mit:

extern template class std::vector<MyClass>;

Allerdings beisst sich nun dieses Konzept mit der “silent degredation” da durch dieses Vorgehen eine volle Spezialisierung und damit eine volle Instantiierung vorgenommen wird.

15.7 Static Dispatching / Tag dispatch

Beim static dispatching bzw. auch tag dispatching geht es darum, verschiedene Funktionen (oder Members) mit tags zu annotieren und, je nach Bedarf, gezielt auszuführen.

Angenommen, man hat das folgende Problem:

Es gibt ein Klassen-Template template<class T> Foo welche eine Member-Methode bar() hat.

Nun gibt es zwei unterschiedliche Instanziierungen für den Container Foo: einmal als Foo<std::string> und einmal als Foo<int>.

Basierend auf dieser Instanziierung sollen nun zwei komplett unterschiedliche Methoden (im Sinne von unterschiedlichen Implementierungen) der Methode bar() aufgerufen werden.

Dies ließe sich – in diesem Beispiel –, auch durch partielle Spezialisierung erreichen, dennoch greift das Beispiel in dem Sinne, als dass die tag dispatching-Technik auch in anderen Fällen eingesetzt werden kann; nicht nur in diesem Fall.

Ein member-dispatch-pattern, um dieses zu erreichen könnte z.B. so aussehen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <type_traits>

// C++11 Proof of concept of different implementations of
// member methods for different types of class instances.

template<class T> struct tag {};

template<class T>
struct Foo {

    [[ dispatched_to ]]
    std::string bar(tag<std::string>) {
        return std::string{"foo"};
    }

    int bar(tag<int>) [[ dispatched_to ]] {
        return 5;
    }

    auto bar() [[ dispatcher ]]
    ->decltype(this->bar(tag<T>{})) // 'this' seems to be needed here.
    {
        return bar(tag<T>{});
    }
};

15.8 Detail-Beschreibung

  • es gibt einen zentralen Einstiegspunkt, die Methode bar(): die Methode bar() ist mit dem Attribut [[dispatcher]] markiert und zeigt damit an, dass bar() keinerlei Logik hält, sondern lediglich weiterleiten (dispatchen) wird, zu den anderem Methoden, welche die Logik beinhalten. Das Attribut [[dispatcher]] hat keinerlei Funktionalität, ausser, dass sie dem Anwender einen Hinweis darauf gibt, was diese Methode macht.

  • ebenso wie es einen [[dispatcher]] gibt, gibt es also nun Logik-tragende Funktionen oder Methoden, zu welchen dispatched wird, diese sind mit dem Attribut [[dispatched_to]] markiert – wiederum ist dieses Attribut lediglich ein Hinweis an den Anwender und könnte dementsprechend ausgelassen werden, es tragt keine weitere Funktionalität. Die beiden Logik-tragenden Methoden sind:

    • int bar(tag<int>) und
    • std::string bar(tag<std::string>)
  • bei einem genauen Blick auf den Funktionsteil des [[dispatcher]] sieht man, dass es die ge-tag-ten Methoden [[dispatched_to]] aufruft, bzw. an diese weiterleitet. Diese letzteren Methoden sind implementationsabhängig, basierend auf dem type T, mit welchem die Klasse Foo<> instanziiert ist.

    • zu diesem Zweck gibt es eine weitere (Mini-)Klasse template<class T> struct tag{}; welche auch wiederum zum größten Teil nur “deskriptiven” Charakter hat, um anzudeuten, dass hier ein tag-dispatching stattfindet.
    • bei der Weiterleitung – dem dispatching –, verwendet auto bar() [[ dispatcher ]] diesen Tag derart, dass es:
      • ein Mini-Objekt vom Typ tag<T> anlegt – durch uniform initialization: tag<T>{}
    • dieses Mini-Objekt kann auch wiederum als lediglich deskriptiv betrachtet werden, denn es wird letztendlich vom Optimizier entfernt werden
  • nun aber ist der Weg frei, je nach Instanziierung entweder int bar(tag<int>) oder std::string bar(tag<std::string>) aufzurufen.

  • Schlussendlich wird auch noch innerhalb der dispatchenden Methode der Rückgabewert offengehalten und richtet sich nach dem Rückgabewert der logik-tragenden, [[dispatched_to]]-Methode: ->decltype(this->bar(tag<T>{}))

15.9 Bemerkungen

Offensichtlich lässt sich mit den [[Attributen]] spielen und diese sind variabel hinsichtlich dessen, wo diese genau platziert werden.

Attribute wurden in C++11 eingeführt, um den Weg auf Sprachebene dahingehend zu öffnen und standardisieren, dass der Compiler (zu einem späteren Zeitpunkt) diese Attribute in der Kompilierung nutzen kann – bisher jedoch werden diese von den gängigen Compilern großflächig ignoriert, sind aber bereits nutzbar, wie in diesem Beispiel; machen aber lediglich für den Anwender Sinn.

Die Implementation von diesem Code-Stück findet sich hier.

15.10 Wrapping von C++ nach Javacript (Node)

Siehe hierzu das eigene Kapitel Node Wrapping.

16 Serialisierung

Unter Serialisierung versteht man das Abspeichern einer konkreten, instanziierten Klasse – henau genommen der Klassentyp, seine Member und die Vererbungskette – auf der Festplatte, um diese Klasse in genau diesem Zustand zu einem späteren Zeitpunkt wiederherstellen zu können.

16.1 Methoden zur Serialisierung

Für das Serialisieren und Deserialisieren in C++ gibt es mehrere Möglichkeiten:

  1. selbst geschriebene Serialisierungsmethoden verwenden
  2. Boost Serialization
  3. cereal
  4. generic-serialize

16.1.1 Hinweise

16.1.1.1 Selbst geschriebene Serialisierungsmethoden

zu verwenden, kann mehrere Nachteile haben:

  • es gibt schon gute und weithin-geteste Frameworks wird boost-serialization und cereal.
  • Serialisierung in C++ richtig zu machen, kann sehr kompliziert sein und viel Aufwand erfordern (v.a. die Serialisierung von Pointern und tracken derselben, von Vererbungsketten (virtual vs. non-virtual) und viele weitere detailreiche Probleme)
  • Übertragbarkeit und Wartung: sollten andere Entwickler die mit der vorhandenen Code-Base weiterarbeiten müssen, so kennen diese höchstwahrscheinlich schon boost serialization und haben vorher schon mit dieser gearbeitet

16.1.1.2 Boost Serialization

ist eine weitverbreitete, d.h. gut bekannte und in vielen Projekten getestete Serialisierungsbibliothek. Boost Serialization kann u.a. auch raw pointer serialisiern, wenn solche als Klassenmember verwendet werden und Vererbungsketten bei der Deserialisierung richtig auflösen. Leider ist es z.T. etwas komplex und die Dokumentation nicht gerade leicht zu verstehen; dafür aber ist es mit eine der mächtigsten Serialisierungsbibliotheken.

16.1.1.3 cereal

cereal ist eine junge Serialisierungsbibliothek und verwendet v.a. Features aus C++11, wie z.B. Template Meta Programmierung, um weitreichende Annahmen über Datentypen machen zu können. (Dies ist vor allem darum wichtig, weil C++ keine Introspection hat bzw. haben kann).

cereal’s API ist relativ einfach zu verstehen und einzusetzen, weshalb cereal in SIS eingesetzt wird, um den C++-Teil der Automaten auf der Festplatte zu speichern und wieder zu laden.

Hinweis: in SIS werden nur die C++-Schicht der Automaten von cereal serialisiert; für die C-Schicht werden die C-eigenen Serialisierungsmethoden verwendet. Da beide Schichten und die darin befindlichen Klassen aufeinander aufbauen, ist hierbei aber tunlichst darauf zu achten, dass beide (De-)Serialisierungen ordentlich aufeinander abgestimmt sind! Siehe die entsprechenden Kapitel bei SIS für eine tiefere Dokumentation zu diesem Thema.

16.1.1.4 generic serialize

ist eine Idee und ein Proof of Concept von Florian Fink und Daniel Bruder, um einzelne Container gezielt durch ein- und die selbe API im Binärformat auf die Festplatte schreiben und von dort wieder laden zu können. Dies wird ermöglicht durch C++11 und Template Meta Programmierung und SFINAE. Teile hiervon werden im folgenden gleich beschrieben werden.

16.2 Formate

Die wesentlichen Formate, um Klassen, deren Members und Vererbungsketten auf Festplatte zu speichern und später wieder zu laden sind v.a. die drei folgenden:

  • JSON / Text
  • XML
  • Binärformat

cereal und boost serialization unterstützen jeweils alle 3 der hier genannten Formate.

Selbstverständlich ist das Byte-Format das platzsparendste unter den Formaten, dabei auch das am kompliziertesten “zu debuggende”, sollten Fehler auftreten. JSON ist sowohl platzsparend als auch bequem zu lesen. XML ist am meisten “verbose”.

16.3 Klassische Verfahren: Binärformat und byte-layout

Beim Serialisieren in das Byte-Format gibt es eine gängige Praxis die hier beschrieben werden soll.

Im Binärformat muss der Anwender das byte-layout genau kennen, um etwas sinnvolles damit anfangen zu können – die Grafik in ab Seite soll dies verdeutlichen.

Beispiel für ein Byte-Layout

Beispiel für ein Byte-Layout

In diesem Fall werden 4 Variablen gespeichert:

  • ein char
  • ein unsigned int
  • ein bool
  • ein std::vector<std::string> mit der Größe 2:
    • hierbei hat der erste std::string eine size() == 4
    • hierbei hat der zweite std::string eine size() == 6

Die ingesamte Größe des Binärformats beträgt dann 19 Byte.

16.3.1 Beobachtungen

  • Der erste Character belegt 1 Byte
  • unsigned int beträgt 4 Byte (auf einem 64-Bit-System)
  • bool belegt 1 Byte: Obwohl ein bool theoretisch nur 1 Bit statt einem ganzen Byte (8 bit) bräuchte, tritt hier das Phänomen des Padding auf und das bit wird aligned (siehe dazu http://en.wikipedia.org/wiki/Data_padding)
  • Der std::vector<std::string> ist folgendermaßen aufgebaut:
    • das erste byte wird belegt mit der Angabe, wie viele Elemente der vector enthält, also mit vector.size()
    • das nächste byte kündigt die Größe des ersten std::strings an: 4
    • nun folgen die 4 characters des ersten strings
    • da der vector 2 strings enthält muss nun die Längeninformation über den zweiten String folgen: string2.size() == 6
    • nun folgen noch 6 chars des zweiten strings des vector und die Informationen sind vollständig.

16.4 APIs

16.4.2 cereal

Die API von cereal ist relativ einfach – für einen tiefergehenden Einblick vergleiche das Kapitel Zweistufige Serialisierung in SIS: Symmetric Index Structures.

Im Grunde ist es lediglich notwendig eine Methode der folgenden Art in seiner Klasse zu schreiben:

Angenommen, die Klasse hat folgendes Member list_:

#include <vector>
#include <string>

class WordList {
public:
    typedef std::vector<std::string> list_t;

    /* Constructors, Methods, etc... */

private:            // Members
    list_t list_;
};

So reicht es bereits aus, die notwendigen header von cereal und eine Methode serialize() folgendermaßen zu schreiben:

// This method lets cereal know which data members to serialize
template<class Archive>
void serialize(Archive & archive) {
    archive( list_ ); // serialize things by passing them to the archive
}

Die Klasse sieht nun also so aus:

#include <vector>
#include <string>

#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>

class WordList {
public:
    typedef std::vector<std::string> list_t;

    /* Constructors, Methods, etc... */

private:    
    list_t list_;

public:
    template<class Archive>
    void serialize(Archive & archive) {
        archive( list_ );
    }
};

Nun kann die Klasse von aussen serialisiert und deserialisiert werden:

#include <iostream>
#include <fstream>
#include <string>

#include <cereal/archives/binary.hpp>

#include "WordList.hpp"

int main() {
    WordList wl;

    // Elemente zum Container hinzufügen
    std::string baz{"baz"};
    wl.add("foo");
    wl.add(std::string{"bar"});
    wl.add(baz);

    std::cout << wl.size() << std::endl;

    // Binär-Datei anlegen, in welche serialisert werden soll
    std::ofstream os{"out.bin", std::ios::binary};
    
    // cereal-Archiv auf die Binär-Datei anlegen
    cereal::BinaryOutputArchive binary_archive_file{ os };
    
    // Serialisierbaren Container in das cereal-Archiv geben
    binary_archive_file( wl );

    return 0;
}

Nach XML oder JSON zu serialiseren funktioniert genau nach dem selben Schema:

int main() {
    /* Container füllen ... */

    // JSON nach std::cout serialisieren:
    cereal::JSONOutputArchive     json_archive_cout{std::cout};
    json_archive_cout( wl );
    
    /* ... */
}

Das Einlesen ist wiederum genauso einfach:

#include <iostream>
#include <fstream>
#include <string>

#include <cereal/archives/binary.hpp>

#include "WordList.hpp"

int main() {
    
    /* Es gibt eine Binär-Serialisierte WordList ... */
    std::ifstream is{"out.bin", std::ios::binary};

    // cereal Input-Archiv auf diese Datei festlegen
    cereal::BinaryInputArchive binary_archive{ is };

    // leeren Container anlegen
    WordList wl;
    
    // Container aus Binär-Datei laden
    binary_archive(wl);

    return 0;
}

16.4.3 generic serialize

generic serialize erlaubt es, beliebige Standard-Container (mit beliebiger Schachtelung) individuell mit save zu speichern und mit load zu laden. Es kümmert sich automatisch darum, die richtige Speichermethode für den Container und die “Tiefe” des Containers zu finden.

Die API von generic serialize ist ebenfalls sehr einfach und überschaubar. Sie funktioniert so:

namespace usb = util::serialize::binary;

/* Example: Write nested container to a binary file */
std::list<std::map<std::string, unsigned>>  list = {
  {{"foo", 1}, {"bar", 2}},
  {{"baz", 1}, {"quux", 2}}
};

/* save */
std::ofstream   os{"file.bin", std::ios::out | std::ios::binary};
usb::save(os, list);
os.close();

Das Einlesen von Binärdaten mit generic serialize ist ebenfalls sehr einfach:

namespace usb = util::serialize::binary;

/* Example: Read nested container from binary file, way #1 */
std::ifstream   is{"file.bin", std::ios::in | std::ios::binary};
std::list<std::map<std::string, unsigned>>  list;
usb::load(is, list);

Ein anderer Weg wäre es, den Container-Typ als template-Argument anzugeben und C++11’s auto zu nutzen:

namespace usb = util::serialize::binary;

/* Example: Read nested container from binary file, way #2 */
std::ifstream   is{"file.bin", std::ios::in | std::ios::binary};

auto list = usb::load<
    std::list<std::map<std::string, unsigned>>
  >(is);

Auf diese Art und Weise werden beliebige Container in einem Format wie in der Grafik ab Seite angedeutet, angelegt.

16.4.3.1 Beobachtungen

Im wesentlichen bedient sich generic serialize der Techniken der Template Meta Programmierung und den Hilfestellungen von http://flamingdangerzone.com/cxx11/2012/05/29/type-traits-galore.html.

Dabei funktioniert der Speicher-Algorithmus folgendermaßen:

  • Wenn ein Container vorliegt (gesteuert von den Prinzipien von SFINAE und EnableIfContainer<T>), dann betrachte rekursiv alle Elemente ([Type Traits][]: value_type) dieses Containers, so lange bis du auf einen fundamentalen Datentyp stößt: int, char, o.ä.

  • Sobald ein Container bis auf seine fundamentalen Datentypen zurückgeführt, ist, so kann dieser Type binär gespeichert werden:

    template <typename T, EnableIfFundamental<T>...>
    T
    save(std::ofstream & os, T t) {
        os.write(reinterpret_cast<const char*>(&t), sizeof(t));
        return t;
    }
  • Hierbei wird zuerst die Größe des Containers und danach die einzelnen fundamentalen Datentypen gespeichert, z.B.:

    template <typename T, EnableIfContainer<T>...>
    T
    save(std::ofstream & os, T t) {
        unsigned size = t.size();
        save(os, size);
    
        for (auto it = begin(t); it != end(t); ++it) {
            save(os, *it);
        }
        return t;
    }

Das Einlesen funktioniert mutatis mutandis.

Ein Container liegt vor – und aktiviert nach den Prinzipien von SFINAE – die entsprechende save/load Funktionalität wenn folgendes Konstrukt einen gültigen Typ zurückliefert:

template<typename C>
struct is_container<C,
    typename is_of_type<
        typename C::const_iterator(C::*)() const,
        &C::begin,
        &C::end
    >::type
> : std::is_class<C> { };

D.h. es wird, um einen Container zur Compile-Zeit zu erkennen vom type C folgendes erwartet:

  • std::is_class<C> ist ein gültiger Type
  • Der betreffende Container C hat eine Methode begin() und eine Methode end() welche einen const_iterator zurückliefern.

Für mehr Details vergleiche https://github.com/xdbr/cpp-generic-serialize.

17 Node Wrapping

Node-Wrapping bezeichnet das wrappen von C++-Klassen derart, dass diese innerhalb von Javascript verwendbar gemacht werden.

Im Rahmen von SIS wird dies konkret vorgeführt: die dokument-indexierenden Automaten auf der C++-Ebene werden von der Javascript-Ebene (und damit dem node-Webserver) nur ein einziges Mal deserialisiert und ab dort als vollwertige Automaten im Speicher gehalten.

Vom node-Webserver aus nun lassen sich die C++-Automaten ansprechen und benutzen.

Um dieses language-binding zu bewerkstelligen bietet node den sog. “addon”-Mechanismus, welcher es eben erlaubt, C++-Klassen mit Javascript zu verbinden.

Die Verbindung dieser beiden Sprach-Welten läuft dabei in beide Richtungen: sowohl lassen sich aus der C++-Welt Javascript-Objekte bilden und diese nach Javascript “pushen”, als auch C++-Objekte auf der Javascript-Seite konstruieren, laden und dergleichen.

Möglich wird dies durch die derzeit schnellste Javascript-Implementation, V8. V8 ist eine Javascript-Implementation in C++ und wird von Google open source bereitgestellt. (V8 ist übrigens auch im Browser Chrome für die Javascript-Ausführung zuständig.) Über diese Bridge nun können alle oben genannten Aufgaben zwischen den beiden Sprach-Welten wahrgenommen werden.

17.1 Beispiel

Das Kapitel Web-Ebene im Teil SIS zeigt in genauerer Weise ein Beispiel für die Verwendung der C++-Automaten auf Javascript-Ebene.

17.2 Node Addon

Da das Schreiben von Node-Addons keine ganz simple Sache ist, empfiehlt es sich die Dokumentation hierzu sehr genau zu studieren und sich unbedingt so nahe wie möglich an die vorgeschlagenen Patterns und Beispiele in der Dokumenetation zu halten: http://nodejs.org/api/addons.html

17.3 Schematische Darstellung

auf Seite zeigt die schematische Darstellung eines node-wrap.

Node Wrapping Pattern

Node Wrapping Pattern

17.4 Demonstration

Diese Demonstration hält sich sehr eng an das Pattern “Wrapping C++ Objects” aus der node-addon-Dokumentation (http://nodejs.org/api/addons.html#addons_wrapping_c_objects) und beschreibt noch einmal die einzelnen Schritte etwas deutlicher als es die Dokumenation selbst tut.

Angenommen, es gibt eine einfache Klasse WordList (bzw. WL) mit folgendem Header (und den entsprechenden Definitionen zu den Methoden in einem passenden .cpp-File):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#ifndef WL_HPP_A8E3G2X4
#define WL_HPP_A8E3G2X4

#include <string>
#include <vector>

template<class StringType>
class WL {
    typedef StringType               string_type;
    typedef std::vector<string_type>  value_type;

public:
    explicit WL() = default;
    virtual ~WL() = default;

    void add(const string_type & s);
    size_t size() const;

    auto begin() -> typename value_type::iterator;
    auto end() -> typename value_type::iterator;
    bool operator== (const WL& other) const;

private:
    value_type wl_;
};

#include "WL.cpp"

#endif /* end of include guard: WL_HPP_A8E3G2X4 */

Diese Klasse soll nun also nach node gewrapped werden, damit sie folgendermaßen innerhalb von Javascript verwendet werden kann:

1
2
3
4
5
6
7
8
9
10
var addon = require('./build/Release/WLNodeWrap');

var wl = new addon.WLNodeWrap();

wl.add('foo');
wl.add('bar');
wl.add('baz');

var size = wl.size();
console.log( size ); // 3

Um dieses zu erreichen sind folgende Schritte nötig:

  1. Einsteigsdatei addon.cpp schreiben
  2. Wrapper-Klasse WLNodeWrap, die von node::ObjectWrap erbt und WL als Member enthält schreiben
  3. binding.gyp, die build-Datei für node-gyp, schreiben
  4. entstehende *.node Datei entsprechend in Javascript einbinden und benutzen

17.4.1 Schritt 1: Einsteigsdatei addon.cpp schreiben

Eine kleine Datei addon.cpp wird die gesamte Struktur von C++ nach Javascript verbinden, und sieht – dem Beispiel der Dokumentation folgend –, so aus:

#ifndef BUILDING_NODE_EXTENSION
#define BUILDING_NODE_EXTENSION
#endif
#include <node.h>
#include "WLNodeWrap.hpp"

using namespace v8;

void InitAll(Handle<Object> exports) {
  WLNodeWrap::Init(exports);
}

NODE_MODULE(WLNodeWrap, InitAll)

Hier wird festgelegt, dass ein Modul mit dem Namen WLNodeWrap definiert wird, und, sobald dieses per require (var addon = require('./build/Release/WLNodeWrap'); auf Javascript-Seite aufgerufen wird, zur Funktion InitAll gesprungen werden soll.

Die Funktion InitAll wiederum wird WLNodeWrap::Init aufrufen, welches wir sogleich definieren werden.

17.4.2 Schritt 2: Wrapper-Klasse WLNodeWrap, die von node::ObjectWrap erbt, schreiben

Die Wrapper-Klasse WLNodeWrap wiederum orientiert sich sehr stark an der o.g. node-Dokumentation.

Im wesentlichen erbt sie von node::ObjectWrap und exportiert eine static-Methode Init. (Diese static Methode ruft der vorherige Schritt auf: NODE_MODULE -> WLNodeWrap -> InitAll -> WLNodeWrap::Init!)

Desweiteren wird die eigentliche, zu wrappende Klasse WL<std::string> der Klasse WLNodeWrap als Member hinzugefügt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#ifndef NODEWRAP_HPP_SBNDJF7E
#define NODEWRAP_HPP_SBNDJF7E

#include <node.h>
#include <string>
#include "WL.hpp"

class WLNodeWrap : public node::ObjectWrap {
    typedef WL<std::string> wrapped_wl;
    
public:
    static void Init(v8::Handle<v8::Object> exports);

private:
    explicit WLNodeWrap() = default;
    virtual ~WLNodeWrap() = default;

    static v8::Handle<v8::Value> New(const v8::Arguments& args);
    
    // Add wird nach WL.add(const std::string&) gemapped werden
    static v8::Handle<v8::Value> Add(const v8::Arguments& args);
    // Size wird nach WL.size() gemapped werden
    static v8::Handle<v8::Value> Size(const v8::Arguments& args);
    static v8::Persistent<v8::Function> constructor;

    wrapped_wl wl_;
};

#endif /* end of include guard: NODEWRAP_HPP_SBNDJF7E */

Selbstverständlich gibt es eine passende Datei WLNodeWrap.cpp, welche diese Deklarationen des Header definiert, auch hier kann man sich wieder sehr eng an die node-Dokumentation halten:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#ifndef BUILDING_NODE_EXTENSION
#define BUILDING_NODE_EXTENSION
#endif

#include <node.h>
#include "WLNodeWrap.hpp"

using namespace v8;

Persistent<Function> WLNodeWrap::constructor;

void WLNodeWrap::Init(Handle<Object> exports) {

  // Prepare constructor template
  Local<FunctionTemplate> tpl = FunctionTemplate::New(New);
  tpl->SetClassName(String::NewSymbol("WLNodeWrap"));
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  // Prototype
  tpl->PrototypeTemplate()->Set(String::NewSymbol("add"),
      FunctionTemplate::New(Add)->GetFunction()
  );
  tpl->PrototypeTemplate()->Set(String::NewSymbol("size"),
      FunctionTemplate::New(Size)->GetFunction()
  );

  constructor = Persistent<Function>::New(tpl->GetFunction());
  exports->Set(String::NewSymbol("WLNodeWrap"), constructor);
}

Handle<Value> WLNodeWrap::New(const Arguments& args) {
  HandleScope scope;

  if (args.IsConstructCall()) {
    // Invoked as constructor: `new WLNodeWrap(...)`
    WLNodeWrap* obj = new WLNodeWrap();
    obj->Wrap(args.This());
    return args.This();
  } else {
    // Invoked as plain function `WLNodeWrap(...)`, turn into construct call.
    const int argc = 1;
    Local<Value> argv[argc] = { args[0] };
    return scope.Close(constructor->NewInstance(argc, argv));
  }
}

Handle<Value> WLNodeWrap::Add(const v8::Arguments& args) {
    HandleScope scope;

    // Standard unpacking of strings
    Local<String>           String = args[0]->ToString();
    String::Utf8Value       ustring{ String };
    std::string             str{ *ustring };

    // Unwrap WL to `This'
    WLNodeWrap* This = node::ObjectWrap::Unwrap<WLNodeWrap>(args.This());

    // Talk to WL's member `wl_'
    This->wl_.add(str);
    
    return scope.Close(Undefined());
}

Handle<Value> WLNodeWrap::Size(const v8::Arguments& args) {
    HandleScope scope;

    // Unwrap WL to `This'
    WLNodeWrap* This = node::ObjectWrap::Unwrap<WLNodeWrap>(args.This());

    // Return WL::wl_'s return value to Javascript (as a number)
    return scope.Close( Number::New( This->wl_.size() ) );
}

17.4.2.1 Besprechung des Codes

  • Zeilen 1–17: Diese Zeilen sind direkt aus der node-Dokumentation

  • Zeilen 18–23: An dieser Stelle wird das Mapping vorgenommen, welches besagt, dass auf Javascript-Seite eine Methode add vorhanden sein wird, welche auf WLNodeWrap::Add zeigt. Dort wiederum wird dann auf das eigentliche add(), nämlich WL.add() weitergeleitet werden. (Das selbe gilt für size() [JS-Seite] -> WLNodeWrap::Add [JS-V8-C++-Bridge Ebene] -> WL::size() [echte C++-Ebene])

  • Die Zeilen 54–57 zeigen die Verbindung aus der node::ObjectWrap/JS-V8-C++-Bridge-Ebene zur “echten” C++-Ebene und methode:
    • zunächst wird das “eigentliche” Objekt auf node-Seite “ausgepackt” (Zeile 54) und dessen member wl_, die “eigentliche” C++-Klasse angesprochen, und auf dieser nun die Methode add(const std::string&) aufgerufen.
  • Jede Methode auf der V8-Ebene muss seinen Scope schließen und dieses als Rückgabewert zurückliefern. Zeile 59 zeigt, wie man ein “void” zurückgeben kann.

  • Die Zeilen 66–69 zeigen erneut, wie sich size()/Size()/size() von Javascript bis in die C++-Ebene verknüpfen lassen.

  • Die Zeilen 48–51 führen den “klassischen Dreisatz” vor, wie Strings von Javascript bis in C++-Strings umgewandelt werden.

17.4.3 Schritt 3: binding.gyp, die build-Datei für node-gyp schreiben

Nicht zuletzt muss noch eine Datei binding.gyp angelegt werden, welche – ganz wie ein Makefile –, die build-Datei für node-gyp darstellt.

node-gyp ist ein spezielles build-Tool (vergleiche Build Systeme) für “node-addons” und kümmert sich um das notwendige linking zu den benötigten node- und v8-libraries.

Zum vorhandenen Projekt sieht die binding.gyp folgendermaßen aus (es geht in dieser binding.gyp v.a. auch darum, C++11 transparent, über verschiedene Betriebssysteme hinweg verwenden zu können):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{
  "targets": [
    {
      "target_name": "WLNodeWrap",
      "sources": [ "addon.cpp", "WLNodeWrap.cpp" ]
    }
  ],
  # global flags go here
  # `cflags_cc!` is supposed to mean: remove these flags
  # `cflags_cc` is supposed to mean: add these flags
  'conditions': [
      [
          'OS=="mac"', {
              'xcode_settings': {
                  'GCC_ENABLE_CPP_RTTI': 'YES',
                  'GCC_ENABLE_CPP_EXCEPTIONS': 'YES',
                  'CLANG_CXX_LANGUAGE_STANDARD': 'c++11',
                  'MACOSX_DEPLOYMENT_TARGET': '10.7',
                  'OTHER_CFLAGS':[ 
                    '-fPIC',
                    '-Wall',
                    '-Wextra',
                    '-Wno-attributes',
                    '-Wno-pointer-arith'
                  ]
              }
          }
      ],
      [
          'OS=="linux"', {
              'cflags_cc!': [ '-fno-rtti' ],
              'cflags_cc': [
                '-frtti',
                '-fPIC',
                '-fexceptions',
                '-std=c++11',
                '-Wall',
                '-Wextra',
                '-Wno-attributes',
                '-Wno-pointer-arith'
              ]
          }
      ]
  ]
}

Hiermit ist es also möglich, wie oben gezeigt, die C++-Klasse WL von Javascript aus anzusprechen:

1
2
3
4
5
6
7
8
9
10
var addon = require('./build/Release/WLNodeWrap');

var wl = new addon.WLNodeWrap();

wl.add('foo');
wl.add('bar');
wl.add('baz');

var size = wl.size();
console.log( size ); // 3

18 Design Patterns

18.1 MVC

18.2 MVVM

18.3 MV*

18.4 (Teile aus SIS)


19 Test Driven Development

19.1 Motivation und Vorteile

Die Vorteile von Test-geleiteter Entwicklung sind mannigfaltig. Unter anderem erlaubt eine Test-Suite folgendes:

  • Es ist einfach nach der Kompilierung einen sogenannten “Sanity-Test” zu machen, um zu überprüfen, ob alle Teile auf dem target-host genau so funktionieren wie sie es auf der Entwicklungsmaschine getan haben und alle Teile richtig kompiliert wurden und verfügbar sind
  • Es ist einfach, später neue Funktionalitäten einzubauen, da stets das Sicherheitsnetz besteht, zu sehen, ob die Veränderung am Code möglicherweise an anderer Stelle einen Fehler auslöst
  • Es ist wesentlich einfacher, sein eigenes Programm zu debuggen, und man benötigt wesentlich weniger sog. “printf-debugging”, da die (vielen kleinen) tests mögliche Fehler sehr gut lokalisieren und eingrenzen. In den meisten Fällen wird durch gute (d.h. viele kleine) Tests die Ursache des Problems sehr schnell ersichtlich und es wird einem mühsames “dem Code hinterhersteigen” erspart.

Test Driven Development geht dabei einen zunächst ungewöhnlich erscheinenden Weg: es werden zuerst die (failenden) Tests geschrieben, und danach erst die dazu passende Implementation (also die Implementation, die den Test “pass”-en lässt).

Der Workflow im TDD ist also kurz gesagt folgender:

  1. Test schreiben der nicht klappen wird
  2. Passende Implementation schreiben, die den Test klappen lässt
  3. Ab hier: wieder bei 1) beginnen

Interessanterweise erlaubt TDD zumeist eine schnellere Entwicklung im Projekt, eine langfristig gesehen stabilere, ausgereiftere und nachhaltigere Code-Base und macht dabei auch noch Spaß.

Obwohl es zunächst paradox erscheinen mag, so gibt es zwar einen klaren Mehraufwand, zuerst die Tests und danach die Implementation zu schreiben, durch das vorherige schreiben der Tests aber

  • wird die Spezifikation noch einmal genauer überdacht,
  • liegt die Spezifikation auch in konkreter Form vor: spätere Entwickler sehen Teile des Codes “live” in der Verwendung
  • liegen die Test-Fälle bereits vor
  • macht man sich bereits implizit Gedanken über die spätere API und Verwendung der (noch zu schreibenden Klasse)
  • Können edge-cases wesentlich besser erkannt und daher auch getestet werden

19.2 Arten von Tests

Zumeist sind die sog. Unit-Tests die wichtigsten, bei einem größeren Projekt wie diesem jedoch ist es denkbar, volle Integration Tests, Sanity-Tests, etc. parat zu haben.

19.2.1 Unit-Tests

Ein Unit-Test bildet klassischerweise die Funktionalität einer Klasse in einer Isolierten Art und Weise ab.

Idealerweise bildet der Unit-Test zu einer Klasse zugleich auch ihre Spezifikation ab und prüft diese anhand der Fixtures durch.

19.2.1.1 Features/Fixtures

Angenommen, es gibt eine Klasse string, und diese Klasse soll eine Methode append(char c) besitzen, welche einen Buchstaben an den String anhängt.

Die Spezifikation sagt also:

  • es gibt eine Klasse string
  • string::append(char c) soll einen Buchstaben an den bestehenden String anfügen

Die Fixture enthält vorgegebene Eingaben und die erwarteten Ausgaben/Rückgabewerte. Die stringAppendFixture wird genau dieses verhalten abbilden:

In einer schematischen Darstellung:

class stringAppendFixture {
  string teststring1_append_in  = "foobar";
  string teststring1_append_out = "foobarc";
};

Jede Fixture ist einem Feature zugeordnet. Das Feature ist ein kleiner Testfall, der die Funktionen der getesteten Klasse verwendet, die Eingaben der Fixture an diese füttert und die Rückgaben anhand der Fixture überprüft.

Die Klasse stringFeature wird also die Spezifikation von string::append prüfen (erneut in schematischer Darstellung):

class stringAppendFeature {
  EXPECT(
       stringAppendFixture::teststring1_append_in.append('c')
    == stringAppendFixture::teststring1_append_out
  );
  // weitere tests der Spezifikation...
};

Diese Sorte von kleinen Tests zu schreiben ist sehr einfach und dabei aber hochgradig effektiv!

Bald nämlich wird einem auffallen, dass es sinnvoll wäre, zu testen, was passiert, wenn der string vorher leer ist:

class stringAppendFixture {
  string teststring1_append_in  = "foobar";
  string teststring1_append_out = "foobarc";
  string teststring2_append_in  = "";
  string teststring2_append_out = "c";
};

class stringAppendFeature {
  EXPECT(
       stringAppendFixture::teststring1_append_in.append('c')
    == stringAppendFixture::teststring1_append_out
  );

  // hier gibt es schon den zweiten Testfall!
  EXPECT(
       stringAppendFixture::teststring2_append_in.append('c')
    == stringAppendFixture::teststring2_append_out
  );
  // weitere tests der Spezifikation...
};

Erfüllt der Test die Spezifikation? Was wäre denn nun mit dem Fall, dass der String auf \n endet? Soll der char c hinter dem \n angefügt werden, das newline möglicherweise komplett ersetzen, oder sich womöglich vor das newline einsetzen?

Ein Test wird Klärung bringen, und, en passant die Spezifikation der Klasse string festigen! Sei die Spezifikation nun erweitert um folgende Regel(n):

  1. es gibt eine Klasse string
  2. string::append(char c) soll einen Buchstaben an den bestehenden String anfügen
  3. wenn der string leer ist, soll der neue string aus dem angefügten Buchstaben bestehen (im Prinzip nur ein Spezialfall von 2.)
  4. wenn der String auf ein newline endet, soll der character vor dem newline einfügt werden und das newline an letzter Stelle behalten werden

Die Tests könnten nun erneut refactored werden und folgendes ergeben:

class stringAppendFixture {
  std::map<string/*input*/, string /*output*/> fixture{
   { "foobar",    "foobarc"   },
   { "",          "c"         },
   { "foobar\n",  "foobarc\n" },
 };
};

class stringAppendFeature {
  const stringAppendFixture F;

  for (const auto & f: F.fixture) {
    EXPECT( f.first == f.second );
  }
};

19.2.2 Integration Tests

Integration Tests testen in den meisten Fällen das Zusammenspiel von einzelnen Klassen oder Software-Komponenten. Wie im vorherigen Abschnitt gezeigt wurde, sind diese einzelnen Klassen bereits durch die Unit-Tests in sich getestet. Der Integration Test nun testet diese einzelnen Kompomeneten in ihrem Zusammenhang, ihrer Kooperation etc. Diese Art von Tests wird aber zumeist eher ab einer umfangreicheren Größe des Projekts benötigt.

19.2.3 Sanity Tests

Neben Unit- und Integration-Tests gibt es auch noch die sog. Sanity-Tests. Diese Art von Tests validiert und prüft User-Eingaben, Datentypen und ähnliches auf korrekte Werte.

19.2.4 E2E Tests

E2E-Tests, bzw. End-to-end-tests sind sehr ähnlich zu Integration Tests, indem sie das Zusammenspiel der einzelnen Komponenten prüfen, haben aber eher ein konkretes Interaktions-Szenario des Benutzers im Blick: oft werden e2e Tests in Web-Frameworks und in der Entwicklung von Webseiten verwendet, um ein komplettes “Szenario” der Benutzer-Interaktion durchzuspielen und zu prüfen, ob dieses von Anfang bis Ende fehlerlos ablaufen kann.

Beispiel:

  • User geht auf Seite
  • User sucht etwas
  • User klickt ein Ergebnis an
  • User wird auf Seite des Ergebnisses geleitet
  • User will dort eine Eingabe machen
  • User muss angemeldet sein
  • User meldet sich an
  • User is angemeldet
  • User kann Eingabe machen
  • User kann Eingabe speichern

20 Bug tracker

20.1 Motivation

Der Bug-Tracker dient der gemeinsamen Verwaltung von Bugs und der Versionierung

Obwohl eigentlich einleuchtend und selbstverständlich haben “ordentliche” Bug-Tracking best practices eine Reihe entscheidender Vorteile:

  • Transparenz: Alle Mitarbeiter haben einen genauen Überblick über den Stand des Projekts / der einzelnen Komponenten
  • Milestones und wesentliche Entwicklungsschritte über Komponenten hinweg können besser erreicht werden, das gesamte Projekt sich schneller entwickeln
  • Genauigkeit: Durch die Transparenz und das Wegfallen von “Flüsterpost-Ketten” ist es wesentlich einfacher sich gemeinsam zu koordinieren
  • Genaue Rückgabe: Es ist zu jedem Zeitpunkt klar, was man den Philsophen gegenüber geliefert hat / was noch aussteht / wer gerade woran arbeitet.
  • Vererbbarkeit: Wenn sich die Zusammensetzung der Teams ändert, können nachfolgende Entwickler sich schnell und klar einen Überblick über die ausstehenden Probleme und bisherigen Lösungen verschaffen.

20.2 WAST-Workflow

Im folgenden eine Beschreibung der Konventionen und Vorgänge, wie bugs innerhalb von WAST gehandhabt werden sollen.

20.2.1 Konventionen

Bugs sollen ordentlich dokumentiert werden und getagged werden.

Taggen erlaubt es, die offenen bugs zu sortieren, filtern und damit besser im Überblick behalten zu können.

Ordentlich dokumentieren bedeutet, dass ein nachfolgender genau nachvollziehen kann, wie/wann/wer mit einem Bug umgegangen wurde.

Dazu gehören:

  • taggen: priorisieren (siehe unten, Prioritäten)
  • taggen: Status (siehe unten, Workflow)
  • dokumentieren: Wenn sich der Status eines Bugs ändert, am besten einen Kommentar im Bug-Verlauf hinterlassen, am besten so:
    • Beispiel: [Issued -> Analyze]. Dies würde bedeuten, dass sich Person Foo mit dem Bug auseinandersetzt und den tag Issued entfernt und durch den tag Analyze ersetzt hat. Dies bedeutet für alle anderen, dass:
      • sich bereits jemand um den Bug kümmert
      • andere können die noch offenen bugs bequem filtern und sehen, was als nächstes zu tun ist
    • Beispiel: [p1 -> p2].
      • Bedeutet: Person Foo hat den Bug herunterpriorisiert von P1 nach P2
      • Beispiel: [+p1]
        • Bedeutet: Person hat P1-tag an den Bug hinzugefügt

20.2.2 Prioritäten

Bugs sind am besten Priorisiert von “P1” bis “P4”.

Die Prioritäten entsprechen ungefähr folgenden Ideen:

Prioritäten bei Bugs
Priorität Label Charakterisierung Beschreibung
1 P1 Showstopper. Muss sofort behoben werden. Kritischer Bug.
2 P2 Wichtiger Bug. Sollte ebenfalls schnellstmöglich behoben werden. Wichtiger Bug.
3 P3 Durchaus wichtiger Bug. Durchaus wichtiger Bug, aber nicht unbedingt wesentlich solange noch P1 und P2 Bugs vorliegen. Können auch Feature Requests sein, die dann zumeist zu P4-Bugs werden.
4 P4 Nicht so wichtiger Bug. Meist “Nice To Have” Feature Requests oder wird häufig vergeben für “Next Release/Milestone”, d.h. dieses Feature wird wohl erst im nächsten Milestone umgesetzt werden.

20.2.3 Workflow

Der Workflow orientiert sich an folgendem Modell: Der Bug geht durch verschiedene Phasen, die unterschiedliche Bedeutung haben. Diese werden immer per tag signalisiert. So können andere ablesen, dass jemand an einem bug arbeitet. fasst dies zusammen.

Bug Tracking Workflow Status

Bug Tracking Workflow Status

20.2.4 Spezialität: Umbrella-Bug

Wenn es mehrere “related” Bugs zu einem größeren Thema gibt, so bietet es sich an einen umbrella-Bug zu definieren, der alle diese verstreuten, kleineren Bugs zusammenfasst und miteinander referenziert. Die wird am Einfachsten so gehandhabt, dass:

  • der umbrella-Bug bekommt einen tag ‘umbrella’
  • der umbrella bug bekommt eine Bezeichnung/Beschreibung ‘umbrella bug’
  • der umbrella bug referenziert die die related-bugs, indem er tags bekommt mit den bug-id’s der related bugs, also, z.B. ‘#19, #22, #53’. Damit wäre klar, dass der umbrella-bug die bugs mit den Nummern 19, 22 und 53 zusammenfasst

Wenn man in der Beschreibung des Umbrella Bugs eine kleine Beschreibung anlegt, die sagt ‘this is an umbrella bug for bugs #19, #22, #53’, so wird in den referenzierten Bugs dieses automatisch von gitlab eingetragen: ‘this bug was mentioned in bug XY’. Dies ist ziemlich praktisch.

21 Versionierung

In praktisch jedem Software-Projekt ist Versionskontrolle unerlässlich um Sicherheit zu garantieren und Übersichtlichkeit zu wahren.

Hinsichtlich der Auswahl von Versionskontrollsystemen gibt es eine Vielzahl von Produkten, die sich verwenden lassen:

21.1 git

Die meisten Teile der WAST-Landschaft sind mit git versioniert da dieses System die meisten Vorteile bietet.

Als beliebteste Versionskontrollsysteme sind natürlich svn und git zu erwähnen. git ist ein “version control system (vcs)” wie svn, bzw. ein source control management System (scm), mit dem entscheidenden Unterschied, dass es im Gegensatz zu svn ein verteiltes (distributed) vcs ist.

Diesen entscheidenden Unterschied zu begreifen kann durchaus einen Moment brauchen und die Umstellung, bis man sich in git wirklich “zu Hause” fühlt, kann einen Moment dauern.

Trotz allem aber hat git die Entwickler-Welt im Sturm erobert und das aus guten Gründen.

Die Kombination aus git und gitlab als online-Kollaborations-Oberfläche bietet eine Menge an überzeugenden Vorteilen für die gemeinsame Entwicklung von Software, wie im folgenden kurz dargestellt werden soll.

21.1.1 Vorteile von git

21.1.1.1 branching und merging

Das freie “git Book”53 schreibt dazu:

The Git feature that really makes it stand apart from nearly every other SCM out there is its branching model.

Git allows and encourages you to have multiple local branches that can be entirely independent of each other. The creation, merging, and deletion of those lines of development takes seconds.

This means that you can do things like:

Frictionless Context Switching. Create a branch to try out an idea, commit a few times, switch back to where you branched from, apply a patch, switch back to where you are experimenting, and merge it in.

Role-Based Codelines. Have a branch that always contains only what goes to production, another that you merge work into for testing, and several smaller ones for day to day work.

Feature Based Workflow. Create new branches for each new feature you’re working on so you can seamlessly switch back and forth between them, then delete each branch when that feature gets merged into your main line.

Disposable Experimentation. Create a branch to experiment in, realize it’s not going to work, and just delete it - abandoning the work—with nobody else ever seeing it (even if you’ve pushed other branches in the meantime).

Notably, when you push to a remote repository, you do not have to push all of your branches. You can choose to share just one of your branches, a few of them, or all of them. This tends to free people to try new ideas without worrying about having to plan how and when they are going to merge it in or share it with others.

There are ways to accomplish some of this with other systems, but the work involved is much more difficult and error-prone. Git makes this process incredibly easy and it changes the way most developers work when they learn it.54

21.1.1.2 effizient, klein und schnell

Small and Fast Git is fast. With Git, nearly all operations are performed locally, giving it a huge speed advantage on centralized systems that constantly have to communicate with a server somewhere.

Git was built to work on the Linux kernel, meaning that it has had to effectively handle large repositories from day one. Git is written in C, reducing the overhead of runtimes associated with higher-level languages. Speed and performance has been a primary design goal of the Git from the start.55

21.1.1.3 verteilt

Distributed One of the nicest features of any Distributed SCM, Git included, is that it’s distributed. This means that instead of doing a “checkout” of the current tip of the source code, you do a “clone” of the entire repository.

Multiple Backups This means that even if you’re using a centralized workflow, every user essentially has a full backup of the main server. Each of these copies could be pushed up to replace the main server in the event of a crash or corruption. In effect, there is no single point of failure with Git unless there is only a single copy of the repository.

Any Workflow Because of Git’s distributed nature and superb branching system, an almost endless number of workflows can be implemented with relative ease.56

21.1.1.4 Content-tracking rather than file-tracking

Whereas every other version control system in the world tracks file history (when lines were added to a file, when lines were removed, when a file was renamed, added or deleted etc) Git doesn’t explicitly track this information in any way at all. It instead tracks content within a repository. By walking the repository history Git can reconstruct what happened to a particular file, but this is an extrapolation from the data rather than something explicitly encoded.

This is a radical departure that means that when you do something like cutting a method from one file and pasting it into another then the history of the method goes along with it (think svn blame). This is explained by Linus Torvalds in this post; see also this separate post on tracking renames.

First off, let’s just posit that “files” do not matter. The only thing that matters is how “content” moved in the tree. Ok? If I copy a function from one file to another, the perfect SCM will notice that, and show it as a diff that removes it from one file and adds it to another, and is still able to track authorship past the move.57

21.1.1.5 Low barrier to participation

Nicht zuletzt erlaubt es git auf einfachste Weise ein eigenes vollständig-funktionierendes code-repository aufzusetzen. Bei svn wird für diesen Schritt ein server mit laufendem svn benötigt – und ist damit zumeist an einen Administrator gebunden, z.B. um ein neues repository zu erstellen.

Ein einfaches git init --bare reicht aus, um ein vollständiges git repository anzulegen, auf welches man seine Änderungen pushen und mit anderen kollaborieren kann. Im Endeffekt ist dies bereits ein kleiner “git server” – wenn man so will:

It is incredibly easy to start managing code with Git. Unlike Subversion there is no need to set up a repository on a (possibly remote) server before starting to work. Git is even simpler than SVK, because what would normally be considered a “working copy” actually becomes a (distributed) repository itself, containing both the “working copy” and all of the history of the repository58

21.1.2 git basics

Um schneller mit git arbeiten zu können werden im folgenden die wesentlichen ersten Schritte detailliert erklärt und erläutert. Auf jeden Fall wird empfohlen, weitere Literatur zu diesen Themen zu konsultieren.

21.1.2.1 Überblick

Im Gegensatz zu svn gibt es bei git mehr Ebenen als nur “local repository” und “upstream repository”.

auf Seite gibt ein Überblick59 über die unterschiedlichen Ebenen und die Interaktion zwischen denselben:

Git overview

Git overview

21.1.2.2 Beispiel: Szenario 1: ein eigenes repository erstellen

Nachdem man seinen Namen und seine email-Adresse in git konfiguriert hat, kann man bereits beginnen, produktiv zu arbeiten.60

Nehmen wir als Beispiel ein Projekt genannt eos, das Satzende-Erkennung behandeln soll.

Als erstes legen wir ein Verzeichnis an, in dem das Programm entwickelt werden soll.

> mkdir eos && cd $_

Nun legen wir eine Datei README an, welche die wesentlichen Punkte des repositories erläutert:

> cat <<HERE  >README
eos
---

an end-of-sentence recognizer
HERE

Nun machen wir aus dem reinen Verzeichnis ein repository

> git init

In diesem Befehl wird git ein (verstecktes) Verzeichnis .git in unserem Projekt anlegen, also in eos/.git, in welchem es das repository verwaltet.

Als nächstes machen wir sogleich den ersten commit, um unserem repository eine “Geschichte” zu geben – ab hier beginnt nun also die Versionierung unseres Projekts:

> git commit -am 'initial commit'

Dabei stellen die (kombinierten) Kommandozeilen Switches folgendes dar:

-a
Füge alle git bereits bekannten Dateien zum commit hinzu (bisher haben wir solche noch nicht, aber gewöhnlich wird immer ein git commit -a -m verwendet werden)
-m MESSAGE
die commit message, welche in den log eingetragen wird

Als nächstes werden wir gleich unsere erste Datei in das repository einfügen, wollen davor aber noch kurz den aktuellen status des repository betrachten:

> git status

Hierauf wird git etwa folgendes antworten (je nach Spracheinstellung):

# On branch master
#
# Initial commit
#
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   README
nothing added to commit but untracked files present (use "git add" to track)

Was dies im einzelnen bedeutet:

  1. Wir befinden uns auf dem branch master (mehr zum Thema branching, s.u.)
  2. Das repository ist noch vollständig “initial”, d.h. es wurden noch keine Dateien hinzugefügt
  3. Es gibt “untracked files”, also Dateien, die im Verzeichnis liegen, git aber nicht bekannt sind. Diese Dateien können git auf zweierlei Arten bekannt sein
    1. Sie wurden dem repository hinzugefügt durch git add
    2. git wurde mitgeteilt diese Dateien im repository-Pfad zu ignorieren (mit Hilfe der Datei .gitignore)
  4. wie die Dateien hinzugefügt werden können, sagt git auch gleich: durch git add und zählt die ihm unbekannten Dateien auf (in diesem Fall lediglich die Datei README).

Nun also zum Hinzufügen der ersten Datei:

> git add README

Mit diesem Befehl wurde die Datei aber noch nicht endgültig in das repository aufgenommen: vielmehr wurde die Datei in die sog. staging area (In auf Seite “Index” genannt) übernommen und wird beim nächsten commit endgültig in das repository aufgenommen werden.

Ein weiterer Vergleich des vorherigen Status mit dem jetzigen Status verdeutlicht dies:

> git status

…liefert uns nun folgenden Output:

# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#   new file:   README
#

Da dies die einzige Änderung ist, die wir in diesem Changeset ausführen wollen (iwr könnten auch noch weitere Dateien adden und erst dann commiten), machen wir einen commit:

> git commit -am 'add documentation'

Nun wird…

> git status

…folgendes melden:

# On branch master
nothing to commit (working directory clean)

d.h. wir befinden uns einem “clean-state”, d.h. alle aktuellen Änderungen sind bekannt und es sind keine weiteren Änderungen an irgendwelchen Dateien unseres repository vorgenommen worden.

Um diese letzteren Änderungen zu verfolgen betrachten wir als nächstes den Output von git log:

commit 9112df662257116774d946cb5482116263cbc511
Author: AUTORENNAME <EMAILADRESSE></emailadresse>
Date:   DATUM

    add documentation

Anders als svn hat git keine “klaren” Revisions-Nummern wie r1 etc., sondern statt dessen 40-stellige SHA-Summen für jeden commit.

Dies hat mehrere Vorteile (vergleiche Daten-Sicherheit[^siehe oben, und hier [[link einfügen]]]), aber auch einen entscheidenden Nachteil: sie sind menschlich schlecht lesbar.

Es gibt mehrere Möglichkeiten, sich nicht diese 40-stelligen Nummern merken zu müssen, um, z.B. um mit jemand anderen über eine bestimmte Revision reden zu können.

Hierzu gibt es verschiedene Strategien, die git anbietet, z.B.: Abkürzen der SHA-Summe auf wenige Stellen (solange der damit gemeinte commit eindeutig bleibt). In der Regel einigt man sich auf die ersten 6 Stellen wenn man über einen konkreten commit sprechen will, in diesem Fall also:

9112df

Nebenbemerkung für Interessierte: Es gibt einen sogenannten git plumbing command (sozusagen ein Kommando um mit git selbst zu arbeiten, das aber gewöhnlich nicht im Alltag gebraucht wird (dies wären die porcelain commands)), das anzeigt, was git aus einer solchen abgekürzten Revisions-Nummer herausliest:

> git rev-parse 9112df
9112df662257116774d946cb5482116263cbc511

Eine weitere Strategie, Revisionsnummern menschlich zugänglicher zu machen, besteht darin, Aliases zu verwenden.

Dies kann geschehen durch:

  1. das Alias HEAD, HEAD^, HEAD^^ um jeweils auf den letzten, vorletzten, vor-vor-letzten commit zu referenzieren
  2. oder durch die Aliase HEAD@{1}, HEAD@{2} um auf die selben commits zu referenzieren
  3. durch tags: später werden wir sehen, wie man bestimmte Revisionen explizit “benennen” kann durch git tag. So könnte man also einen ganz bestimmten 40-stelligen commit als v1.2.3 taggen und somit genau das gleiche meinen.

Als nächstes wollen wir in der Entwicklung unseres Satzende-Erkenners fortfahren und dabei eines von git’s wichtigsten features nutzen, das branching, d.h. wir zweigen von der Hauptentwicklungslinie weg, um eine neue Funktionalität zu entwickeln und werden diese, sobald sie fertig ist, wieder in die Hauptentwicklungslinie zurückführen (mergen). git ist im Vergleich zu svn sehr sparsam beim Anlegen von neuen branches und fördert das Anlegen und mergen von branches sehr.

Als erstes wollen wir einmal gucken, ob es denn vielleicht zufällig schon branches gibt:

> git branch
* master

Offensichtlich gibt es derzeit nur einen branch, und zwar den branch master.

Vorsicht! Git zeigt in diesem Fall nur unsere lokalen branches an, nicht aber potentielle branches die auf servern liegen! Bisher haben wir noch keine sog. remotes (also entfernte repositories, wie zum Beispiel ein “Zentral-”Repository) eingetragen, aber um potentiell “entfernte” branches anzeigen zu lassen, brauchen wir folgenden Switch:

> git help branch
[...]
-a, --all
    List both remote-tracking branches and local branches.
[...]

Da aber keine remotes existieren wird das Ergebnis von git branch -a also das selbe ergeben wie git branch (nur lokale branches anzeigen):

> git branch -a
* master

Nun also soll endlich ein branch für die Entwicklung mit dem Namen dev angelegt werden:

> git branch dev

Ein erneuter check, welche branches existieren, zeigt:

> git branch
dev
* master

…was bedeutet, dass es einen branch namens dev gibt, der aktuelle branch aber noch auf master steht. Das wollen wir ändern:

> git checkout dev
Switched to branch 'dev'

Wie erwartet, haben wir in den branch dev gewechselt und können dies auch überprüfen:

> git branch
* dev
  master

Hier also können wir neue Funktionalität entwickeln, ohne die Hauptentwicklungslinie zu beeinträchtigen.

Legen wir also als nächstes unsere ersten Dateien des Satzendeerkenners an, mit folgenden Inhalten:

> cat eos.pl
#!/usr/bin/env perl -0 -p
s/
 (
  (
   (
    (Mr|[[:upper:]]|\d+)\.  # Ausnahmen: Abkürzungen etc.
    |
    .+?                     # alles andere (non-greedy,
                            #  d.h. 1. nicht [.!?] gefolgt von SPACE + Großbuchstabe)
                            #       2. keine Abkürzung
   )+?                      # eine Folge von Abkürzungen oder "alles"
   [.!?](?=\s+[[:upper:]])  # EOS
  )
 )
/$1<EOS>/gx;
EOS

> cat eos.t
Ein Satz? Hier auch! Am 19. August 1972 schläft
Mr. G. W. Spencer. Hier beginnt ein Satz.  In der
Oettingenstr. 67 stehen Computer.
Perl arbeitet hier zeilenweise, Sätze dürfen nicht über Zeilen gehen.
TXT

Als nächstes machen wir das Programm ausführbar:

> chmod u+x eos.pl

Und legen noch ein Verzeichnis für Ergebnisse von unterschiedlichen Testläufen an – diese Ergebnisse und das gesamte zugehörige Verzeichnis werden wir aber nicht in unseren commits haben wollen und ignorieren:

> mkdir var

> cat eos.t | ./eos.pl > var/res.1

Die Ausgabe von git status ist mittlerweile also schon vertraut:

> git status
# On branch dev
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   eos.pl
#   eos.t
#   var/

git teilt uns also mit, dass es zwei Dateien und ein Verzeichnis in unserem Projekt gibt, dass es noch nicht kennt, die wir aber per git add zu unserer Versionskontrolle hinzufügen können.

Da wir das Verzeichnis für Zwischenergebnisse, aber nicht tracken und lieber ignorieren wollen, tun wir folgendes: wir legen eine Datei .gitignore an, die alle Dateien und Verzeichnisse listet, die git ignorieren soll und fügen diese in das repository hinzu.

echo var/ >> .gitignore

Nun zeigt git status foldenes interessantes Ergebnis:

git status
# On branch dev
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   .gitignore
#   eos.pl
#   eos.t

git hat schon die Datei .gitignore ausgewertet und zeigt das Verzeichnis var gar nicht mehr als potentiell-hinzuzufügende Datei an!

Um schöne kleine commits zu haben (die man im Notfall später leichter “zurückspielen” kann), werden wir die fehlenden Dateien nur Stück für Stück zum repository adden:

> git add .gitignore

> git commit -am 'add gitignore'
[dev 1f986bd] add gitignore
 1 file changed, 1 insertion(+)
 create mode 100644 .gitignore

> git status
# On branch dev
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#   eos.pl
#   eos.t
nothing added to commit but untracked files present (use "git add" to track)


> git add eos.*

> git commit -am 'add first version of eos + test data'
[dev 46b34ab] add first version of eos + test data
 2 files changed, 19 insertions(+)
 create mode 100755 eos.pl
 create mode 100644 eos.t

> git status
# On branch dev
nothing to commit (working directory clean)

Nun können wir bereits die kurze History unseres repository anschauen, z.B. in diesem Format:

> git log --oneline --decorate --graph
* 46b34ab (HEAD, dev) add first version of eos + test data
* 1f986bd add gitignore
* 9112df6 (master) add documentation

…und sehen hierbei die Bezeichnungen für die jeweiligen branches auf denen die Änderungen gemacht wurden, und den aktuellen HEAD.

Interessant wird nun folgendes Experiment: Wir lassen uns ganz gewöhnlich von der shell den Inhalt des Verzeichnisses anzeigen, wechseln dann in den master-branch und schauen uns den Verzeichnisinhalt erneut an und vergleichen beide Ergebnisse.

> git branch
* dev
  master

> ls -1
README
eos.pl
eos.t
var

> git checkout master
Switched to branch 'master'
> ls -1
README
var

> git checkout dev
Switched to branch 'dev'
> ls -1
README
eos.pl
eos.t
var

Das Ergebnis ist durchaus aus verblüffend zu bezeichnen: git hat tatsächlich auf File-System-Ebene (!) die Dateien “ausgetauscht” und das Verzeichnis komplett verändert.

Als nächstes nun wollen wir die Entwicklung unseres eos hier beenden und den dev-branch nun in den master-branch zurück-mergen. Dafür werden wir folgendes tun:

  1. Auf den master branch wechseln

    > git checkout master
    Switched to branch 'master'
    
    > git status
    # On branch master
    # Untracked files:
    #   (use "git add <file>..." to include in what will be committed)
    #
    #   var/
    nothing added to commit but untracked files present (use "git add" to track)
  2. den dev-branch mergen

    > git merge dev
    Updating 9112df6..46b34ab
    Fast-forward
     .gitignore |  1 +
     eos.pl     | 15 +++++++++++++++
     eos.t      |  4 ++++
     3 files changed, 20 insertions(+)
     create mode 100644 .gitignore
     create mode 100755 eos.pl
     create mode 100644 eos.t
  3. Die vorhandenen Dateien prüfen

    > ls -1
    README
    eos.pl
    eos.t
    var
  4. Den log prüfen

    > git --no-pager log --graph
    * commit 46b34ab61c7f23ed6d4fbb2baf7dca468bbfb07d
    | Author: NAME <EMAIL>
    | Date:   DATE
    | 
    |     add first version of eos + test data
    |  
    * commit 1f986bd9d98e04aa5ea73a68ea9cbf83a736da7a
    | Author: NAME <EMAIL>
    | Date:   DATE
    | 
    |     add gitignore
    |  
    * commit 9112df662257116774d946cb5482116263cbc511
      Author: NAME <EMAIL>
      Date:   DATE
    
          add documentation
  5. den dev-branch löschen (sonst entsteht bald Chaos!)

    > git branch -d dev
    Deleted branch dev (was 46b34ab).

Zu guter letzt wollen wir die Ergebnisse auf ein remote-repository pushen.

Dafür checken wir erstmal ob remotes eingetragen sind:

> git remote -v 
[kein output]

Daher tragen wir unsere erste remote-Adresse ein und nennen sie origin:

> git remote add origin name@machine:path/to/bare/repository.git
> git remote -v
origin  name@machine:path/to/bare/repository.git (fetch)
origin  name@machine:path/to/bare/repository.git (push)

…dabei funktionieren die Angaben für remote-repositories genau wie beim ssh-Protokoll:

  • name@machine für den Zugang
  • gefolgt von eienm Doppelpunkt :
  • gefolgt vom Pfad zum repository path/to/bare/repository.git
  • (in diesem Fall deutet der repository-Name repository.git an, dass dies ein sog. git “bare” repository ist, dazu an anderer Stelle mehr).

Wenn das Verzeichnis existiert und die Pfadangabe stimmt, lässt sich nun auf dieses repository pushen:

> git push origin master

Hierbei pushen wir unseren branch master auf das remote-repository mit dem Namen origin.

Äquivalent dazu ist der pull, d.h. Änderungen (die womöglich von anderen) in das remote-repository ge-push-t worden sind, abzuholen:

> git pull origin master

21.1.3 git-extras

git-extras (visionmedia 2014) geben viele nützliche (neue) Kommandos zu den Standard git-Kommandos hinzu.

Dadurch kürzen sie einige Wege ab, oder erleichtern andere, routine-mäßige Aufgaben.

Da sie aber mit Hilfe der von git selbst zur Verfügung gestellten Kommandos implementiert sind, könnte man selbstverständlich alles auch ohne git-extras machen.

Dennoch können die git-extras als eine sinnvolle Empfehlung betrachtet werden.

21.1.3.1 Installation auf CIP-Rechnern

Um die git-extras auf den CIP-Rechnern zu installieren kann man folgendes Verfahren nutzen:

git clone https://github.com/visionmedia/git-extras
cd git-extras
make DESTDIR=$HOME PREFIX="" install

Ich empfehle sehr, die git-extras61 zu verwenden und schlage folgendes Branching-Model vor:

  • Der master branch wird nur für fertiggestellte Funktionalität verwendet
  • gearbeitet wird auf dev/feature/bug und refactor branches
  • dann merges oder pull requests
  • zuletzt ein release

Mit git-extras ist das wirklich einfach: Anstatt selbst einen Branch anzulegen, auf diesen zu wechseln, dort zu arbeiten, dann diesen Branch in den master-Branch zu mergen, bietet sich u.a. folgender Workflow an:

pltk@master$              git feature lemmatizer
pltk@feature/lemmatizer$  [hack hack (möglichst kleine Commits bitte!)]
pltk@feature/lemmatizer$  git commit -am 'last commit'
pltk@feature/lemmatizer$  git checkout master
pltk@master$              git feature finish lemmatizer

Bei Bug-Fixes schlage ich folgendes Modell vor:

# [In gitlab ist ein issue im bugtracker eröffnet]
pltk@master$        git bug issue-66
pltk@bug/issue-66$  [ hack hack ...]   
pltk@bug/issue-66$  [ letzter commit ]
pltk@bug/issue-66$  git checkout master
pltk@master$        git feature finish issue-66

22 Semantic Versioning

Die Versionierung (in Zusammenarbeit mit der gemeinsamen Bug Verwaltung) garantiert Referenzierbarkeit hinsichtlich der Zitierbarkeit für die Philosophie (und darüber hinaus).

Beispiel:

22.1 Versions-Schema

22.1.1 Ubuntu-Schema

Das Versionsschema von WAST orientiert sich nach außen am Versions-Schema von Ubuntu. Dabei wrid die Versionsnummer zusammengesetzt aus der Jahreszahl und dem Monat des Releases.

Im oben gegebenen Beispiel hat sich X mit v14.02 auf die Version von WAST zum Zeitpunkt Februar 2014 bezogen. Die Version von November 2015 wird also 15.11 sein. Dabei ist ein monatlicher Release-Cycle gegeben.

Wie gesagt, dies ist die Versionierung nach außen, d.h. im Zusammenspiel aller Komponenten. In der Versionierung nach innen sollte sich jede der Komponenten selbst jedoch am Besten am Schema des Semantic Versioning orientieren.

Dementsprechend kann ein Release und die Version desselben genauer unterteilt werden:

Beispiel:

  • Release 14.02 könnte nämlich in diesem Sinne eine Zusammenstellung der folgenden Komponenten gewesen sein:
    • WAB-Transkription 2.3.1
    • wittfind-Komponente 4.2.3
    • sis-Komponente 3.9.1

22.1.2 Semantic Versioning

Im wewsentlichen versteht man unter semantic versioning eine Versionsangabe mit folgendem Format: Major.Minor.Patch.

Dabei stehen Major-Versionen für (z.T.) untereinander nicht-kompatible APIs oder große Veränderungen/Verbesserungen am Code, bzw. Verweisen auf wesentliche Entwicklungsschritte im Code.

Die Zahl unter Minor bezeichnet den aktuellen Entwicklungsstand des Codes. In den meisten Fällen werden als Minor ungerade Zahlen verwendet um anzudeuten, dass dies eine Entwickler-Version ist, und, nachdem diese Entwicklung eines bestimmten Features (oder ähnlichem) abgeschlossen ist, diese dann um eins hochgezählt zu einer geraden Zahl.

Nicht zuletzt wird Patch bei bugfixes hochgezählt.

Vergleiche z.B.

  • Perl, Version 5.19.4 (Perl5, Weiterentwicklung von 5.18 (als Entwicklerversion), patch-level 4)
  • nodejs, Version 0.10.20 (Noch nicht als 1.0.0-fähig anerkanntes, d.h. noch nicht komplett stabilisiertes, dennoch fortgeschrittenes Projekt in einer 0.10-er-“stable version” mit patch-level 20)

Mehr Informationen zu Sematic Versioning finden sich bei http://semver.org/(Tom Preston-Werner 2013)

23 Lizenzen

Bei allen Entwicklungen – sowohl im kommerziellen wie auch in der “Public Domain” –, ist die Frage nach Lizenzen als sehr wichtig einzustufen.

Die Geschichte der “Open Source”- und “Free/Libre Software”-Lizenzen ist sehr interessant und in erster Linie hier nachzulesen [Lessig, Lessig, Stallman, Moody].

Kurz gesagt mussten sich die “Hacker” auch in Lizenzmodelle einarbeiten und diese “hacken” um ihre Software zu schützen, daher die vielen verschiedenen, z.T. sich widersprechenden und komplizierten Lizenzmodelle.

Im folgenden ein Kurzüberblick über die meistverwendeten Lizenzen und deren Charaktersitika – dies aber vollständig “ohne Gewähr”, um bei Entwicklungen innerhalb von WAST die richtige Lizenz wählen zu können.

[Lizenz-Wizard auch verlinken]

23.1 GNU GPL

23.1.1 GNU GPLv2

23.1.2 GNU GPLv3

23.1.3 GNU LGPL

23.1.4 GPL-Compaitibility

23.1.5 Affero GPL

23.1.6 GPLv2 Loophole

23.2 MIT

23.3 Creative Commons

23.3.1 CC-Bausteine

23.3.2 NC-BY-SA

23.3.3 NC-BY …

23.4 WTFPL

24 Mailingliste


25 Übungen

Grundlegendes zu den Übungen:

Warum:

25.1 Übung 1: git und Kollaboration

  • aktivieren Sie den für Sie von der IFI bereitgestellten gitlab-Account bei rz.ifi.lmu.de
    • Stellen Sie dabei sicher, dass ihre (???) weitergeleitet werden, oder klicken Sie den Aktivierungslink, der an ihre cip.ifi-Adresse geschickt wurde, durch Verwendung von webmail2.cip.ifi.lmu.de oder webmail.cip.ifi.lmu.de, wenn Sie der festen Meinung waren, dass Sie ihre Mails zwar weiterleiten würden, aber unverständlicherweise keine bekommen haben
  • generieren sie ssh-Keys (mit oder ohne Passphrase), und legen Sie ihren Public(!)-Key an der dafür vorgesehenen Stelle in gitlab ab (im Notfall lesen Sie Dokumentation! Einmal: ‘generate ssh keys’ und die gitlab-Dokumentation: ssh keys adden.)

  • nun loggen Sie sich in gitlab ein
    • und forken sie das bisher dort eingestellte Kursmaterial in ein eigenes repository
    • clonen Sie dieses Repository auf ihren Rechner (von dem aus Sie ihren Public-Key erstellt und hochgeladen haben)
  • Installieren Sie die git-extras auf Ihrem und/oder den cip-Rechnern (dann wie [hier] beschrieben). Dieser Schritt ist empfohlen aber nicht zwingend: alles was die git-extras machen, lässt sich mit git alleine auch machen.

  • Kreieren Sie einen branch in dem ge-clone-ten repository
  • Kreieren Sie ein Verzeichnis mit ihrem cip-ifi-Namen
  • Schreiben Sie eine Datei im Markdown-Format in ihrem Ordner, in dokumentieren Sie in dieser ihre bisher geleisteten Schritte – so dass andere von Ihnen lernen könnten und klar ist, was Sie getan haben – es steht Ihnen dabei selbstverständlich frei, hierfür pandoc zu verwenden und damit zu spielen

  • push-en Sie ihren Branch nach origin
  • Wechseln Sie zu ihrem master-branch zurück und mergen Sie ihren erstellten Branch dort hinein (mit einem Kommentar der in etwa sagt, ‘merge branch Foo’)
  • tag-gen Sie Ihr ge-merge-tes Changeset mit einer semantischen Versionsnummber ihrer Wahl (c.f. das Kapitel über semantisches Verionieren; also in etwa 0.1.0 oder v0.1.0)
  • push-en Sie Ihren neuen Master-Branch nach origin
  • push-en Sie Ihren neuen tags nach origin
  • Bieten Sie einen pull-request zum upstream-Repository an (das Repository, dass Sie am Anfang ge-clone-d hatten) und bitten Sie um Übernahme ihres “Patches”

  • Bonus:
    • wenn Sie sich ohne Passwort mit ssh auf den cip-Rechnern einloggen wollen, dann fügen Sie ihren Public-Key dort zu ~/.ssh hinzu, entweder per Hand, oder mit ssh-copy-id (Ihre Distribution liefert dieses möglicherweise schon mit, oder Sie können es sich bequem installieren wenn Sie möchten)
  • Bonus:
    • clonen Sie das repository auf den CIP-Rechner, arbeiten Sie von Remote zu Hause, ändern Sie eine Datei ab, und pushen diese ins gitlab.
  • SSH-Bonus:
    • legen Sie eine Datei ~/.ssh/config an und ermöglichen Sie es sich dadurch ssh cip statt ssh nutzer@cip.ifi.lmu.de sagen zu können, um sich erfolgreich an den cip-Rechnern einzuloggen
  • Remote-Working-Bonus:
    • machen Sie sich mit screen, tmux oder byobu (als Frontend für eines der vorgestellten vertraut) und ermöglichen Sie es sich dadurch, Terminal-Sessions auf den cip-rechnern laufen lassen zu können und zu diesen später zurückkehren zu können.
  • Bonus:
    • arbeiten Sie mit autossh oder dem Perl-Modul App::assh und versuchen Sie, eine permanente Verbindung zu den cip-Rechnern halten zu können (schalten Sie dafür Ihr Internet ab- und wieder-an, und gucken Sie, ob die Verbindung automatisch wieder hergestellt wird)

25.2 Übung 2: C-Wrapping

Schreiben Sie eine C++-Klasse, die die volle Funkionalität des folgenden C-struct wrapped und abbildet (inklusive der Unterscheidung private/public Methode), mit vollen Konstruktoren und Destruktoren, die das C-Struct automatisch allokieren und de-allokieren

Das C-struct halt folgende Spezifikation:

typedef struct tWordList {
    std::vector<std::string> words_;
} WordList;

// CTOR / DTOR
extern WordList * WordListInit();
extern void WordListFree(WordList*);

// "public methods"
extern void WordListAdd(WordList*, const char*);
extern int WordListSize(WordList*);

// "private methods"
static std::string WordListUpgradeWord(WordList*, const char*);

Schreiben Sie zunächst eine C++-Klasse, die:

  • Einen Konstruktor und einen Destruktor enthält
  • einen (smart-)pointer auf das zu wrappende C-Struct hält
  • das C-struct im Konstruktor allokiert und im Desktruktor de-allokiert
  • alle public und private Methoden des C-Structs unterstützt und dabei
  • jeweils diese anspricht, anstatt selbst etwas zu machen

25.3 Übung 3: cmake

Verwenden Sie ihre Klasse aus der vorhergehenden Woche und schreiben Sie Makefiles im cmake-Format diese.

Das cmake-file CMakeLists.txt soll dabei folgendes unterstützen:

  • es werden zwei libraries gebaut cstructlib und cppstructlib, wobei diese jeweils die beiden Klassen der letzten Woche enthalten
  • main.cpp kann gebaut werden
  • die beiden o.g. libraries werden ordentlich zu main.cpp hinzugelinkt
  • die beiden o.g. libraries können einzeln gebaut werden: cmake .. && make cppstructlib
  • es werden out-of-source-builds unterstützt

25.4 Übung 4: TDD – Test Driven Development

Verwenden Sie Ihre C++-Klasse und den cmake build-Prozess aus der letzten Woche und schreiben Sie Unit-Tests. Auch wenn TDD eigentlich anders herum funktioniert (Failing Test first – implementation to make the test pass later), sollten Sie dennoch ein gutes Gefühl für Unit-Testing und TDD bekommen. Nicht zuletzt ist es zum Einstieg dann doch leichter für eine bereits existierenden (hoffentlich auch funktionierende!) Klasse, Tests zu schreiben.

Die Tests sollen folgendes erfüllen:

  1. die gesamte API wird abgedeckt
  2. alle erdenklichen Edge-Cases, die Ihnen einfallen werden geprüft
  3. alle Tests bestehen
  4. alle Tests können durch einen einfachen Shell-Befehl gestartet werden
  5. (dokumentieren Sie bitte, wie die Tests zu starten sind)

  6. Sie dürfen als Test-Library verwenden was Sie wollen, zwei bestimmte jedoch kann ich Ihnen besonders empfehlen:
    • lest62 – diese dürfen Sie auch gerne als git submodule hinzufügen, wenn Sie das möchten, ich möchte diese nicht gesondert herunterladen müssen, damit Schritt 5 funktioniert
    • Boost-Test63 – diese dürfen Sie als installiert ansehen
  • Machen Sie sich schon einmal mit der zu lesenden Literatur für nächste Woche und, wenn möglich, bereits mit der Theorie für nächste Woche grundlegend vertraut

  • Bonus:
    • Schreiben Sie ein Makefile oder ein Rakefile, das per default-Task die tests frisch kompiliert und laufen lässt [finden sie den link zu guten Beschreibungen von Rakefiles im entsprechenden Kapitel des Skripts]

25.5 Übung 5: Serialisierung

Verwenden Sie ihre C++-Klasse aus der vorangegangenen Woche und schreiben Sie eine Binär-Serialisierung für diese.

Zum Serialisieren können sie folgendes verwenden:

Erweitern Sie Ihre C++-Klasse aus der letzten Woche um folgende Methoden:

  • save(const std::string & filename)
  • load(const std::string & filename)

…wobei die beiden Methoden (möglicherweise als static-Methoden!) die Klasse jeweils auf Festplatte speichern und wieder laden.

Wie immer: schreiben Sie Tests, die Ihre Ergebnisse verifizieren und mit einem Befehl ausführbar sind. Dokumentieren Sie Ihre Vorgehensweise.

  • Bonuspunkte, wenn Sie const boost::filesystem::path & path statt const std::string & filename verwenden
  • Bonuspunkte, wenn Sie neben dem Binärformat noch andere Formate unterstützen (XML, JSON, …)

25.6 Übung 6: Node-Wrapping

[Spezifikation]

Verwenden Sie Ihre C++-Klasse mit den Tests aus der vorangegangenen Woche

Nun schreiben Sie ein Node-Addon, das folgendes kann:

  • ich kann var addon = require('myWrappedCppClass') (oder ähnlich) aufrufen
  • ich kann auf Javascript-Seite addon.add(element); aufrufen und das Element wird in der Member-Variable der C++-Klasse abgespeichert
  • ich kann mit console.log(addon.size()); die Anzahl von Elementen in der Member-Variable der C++-Klasse abfragen
  • ich kann auf Javascript-Seite mit addon.elements() alle Elemente aus der C++-Klasse in ein Javascript-Array holen.
  • schreiben Sie eine for-loop in Javascript, das alle Elemente auf der Shell ausgibt

Hinweise:

  • Verwenden Sie zur Lösung dieser Aufgabe
  • Es steht Ihnen frei, “statt” Javascript Coffeescript zu verwenden.
  • Bonuspunkt, wenn Sie erfolgreich utf8-Strings adden und ordentlich auf der Shell ausgeben

25.7 Übung 7: Template Meta Programmierung

In dieser Übung werden Sie in die Template Meta Programmierung eingeführt. Dabei erlernen Sie, wie man Entscheidungen und – möglicherweise – sogar Berechnungen in die Compile-Zeit überführen kann.

In letzterem Falle bedeutet dies tatsächlich, dass zur Laufzeit bereits alle Berechnungen erfolgreich erledigt wurden und keine Zeit mehr benötigen!

Schreiben Sie eine templatisierte C++-Klasse template<class T> class MyClass, die:

  • einen default CTOR + DTOR hat
    • (Verwenden Sie hierzu bestfalls den C++11-Weg)
  • eine Methode zum hinzufügen von Elementen hat
  • eine Method zum Ausgeben der Anzahl von Elementen hat
  • eine Kollektion als Member-Variable speichern kann, und zwar wie folgt:
    • Wenn die Klasse mit einem std::string oder einem std::wstring instanziiert wurde, dann sollen diese in einem std::vector<> abgespeichert werden
    • wenn die Klasse mit einem numerischen Wert, also int, unsigned int, unsigned long long int, short, float, double, … instanziiert wurde, sollen diese in einem std::set<> gespeichert werden
    • wenn die Klasse mit irgendeiner anderen Art instanziiert wird, soll dieses zur Compile-Zeit mit einem static_assert und einer entsprechenden Fehlermeldung abgelehnt werden.
  • Wie immer haben Sie für alles selbstverständlich so gut es geht Tests zur Verfügung (die sich über einen (1!) Shell-Befehl bequem starten lassen, z.B. make && make test) – zugegeben, manche Dinge lassen sich hier nur schlecht (bis kaum testen)

25.8 Übung 8: Type Traits

Schreiben Sie ein type-trait, das für eine Klasse template<class T> Foo zur Compile-Zeit (!)

  • feststellt, ob die Klasse mit einem std::string, std::wstring oder einer aus diesen abgeleiteten Klassen besteht
  • feststellt, ob es sich um einfache oder wide-strings handelt und
  • eine bool-variable anbietet, die wiederum von anderen Klassen abgefragt werden kann: bool is_wide_string

Also:

namespace trait {
  template<class T> mytrait {
    static constexpr bool is_wide_string = /*...*/;
  }
}

temlpate<class StringType>
class MyClass {
  using is_wide_string = mytrait<StringType>::is_wide_string;
};  

auto main() -> int {  // C++11 only!
  MyClass<std:: string>  stringclass;
  MyClass<std::wstring> wstringclass;
  
  std::cout << std::boolalpha
    <<  stringclass::is_wide_string   // false
    << std::endl
    << wstringclass::is_wide_string   // true
    << std::endl;
  return 0;
}

25.9 Übung 9: Web-Programmierung mit dem MEAN-Stack

Schreiben Sie eine Komponente für LaaS, die “linguistics as a service”-Webapplikation.

Gehen Sie dazu wie folgt vor:

  • clonen sie den code von https://gitlab.cip.ifi.lmu.de/bruder/claas
  • schauen Sie sich die Routen server.js und in lib/controllers/api.js an, und versuchen Sie zu verstehen, was hier passiert – letztendlich werden nur Daten vom Frontend (also von der Client-Seite entgegengenommen und ein Shell-Befehl ausgeführt und die dadurch gewonnen Daten an die Client-Seite zurückgegeben)
  • schauen Sie sich einen Controller der Client-Seite an und versuchen Sie zu verstehen, wie dieser die Daten an die Server-Seite gibt und die Ergebnisse von der Server-Seite entgegennimmt. Betrachten Sie beispielsweise app/scripts/controllers/tokenize.coffee
  • schauen Sie sich eine View auf der Client-Seite an und versuchen Sie zu verstehen, wie die Daten entgegengenommen und die Ergebnisse von der Server-Seite präsentiert werden, z.B: app/views/partials/tokenize.jade
  • Nun schreiben Sie ein kleines Modul selbst! Suchen Sie sich dabei frei aus, wwas sie umsetzen möchten, hier ein paar Ideen: uppercase, lowercase, camelCase, CamelCase to snake_case, strip, Spracherkennung, reverse, wordcount, …

26 Disclaimer

Diese Dokumentation ist ein Work-in-Porgress-Produkt und daher:

  1. in manchen Teilen noch nicht fertig
  2. in anderen Teilen schon deprecated.

Da die Dokumentation auch in einer kollaborativen Team-Arbeit erstellt und gepflegt werden soll, bitte bei Fehlern diese unbedingt korrigieren und per gitlab als Änderungen anbieten, danke!

27 Dank

28 Überblickstabellen

28.1 Komponenten, Maintainer, Dependencies

28.1.1 Komponenten, Beschreibungen, Maintainer

28.1.1.1 wf

Component:     wf
Description:
Maintainer:    flo@cis.lmu.de
Link external: wittfind.cis.lmu.de
Repository:    gitlab.cis.lmu.de/wast/wf
Dependencies:
    * `gcc > 4.7`
    * boost
        * ... (?)
    * cmake
    * (doxygen)

28.1.1.2 wittfind-web

Component:      wittfind-web     
Description:
Maintainer:     maximilian@cis.lmu.de et al.
Link external:  wittfind.cis.lmu.de
Repository:     gitlab.cis.lmu.de/wast/wittfind-web
Dependencies:
    * git
        * git-submodules
            * wf
    * PHP
    * Apache
    * E2E-dependencies:
        * [^node] / [^npm]
            * [^casperjs]
                * [^phantomjs]

28.1.1.3 SIS

28.1.1.3.1 SIS-frontend
Component:      SIS (frontend)
Description:
Maintainer:     bruder.cis.lmu.de
Link external:  sis.cis.lmu.de
Repository:     gitlab.cis.lmu.de/wast/sis3/tree/master/web
Dependencies:   
    * MEAN-Stack
        * [^node] / [^npm]
        * [^expressjs]
        * [^angularjs]
        * [^bower]
            * client-seitige dependencies
              werden durch [^bower]
              installiert
        * [^npm]
            * server-seitige dependencies
              werden durch [^npm]
              installiert
        * MongoDB
28.1.1.3.2 SIS-backend
Component:      SIS (backend)
Description:
Maintainer:     bruder@cis.lmu.de
Link external:  sis.cis.lmu.de
Repository:     gitlab.cis.lmu.de/wast/sis3/tree/master/src   
Dependencies:
    * `gcc > 4.7`
    * cmake
    * (doxygen)
    * git
        * git-submodules
            * lest
    * boost
        * ... (?)

28.1.1.4 Alternativen-Suche

Component:      "Alternativen-Suche"
Description:    Ausformulierung von Alternativen
Maintainer:     seebauerp@cip.ifi.lmu.de / lindinger@cip.ifi.lmu.de / TBD
Link external:  wittfind.cis.lmu.de
Repository:     gitlab.cis.lmu.de/wast/wittfind-web   
Dependencies:
    * perl-module (?)
        * XML-Twig (?)
        * ... (?)
    * ... (?)

28.1.1.5 Helppage

Component:      Helppage
Description:    Beispiel-Anfragen und Hilfe für wittfind
Maintainer:     kreya@cip.ifi.lmu.de
Link external:  wittfind.cis.lmu.de/help.php
Repository:     gitlab.cis.lmu.de/wast/wittfind-web
Dependencies:
    * PHP (Version?)
        * ... (?)
    * ... (?)

28.1.1.6 Feedback

Component:      Feedback
Description:    Bug Reports für Aussenstehende
Maintainer:     bruder@cis.lmu.de, TBD
Link external:  wastfeedback.cis.lmu.de
Repository:     gitlab.cis.lmu.de/wast/wast-feedback
Dependencies:
    * curl
    * gitlab user token / "FeedbackBot"-User
    * MEAN-Stack
        * [^node] / [^npm]
        * [^expressjs]
        * [^angularjs]
        * [^bower]
            * client-seitige dependencies
              werden durch [^bower]
              installiert
        * [^npm]
            * server-seitige dependencies
              werden durch [^npm]
              installiert
        * MongoDB

28.1.1.7 E2E

Component:      E2E
Description:    End-to-end tests für wittfind-web: komplette User-Szenarien durchspielen
Maintainer:     bruder@cis.lmu.de, TBD
Link external:  ---
Repository:     gitlab.cis.lmu.de/wast/wittfind-web/tree/development/tests
Dependencies:
    * [^node] / [^npm]
        * [^casperjs]
            * [^phantomjs]

28.1.1.8 Reader

Component:      Reader
Description:    Reader-Applikation für Facsimile
Maintainer:     lindinger@cip.ifi.lmu.de (et al.?)
Link external:  ---
Repository:     gitlab.cis.lmu.de/wast/wittfind-web/tree/development/tests
Dependencies:
    * PHP (Version?)
    * [^turn.js]
    * ... (?)

28.1.1.9 wast-doc

Component:      wast-doc
Description:    Dokumentation für WAST                                                                                      
Maintainer:     bruder@cis.lmu.de / TBD
Link external:  www.cip.ifi.lmu.de/~bruder/wast/
Repository:     https://gitlab.cis.lmu.de/wast/wast-doc
Dependencies:
    * [make](https://www.gnu.org/software/make/manual/make.html)
    * [pandoc](johnmacfarlane.net/pandoc/)
    * [gpp](http://files.nothingisreal.com/software/gpp/gpp.html)
    * [/github.com/typesetters/p5-App-pandoc-preprocess)
        * [dot](http://www.graphviz.org/Documentation.php)/[neato](http://www.graphviz.org/pdf/neatoguide.pdf)
        * [rdfdot](RDF::Trine::Exporter::GraphViz)
        * [ditaa](http://ditaa.sourceforge.net/)
        * [Image::Magick](http://www.imagemagick.org/) (image manipulation)
        * [rsvg-convert](http://live.gnome.org/LibRsvg)
        * [yuml](https://github.com/wandernauta/yuml) (install as python module using `pip install https://github.com/wandernauta/yuml/zipball/master` or `easy_install https://github.com/wandernauta/yuml/zipball/master`)
        * [plantuml](http://plantuml.sourceforge.net/)

28.1.3 Dependency Graph (experimental)

WAST Komponenten und Dependencies

WAST Komponenten und Dependencies

28.2 Mitwirkende

Komponente Teil Personen Von Bis
SIS alle 4 Schichten Daniel Bruder 01/2012 dato

29 Workflows

29.1 git branching model workflow

In der Entwicklung der WAST-Landschaft arbeiten wir mit dem sehr bekannten und verbreiteten git branching model von dieser Seite69 — auf Seite fasst den git branching model workflow an dieser Stelle kurz zusammen.

git branching model

git branching model

29.2 Continuous Integration Workflow

auf Seite verdeutlicht den Workflow einer CI, für mehr Informationen siehe auch das Kapitel Continuous Integration.

Continuous Integration

Continuous Integration

29.3 WAST pipeline

auf Seite gibt einen Überblick, wie die WAST-pipeline (in zunehmender Kombination mit dem Unified Deployment-Prozess) aussieht:

WAST Data Workflow

WAST Data Workflow

29.4 bug tracking workflow

Graphik auf Seite fasst noch einmal als Überblick den Bug Tracking Workflow zusammen – für eine insgesamte Beschreibung siehe auch Kapitel [Bug Tracker][].

Bug Tracking Workflow

Bug Tracking Workflow

30 Hinweise

30.1 History Visualisierung (gource)

Es ist nicht nur schön anzusehen, sondern oft auch aufschlussreich, sich die Visualisierung der history per gource [webhinweis] anzusehen, um zu verstehen wie ein Projekt gewachsen ist

31 next steps

31.1 E2E-Tests für Feedback, SIS, wittfind-web

31.2 Integration-Tests und Projekt-Organisation

31.3 Integration von SIS und Feedback in wittfind-web

31.4 Integration der restlichen 5000 Facsimile Seiten und Transkriptionen (in SIS und wf)

31.5 Erweiterung von WAST auf *AST: Öffnung und Aufbereitung für andere Editionsprojekte.

32 Ressourcen

32.1 UML Legende

UML Legende: Class

UML Legende: Class

UML Legende: Simple Association

UML Legende: Simple Association

UML Legende: Cardinality

UML Legende: Cardinality

UML Legende: Directional Association

UML Legende: Directional Association

UML Legende: Aggregation

UML Legende: Aggregation

UML Legende: Composition

UML Legende: Composition

UML Legende: Inheritance

UML Legende: Inheritance

UML Legende: Interface Inheritance

UML Legende: Interface Inheritance

UML Legende: Dependencies

UML Legende: Dependencies

UML Legende: Interface

UML Legende: Interface

UML Legende: Class with Details

UML Legende: Class with Details

32.2 Literatur

<#startdeprecated>

  • Daniel Bruder, SIS – Symmetrische Indexstrukturen, Magisterarbeit, CIS, LMU, 2012, [webadresse]
  • Stroustrup
  • Di Gennaro
  • Herb Sutter
  • Alexandrescu
  • gitlab manual
  • git manual
  • git extras
  • node manual
  • angular manual
  • express manual

+++ END DEPRECATED +++

32.3 Kontakt, Ansprechpartner

  • Max
  • Flo
  • Daniel
  • Patrick
  • Bug-Tracker
  • Webseite
  • irc

33 Definitionen

Deploy

Defintion von Deploy

WAST

Wittgenstein Advanced Search Tools

SPA
Single Page Application
aktuell modernste Variante von Web-Applikationen

laden immer nur bestimmte Teile der Seite neu und es gibt keinen kompletten Refresh der Seiten

Fat Client
innerhalb von MVC Applikationen deutliche Verschiebung von Business-Logik vom Controller hin zum Client
kann Server-Kosten senken

siehe auch AngularJS

MVC

Pattern für Applikationen: Model View Controller

AngularJS
ein Web-Framework mit MVVM- bzw. MV*-Ansatz
weicht bewusst den als zu starr empfundenen MVC-Ansatz auf (besonders ausgeprägt bei Ruby on Rails)

favorisiert Fat Client-Architektur

VCS
version control system (z.B. svn)

im Gegensatz zu DVCS

DVCS
distributed version control system (z.B. git)

im Gegensatz zu VCS

CTOR

Constructor

DTOR

Destructor

SFINAE

Substitution Failure Is Not An Error

RAII

Resource Acquisition Is Initialization


34 Seminarplan

Seminarplan
Sitzung Datum Themen Hausaufgabe Vorbereitung für nächste Sitzung
1 11.04.2014 Einführung / WAST: Projekt-Landschaft / Seminarplan / Hinweise zur Dokumentation Übung 1: git Techniken-Kapitel zum Thema Wrapping von C nach C++ / C-Wrapping Beschreibung in SIS
N/A 18.04.2014 Stunde findet nicht statt
2 25.04.2014 C-Wrapping Übung 2: C-Wrapping cmake, TDD
3 02.05.2014 Build-Systeme / Test Driven Development Übung 3: cmake + Übung 4: cmake Einschlägige Kapitel zu Serialisierung
4 09.05.2014 Serialisierung: Serialisierung in C++ / Verschiedene Libraries zur Serialisierung / Verwendung von “cereal zur Serialisierung / Zweistufige Serialisierung in SIS Übung 5: Serialisierung Web-Apps / MEAN-Stack / Techniken: Node-Wrapping
5 (16.05.2014) –Betriebsausflug–
6 23.05.2014
7 (30.05.2014)
8 06.06.2014
9 13.06.2014
10 (20.06.2014)
11 27.06.2014
12 04.07.2014
13 11.07.2014

34 Bibliographie

Abrahams, David, and David Gurtovoy. 2005. C++ Template Metaprogramming: concepts, Tools, and Techniques from Boost and Beyond. Boston: Addison-Wesley.

Alexandrescu, Andrei. 2000. Modern C++ Design: applied Generic Programming and Design Patterns. Boston, MA; London: Addison-Wesley.

Arseny Kapoulkine. 2014a. “pugixml.” pugixml. http://pugixml.org/.

———. 2014b. “zeux/Pugixml.” GitHub. https://github.com/zeux/pugixml.

Blumer, A., J. Blumer, D. Haussler, R. McConnell, and A. Ehrenfeucht. 1987. “Complete Inverted Files for Efficient Text Retrieval and Analysis.” J. ACM 34 (3) (July): 578–595. doi:10.1145/28869.28873. http://doi.acm.org/10.1145/28869.28873.

boost.org. 2014a. “Boost Test Library.” http://www.boost.org/doc/libs/1_55_0/libs/test/doc/html/index.html.

———. 2014b. “Serialization.” http://www.boost.org/doc/libs/1_55_0/libs/serialization/doc/index.html.

C++1y Standard Committee. 2014. “xdbr/Cpp-Make-Unique.” GitHub. https://github.com/xdbr/cpp-make-unique.

Daniel Bruder. 2012. “SIS – Symmetric Index Structures.” PhD thesis, LMU München. www.cip.ifi.lmu.de/~bruder/ma/MA/sis/MA-SIS-DB.pdf.

———. 2014. “xdbr/Cmake-Module-Submodule.Cmake.” GitHub. https://github.com/xdbr/cmake-module-submodule.cmake.

Daniel Bruder, Florian Fink. 2014. “xdbr/Cpp-Generic-Serialize / GitHub.” https://github.com/xdbr/cpp-generic-serialize.

“Day 1 Keynote - Bjarne Stroustrup: C++11 Style.” 2012. http://www.youtube.com/watch?v=0iWb_qi2-uI&feature=youtube_gdata_player.

Di Gennaro, Davide. 2012. Advanced C++ Metaprogramming. S.l.: s.n.].

Emscripten Authors. 2013. “Home · Kripken/Emscripten Wiki · GitHub.” https://github.com/kripken/emscripten/wiki.

expressjs. 2014. “Express - Node.Js Web Application Framework.” http://expressjs.com/.

Fink, Florian. 2013. “Programming of the Rule-Based Wf.”

Gaston, Gross. 1991. “La Form d’un Dictionnaire Electronique.” LADL-Report. Laboratoire d’Automatique Documentaire Et Linguistique.

Gerdjikov, Stefan, Stoyan Mihov, Petar Mitankin, and Klaus U. Schulz. 2013. “Good Parts First - a New Algorithm for Approximate Search in Lexica and String Databases.” arXiv:1301.0722 [Cs] (January). http://arxiv.org/abs/1301.0722.

“Git - Submodules.” 2014. http://git-scm.com/book/en/Git-Tools-Submodules.

Gitlab cis group. 2014. “Gitlab Cis Group / Gitlab.” Gitlab Cis Group / Gitlab.

Google. 2014. “AngularJS — Superheroic JavaScript MVW Framework.” http://angularjs.org/.

Gotscharek, Annette, Ulrich Reffle, Christoph Ringlstetter, Klaus U. Schulz, and Andreas Neumann. 2011. “Towards Information Retrieval on Historical Document Collections: the Role of Matching Procedures and Special Lexica.” International Journal on Document Analysis and Recognition (IJDAR) 14 (2): 159–171. doi:10.1007/s10032-010-0132-6. http://dx.doi.org/10.1007/s10032-010-0132-6.

Grassi, Marco, Christian Morbidoni, Michele Nucci, Simone Fonda, and Francesco Piazza. 2013. “Pundit: Augmenting WEB Contents With Semantics.” Literary and Linguistic Computing. Special Issue “Digital Humanities 2012: Digital Diversity: Cultures, Languages and Methods”. Edited by Paul Spence, Susan Brown and Jan Christoph Meister 28 (4) (December).

Guenthner, Franz, and Petra Maier. 1994. “Das CISLEX Wörterbuchsystem.” CIS-Bericht-94-76.

Inenaga, Shunsuke, Hiromasa Hoshino, Ayumi Shinohara, Masayuki Takeda, and Setsuo Arikawa. 2001. Construction of the CDAWG for a Trie.

Krey, Angela. 2013. “Semantische Annotation von Adjektiven Im Big Typescript von Ludwig Wittgenstein.”

Langer, Stefan, and Daniel Schnorbusch (Hrsg.). 2005. Semantik Im Lexikon. Tübingen: Narr.

Lindinger, Matthias. 2013. “Highlighting von Treffern Des Tools WiTTFind Im Zugehörigen Faksimile.”

Marco Rogers. 2013. “Marco Rogers: Creating Node Addons, C/C++ Extensions for Fun and Profit.” http://www.youtube.com/watch?v=q1ri36UI5GA&feature=youtube_gdata_player.

Martin Moene. 2013. “martinmoene/Lest · GitHub.” https://github.com/martinmoene/lest.

Maurice, Gross. 1997. “The Construction of Local Grammars.” Finite-State LanguageProcessing: 329–354.

Meyers, Scott. 2005. Effective C 55 Specific Ways to Improve Your Programs and Designs. - Includes Index. Upper Saddle River NJ: Addison-Wesley.

MongoDB. 2014. “MongoDB.” http://www.mongodb.org/.

node. 2014. “node.Js.” http://nodejs.org/.

Node API. 2014. “Addons Node.Js V0.10.25 Manual & Documentation.” http://nodejs.org/api/addons.html#addons_wrapping_c_objects.

Pichler, Alois. 2010. “„Towards the New Bergen Electronic Edition“.” Wittgenstein After His Nachlass. Edited by Nuno Venturinha: 157–172.

Pichler, Alois, H.W. Krüger, D.C.P. Smith, T.M. Bruvik, A. Lindebjerg, and V. Olstad. 2009. “Wittgenstein Source Bergen Facsimile Edition (BTE).” Wittgenstein Source. Bergen: WAB, Wittgenstein Source.

R. Martinho Fernandes. 2013. “Rule of Zero @ Flaming Dangerzone.” http://flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html.

Rothhaupt, Josef G. F. 2006. “Zur Dringend Notwendigen Revision Des „Standard View“ Der Genese Der ‘Philosophischen Untersuchungen’.” Gasser, Georg / Kanzian, Christian / Runggaldier, Edmund (Hg.): Cultures: Conflict - Analysis - Dialogue. Papers of the 29th International Wittgenstein Symposium 2006, Kirchberg 2006: S. 278–280.

Rothhaupt, Josef G. F. 1996. “Farbthemen in Wittgensteins Gesamtnachlaß. Philologisch-Philosophische, Untersuchungen Im Längsschnitt Und in Querschnitten.” PhD thesis, Weinheim.

Schiller, Anne, Christine Thielen, and Simone Teufel. 1999. “Stuttgart Tübinger Tagset (STTS).” http://www.ims.uni-stuttgart.de/forschung/ressourcen/lexika/TagSets/stts-table.html.

Schmid, Helmut. 1994. “Probabilistic Part-of-Speech Tagging Using Decision Trees.” Proceedings of International Conference on New Methods in Language Processing, Manchester, UK.

Seebauer, Patrick. 2012. “Verbesserung Der Suche Im Wittgenstein-Nachlass. Suche in Alternierenden Texten.”

Strutynska, Olga. 2012. “Nachlass von Ludwig Wittgenstein: Optimierung Eines Digitalen Lexikons Und Semantische Kodierung Der Nomen. Evaluation Des Lexikons Mit Hilfe von Konkordanzanalysen.”

“Substitution Failure Is Not an Error.” 2014. Wikipedia, the Free Encyclopedia. http://en.wikipedia.org/w/index.php?title=Substitution_failure_is_not_an_error&oldid=581401656.

TEI Consortium. 2009. “Guidelines for Electronic Text Encoding and Interchange.” TEI Consortium.

Tom Preston-Werner. 2013. “Semantic Versioning 2.0.0.” http://semver.org/.

uscilab. 2014. “cereal Docs - Main.” http://uscilab.github.io/cereal/.

visionmedia. 2014. “git-Extras.” https://github.com/visionmedia/git-extras.

Volos, Lyudmyla. 2013. “Disambiguierung von Partikelverb – Konstruktionen Und Verbpräpositional – Konstruktionen Im Big Typescript von Ludwig Wittgenstein.”


  1. http:/www.wittgensteinsource.org

  2. At the time being, there are only two formats supported; one format for the web-service, and another format for the terminal.

  3. Intel 2.90GHz Quadcore

  4. On the web the limit is set to 25 for unregistered users.

  5. I could show you the exact colour of the wallpaper if there was anything around that has this colour.

  6. http://www.wittgensteinsource.org/Ts-213_n

  7. Shortcomings of the denomination of universality made by Frege and Russell.

  8. I only mean, what I say.

  9. zu finden in vendor/cautomata

  10. zu finden in src/

  11. zu finden in web/

  12. http://nodejs.org/api/addons.html

  13. http://angularjs.org/

  14. link: späteres Kapitel (in fussnote)

  15. das Kürzel für MongoDB+Express+Angular+Node, verlgeiche hierzu auch das nächste Kapitel und das Kapitel [Teilbesprechungen][] und seine Unterkapitel [Der MEAN-Stack][] und [Web-Applikationen, Basics][]

  16. gyp steht für “Generate Your Project” und wurde von Google für den Build-Prozess von Chrome entwickelt: um die Komplexität dessen zu ermessen, vergleiche: “Life Of A Chromium Developer” auf http://dev.chromium.org/Home

  17. http://node-modules.com/search?q=forever

  18. https://gitlab.cis.uni-muenchen.de/wast/wast-feedback

  19. http://gitlab.cis.lmu.de

  20. https://gitlab.cis.uni-muenchen.de/wast/wittgenstein-issues/issues

  21. http://www.martinfowler.com/articles/continuousIntegration.html

  22. unbedingt https verwenden, sonst kann es sein, dass man von http nicht weitergeleitet wird auf https!

  23. Dieses Skript zum Beispiel wird ebenfalls durch ein Makefile gesteuert: dazu gehören neben dem Typesetten auch das Hochladen auf den webspace und andere Aufgaben.

  24. http://www.cmake.org/

  25. http://rake.rubyforge.org/

  26. https://code.google.com/p/gyp/wiki/GypUserDocumentation

  27. MongoDB (2014)

  28. expressjs (2014)

  29. Google (2014)

  30. node (2014)

  31. node (2014)

  32. vergleiche hierzu auch das Kapitel MVC im Abschnitt Design Patterns

  33. http://johnmacfarlane.net/pandoc/

  34. http://en.nothingisreal.com/wiki/GPP

  35. https://github.com/typesetters/p5-App-pandoc-preprocess bzw. https://metacpan.org/release/App-pandoc-preprocess

  36. http://johnmacfarlane.net/pandoc/

  37. http://en.nothingisreal.com/wiki/GPP

  38. https://github.com/typesetters/p5-App-pandoc-preprocess bzw. https://metacpan.org/release/App-pandoc-preprocess

  39. http://www.graphviz.org/Documentation.php

  40. http://www.graphviz.org/pdf/neatoguide.pdf

  41. https://metacpan.org/release/RDF-Trine-Exporter-GraphViz

  42. http://ditaa.sourceforge.net/

  43. http://www.imagemagick.org/

  44. http://live.gnome.org/LibRsvg

  45. https://github.com/wandernauta/yuml

  46. http://plantuml.sourceforge.net/

  47. http://www.dante.de/tex/TeXDistro.html

  48. https://gitlab.cis.uni-muenchen.de/David/stub-template-bathesis

  49. https://github.com/typesetters/p5-App-pandoc-preprocess

  50. https://gitlab.cis.uni-muenchen.de/David/stub-template-bathesis

  51. https://gitlab.cis.uni-muenchen.de/David/stub-template-bathesis

  52. siehe hierzu auch das Kapitel Integration Tests und E2E Tests. Inegration Tests prüfen das korrekte Zusammenspiel von Komponenten welche, einzeln für sich bereits alle ihre eigenen [Unit Tests][] bestehen. Mit E2E Tests beschreibt man komplette User-Szenarien, wie diese sich auf der Webseite bewegen und mit dieser interagieren. Hierbei wird auch gleichzeitig die Verfügbarkeit der Webserver und der auf diesen vorhandenen routes, etc. geprüft)

  53. http://www.git-scm.com/book

  54. http://www.git-scm.com/about/branching-and-merging

  55. http://www.git-scm.com/about/small-and-fast

  56. http://www.git-scm.com/about/distributed

  57. https://wincent.com/wiki/Git_advantages

  58. https://wincent.com/wiki/Git_advantages

  59. diese Graphik ist inspiriert von http://www.ndpsoftware.com/git-cheatsheet.html und dort interkativ mit Erklärungen zu den jeweiligen Ebenen und Interaktionen verfügbar

  60. Zu diesem Punkt ist u.a. diese Hilfe sehr gut geeignet: https://help.github.com/articles/set-up-git#set-up-git

  61. https://github.com/visionmedia/git-extras

  62. Martin Moene (2013)

  63. boost.org (2014a)

  64. Daniel Bruder, Florian Fink (2014)

  65. uscilab (2014)

  66. boost.org (2014b)

  67. Node API (2014)

  68. Marco Rogers (2013)

  69. http://nvie.com/posts/a-successful-git-branching-model/