Portabilität

Seminar: Softwareentwicklung (Programmierstil)
WS 02/03
Johannes Kepler Universität Linz, System Software Group
Betreuer: Prof. Hanspeter Mössenböck
Autor: Christine Schiestl

Abstract

Von "guter Software" wird häufig erwartet, dass sie mit möglichst wenig Aufwand fast überall und in beinahe jeder Situation stabil läuft. Portabler Code versucht diesem Wunsch nachzukommen, indem so implementiert wird, dass ein Programm möglichst einfach an unterschiedliche Betriebssysteme, Benutzergruppen, kulturelle Gegebenheiten (zum Beispiel Sprache) usw. angepasst werden kann.Um dieses Ziel realisieren zu können ist bereits in der Spezifikations- und Entwurfsphase, aber vor allem bei der Implementierung, einiges zu beachten. Legt man zusätzlich bezüglich Lesbarkeit, Überschaubarkeit, Wartbarkeit ... des Codes auf einen "guten Programmierstil" wert, können sich diverse Konflikte ergeben.
Diese Seminararbeit behandelt daher im Wesentlichen, was unter Portabilität von Software genau verstanden wird, welche Probleme sich dabei ergeben können und wie diese gelöst werden können ohne dass der Programmierstil wesentlich darunter leidet.

1. Einleitung

Portabilität ist eine wünschenswerte Eigenschaft bei vielen Softwareprodukten, denn sie verlängert einerseits den Lebenszyklus und ermöglicht andererseits einen breiteren Einsatz der Software [MoJa93]. Die Definition von James Mooney gibt einen guten Einblick, warum das so ist.

"A software unit is portable (exhibits portability) across a class of environments to the degree that the effort required to transport and adapt it to a new environment in the class is less than the effort of redevelopment." [MoJa01]

Es zahlt sich also nicht aus, portable Software gänzlich neu zu entwickeln, wobei mit "Software" Subsysteme, ganze Systeme, aber vor allem Programme / Applikationen und Komponenten gemeint sind. Anstelle einer Neuentwicklung kann die vorhandene Software auch physisch in eine neue Umgebung gebracht, also transportiert, und entsprechend den geänderten Bedingungen modifiziert, bzw. adaptiert, werden. Transport und Adaption sind somit die Hauptphasen bei der Portierung, wobei ein Transport mit Hilfe von transportablen Medien wie CDs erfolgen kann, oder direkt durch eine Übertragung über ein Netzwerk. Es müssen jedoch in beiden Fällen mögliche low-level Inkompatibilitäten in den Datenformaten berücksichtigt werden. [MoJa01]

Um weitere Portabilitätsprobleme verstehen zu können, muss man die Beziehung zwischen Software und ihrer Umgebung verstehen [MoJa94], weil ja die Software-Einheit in eine neue Umgebung portiert werden soll. In der Definition ist dabei von einer Klasse von Umgebungen die Rede. "Environment" bezeichnet in diesem Zusammenhang eine Sammlung aller bedeutenden externen Elemente mit denen das Programm interagiert, wie zum Beispiel andere Software Einheiten, das Betriebssystem, die Hardware, aber auch User, Programmierer und die Dokumentation. Ein Programm wird jedoch höchstwahrscheinlich nicht nur in eine ganz spezielle Umgebung, sondern in mehrere sehr unterschiedliche Umgebungen (verschiedene Betriebssysteme, User aus unterschiedlichen Ländern usw.) portiert. Da ein portables Design nicht alle denkbar möglichen Umgebungen abdecken kann, sagt Portabilität nur in Hinblick auf eine Klasse von Umgebungen, innerhalb der portiert wird, etwas aus, wobei eine Klasse durch eine Menge von gemeinsamen Eigenschaften charakterisiert ist. [MoJa01]

Bedenkt man, wie viele verschiedene Technologien im Bereich von Hardware und Software es gibt und wie schnell sich diese entwickeln, ist die Wahrscheinlichkeit einer Portierung der Software sehr hoch. Plant man Portabilität von Anfang an ein, können die Kosten einer späteren Portierung und eventuell auch die Gesamtkosten, die entstehen solange die Software im Einsatz ist, reduziert werden.

2. Anforderungsanalyse und Spezifikation

Die Entwicklung neuer Software startet für gewöhnlich mit einer eingehenden Analyse der Anforderungen. In weiterer Folge werden ein oder mehrere Spezifikationen festgelegt. Auf diese Weise können Funktionalität und andere Eigenschaften der Software identifiziert werden. Es gibt viele unterschiedliche Methoden um zu einer Spezifikation zu gelangen. Unabhängig davon sollten in Hinblick auf Portabilität vor allem folgende Richtlinien beachtet werden:

Möglichst alles vermeiden, was für die Portabilität hinderlich ist

So sollten keine unnötigen Systemabhängigkeiten eingebaut werden. Ein Beispiel wäre:

"Ein Objekt wird durch zweimal klicken mit der rechten Maustaste innerhalb von 0,5 Sekunden innerhalb eines Kreises mit 10 Pixel Radius um den Mittelpunkt ausgewählt. ..."

Die Abhängigkeiten, dass ein Bildschirm eine gewisse Auflösung oder eine Maus mehrere Tasten haben muss, sind nicht notwendig. Besser und ohne unwichtige Details ist die Aussage:

"Ein Objekt kann durch die Wahl eines passenden Werkzeugs ausgewählt werden."

Die meisten Systemabhängigkeiten entstehen in Zusammenhang mit User Interfaces, aber nicht alle. So besteht beispielsweise keine Notwendigkeit dafür, dass Integer durch exakt 32 Bits repräsentiert werden, wenn sich nur einige Werte zwischen 0 und einer Million bewegen.

Ziele im Zusammenhang mit Portabilität explizit spezifizieren

Es ist sinnlos zu spezifizieren, dass die Portierung eines Programms in jede neue Umgebung einfach sein soll, da dieses Ziel weder testbar, geschweige denn realisierbar ist. Denn wie in der Einleitung erwähnt, macht Portabilität nur in Hinblick auf eine Klasse von Umgebungen Sinn. [MoJa97] Eine Schwierigkeit besteht darin, dass ein wirklich portables Design Portierbarkeit für Umgebungen erahnen muss, deren genaue Charakteristiken momentan noch gar nicht bekannt sind! [MoJa01]
Da Portabilität häufig in Widerspruch mit anderen Zielen wie hohe Performance, effizienter Ressourcenverbrauch oder geringe Entwicklungskosten steht, sollte in der Spezifikation zudem auch festgestellt werden, wie viel Portabilität überhaupt notwendig ist. [MoJa97] Portabilität bedeutet zusätzlichen Aufwand. Um die Frage zu klären, ob es sich vielleicht gar nicht lohnt in Portabilität zu investieren, ist eine Metrik hilfreich, wie sie in einem eigenen Kapitel ansatzweise beschrieben wird.

2.1 Internationalisierung

Bei der Anforderungsanalyse und der Spezifikation stehen vor allem der Benutzer, seine Bedürfnisse und seine Sicht im Mittelpunkt. [ChGe00] Bei Portabilität in Zusammenhang mit unterschiedlichen Benutzergruppen ergeben sich Probleme, die deshalb bereits in diesen Phasen der Entwicklung Beachtung finden sollten. Beispiele dafür gibt es viele. [MoJa94]

  • Nicht jeder spricht Englisch und vor allem nicht gleich gut. Deshalb sollten zum Beispiel Fehlermeldungen keinen Slang beziehungsweise Fachjargon enthalten.
  • Deutsch ist bekannt dafür, dass Geschriebenes besonders viel Platz braucht um das Selbe aussagen zu können.
  • Nur in Nordamerika verwendet man für ein Datum das Format "mm/dd/yy".
  • Das $-Zeichen steht nicht überall für die Währung.
  • Farben oder Symbole haben in den einzelnen Kulturen oft eine unterschiedliche Bedeutung.
  • Wir zählen uns in der Regel zu den erfahrenen Benutzern. Da viele Anwendungen aber nicht für Experten programmiert werden, muss man oft von unerfahrenen Usern ausgehen.
  • Nicht jeder Benutzer hat die gleichen Systemvoraussetzungen gegeben. Soll die Anwendung beispielsweise auch in Entwicklungsländern laufen, müssen Modi für extrem eingeschränkte Plattformen unterstützt werden. [MoJa01]
  • Nicht überall wird der ASCII-Zeichensatz, ein 7 Bit- Code, verwendet. Im französischen benötigt man beispielsweise für das Wort "garçon" einen Buchstaben, der selbst im 8 Bit Code Latin-1, der in Westeuropa verbreitet ist, mit E7 bereits außerhalb liegt. Denkt man an die asiatischen Sprachen braucht man schon wesentlich mehr Bits. Unicode versucht bereits eine einzige Kodierung für alle Sprachen der Welt zu bieten und wird z.B. von Java verwendet. Rückwärtskompatible Zeichensätze sind bezüglich Portabilität eine große Hilfe. [KePi99]

01.02.2002
 
1. Februar ?
2. Jänner?

$ / € / ...

garçon
 
67 61 72 E7 6F 6E

Das Problem, wenn man ein Programm über verschiedene kulturelle Anforderungen hinweg portabel machen will, gestaltet sich ähnlich wie beim Portieren auf eine andere Plattform. [MoJa01] Die wesentlichen Teile der Software sind in jeder Umgebung gleich, aber einzelne Teile müssen entsprechend angepasst werden. Am einfachsten gestaltet sich die Portierung daher, wenn der Software ein modulares Design zugrunde liegt und die Unterschiede isoliert werden. [MoJa94]

3. Design

In dieser Phase identifiziert man eine Sammlung von Elementen (Prozeduren, Datenstrukturen, Objekte etc.) die bei der Implementierung verwendet werden und definiert eine passende Einteilung der Elemente in Module. Das Ergebnis des Designs ist eine Sammlung von interagierenden Modulen die über Schnittstellen miteinander kommunizieren.

3.1 Schnittstellen zur Umgebung

Da ein Softwareprodukt immer in eine spezielle Umgebung eingebettet ist, müssen nicht nur die softwareeigenen Schnittstellen zwischen den Modulen, sondern auch jene, über die die Software mit ihrer Umgebung interagiert, reflektiert werden. [MoJa97]
Sind viele Schnittstellen innerhalb der einzelnen Umgebungen der Klasse gleich, kann ein hoher Grad von Portabilität erreicht werden, da dort kein Aufwand zum Anpassen der Software notwendig ist. Allgemein akzeptierte, eindeutige und vollständige Standards für betroffene Schnittstellen helfen in Folge dessen viele Probleme in Zusammenhang mit Portabilität zu lösen. [MoJa01]
Sollten Zugriffe auf Schnittstellen zu diversen Zielumgebungen variieren kann es notwendig sein, diese Zugriffe in wenigen Modulen zu isolieren. Operationen auf spezielle I/O Geräte werden beispielsweise häufig in einem Modul gekapselt. [MoJa97]

Mit entsprechenden Modellen können die jeweiligen Schnittstellen dargestellt werden. Auf diese Weise können Möglichkeiten gefunden werden um eine bessere Portabilität der Software zu erreichen.

Modell Dieses Modell zeigt beispielsweise die wichtigsten Schnittstellen eines Programms oder einer Komponente die üblicherweise während der Ausführung eine Rolle spielen [MoJa01]. Es lässt aber einen wesentlichen Aspekt unberücksichtigt, nämlich wie sich die Schnittstellen mit der Zeit verändern [MoJa94].

3.2 verschiedene Zielsysteme

Soll die Software auf unterschiedlichen Zielsystemen laufen, ist für das Design entscheidend, ob man entweder
  • die besten Eigenschaften jedes Systems nutzt und bei der Kompilierung und Installation dann entsprechend unterscheiden muss (Vereinigung)
  • oder ob man nur jene Eigenschaften, die für alle Zielsysteme gelten, verwendet. In diesem Fall wird die Schnittmenge der Eigenschaften genutzt.
Modell
Letzteres wird empfohlen, auch wenn die Performance darunter leidet und die Zielsysteme bzw. die Möglichkeiten des Programms eingeschränkt werden. Die Vorteile sind, dass der Code einfacher wird und bedingte Kompilierung (Differenzierung der einzelnen Systeme mittels #ifdef-Zweige) vermieden wird.
Denn durch #ifdef Statements mixt man Kompilezeit-Kontrollfluss und Laufzeit-Kontrollfluss, wodurch der Code schwer zu lesen und damit auch komplizierter zu warten wird. Modifikationen am Programm gestalten sich dadurch ebenfalls schwieriger. Schließlich müssen Änderungen in einem #ifdef - Block auch in den restlichen Blöcken nachgezogen werden. Es ist nicht leicht ein solches Programm konsistent zu halten und fast unmöglich vollständig zu testen, da es in separat kompilierte Programme geteilt wird. [KePi99]

#ifdef MAC
    ...
#elif DOS
    ...
...
#endif

Benötigen verschiedene Systeme doch unterschiedlichen Code, sollten die Unterschiede in getrennten Files verwaltet werden. Dadurch entsteht ein übersichtlicheres Design.
Interfaces können portablen von nicht-portablem Code abgrenzen und Systemabhängigkeiten verstecken. So liefern zum Beispiel die I/O - Bibliotheken häufig eine Abstraktion des Speichers in Form von Files, die geöffnet und geschlossen werden. Die wahre Realisierung (wo liegen die Werte im Speicher, wie ist ihre Struktur aufgebaut usw.) bleibt jedoch verborgen und kann je nach System variieren. [KePi99] Systemfiles

int open(const char *Pfad,int OModus[unsigned SModus])
int read(int Fd, void *Puffer, unsigned n)
int close(int Fd) [WeMa95]

4. Implementierung

Die meisten Entwickler setzten bei der Implementierungsphase an um ihre Software portabel zu machen. Folgende Prinzipien sollten dabei beachtet werden:

4.1 Eine portable Programmiersprache wählen

Das bedeutet, dass die Sprache möglichst gut standardisiert und weit verbreitet sein sollte. Trotzdem können sich eventuelle Konflikte mit anderen Standards, die während der Design-Phase identifiziert wurden, ergeben. Jede Sprache enthält aber auch selbst Elemente, die zu Schwierigkeiten bezüglich Portabilität führen können. Denn ein Standard kann eine Sprache nie vollständig definieren. Selbst wenn man sich bei der Implementierung immer an den Standard hält, kann es zu Portabilitätsproblemen kommen. So können unterschiedliche Implementierungen zwar richtige aber inkompatible Ergebnisse liefern. [MoJa97]

Byte-Ordnung

Die Byte-Ordnung ist in C und C++ innerhalb von short, int oder long beispielsweise nicht definiert. Das Byte mit der niedrigsten Adresse kann das meist signifikante oder am wenigsten signifikante Byte enthalten. Das hängt stark von der Hardware ab. Probleme treten daher vor allem dann auf, wenn Werte von einem Computer geschrieben und vom nächsten gelesen werden.

Für den Datenaustausch sollte deshalb eine fixe Byte-Ordnung verwendet werden, wobei folgende Vorgehensweise denkbar wäre:
Mit dem Rechts-Shift wird das Byte höherer Ordnung geschrieben, mit dem Bitweisen-UND das Byte niedrigerer Ordnung. Das Zurücklesen funktioniert analog.
Allerdings müssen sich Sender und Empfänger der Daten über die Anzahl der Bytes pro Objekt und die Byte-Ordnung einig sein.
Die Kosten, die dabei entstehen, sind im Verhältnis zum I/O Prozess selbst relativ gering. Außerdem kann so auf #ifdef verzichtet werden.

unsigned short x;
putchar(x >> 8);
putchar(x & 0xFF);

unsigned short x;
x = getchar << 8;
x |= getchar() & 0xFF;

Java ist eine höhere Programmiersprache als C oder C++ und versteckt die Byte-Ordnung völlig. Die Bibliothek enthält für diesen Zweck das Interface "Serializable".

Reihenfolge der Auswertung

In C und C++ muss die Reihenfolge der Auswertung von Operanden von Ausdrücken nicht immer gleich sein. Das zeigen folgende Beispiele:

n = (getchar() << 8) | getchar();
ptr[count] = name[++count];
printf("%c %c\n", getchar(), getchar());

Java ist da wesentlich strikter in der Reihenfolge der Auswertung. Ausdrücke, die Seiteneffekte hervorrufen können, müssen in einer bestimmten Reihenfolge (von links nach rechts) ausgewertet werden. Trotzdem soll man das Anwenden dieser Regel vermeiden, zumindest, wenn die Möglichkeit besteht, dass das Programm in eine andere Sprache konvertiert werden soll. Die Portierung in eine andere Programmiersprache ist zwar ein extremer Test von Portabilität, kann aber unter Umständen Sinn machen. [KePi99]

Größe der Basistypen

Die Größe von Basistypen ist in C und C++ nicht genau definiert, sondern nur, wie viele Bits sie mindestens haben müssen und in welcher Relation sie zueinander stehen. Dadurch kann es passieren, dass man möglicherweise in einer falschen Annahme zu wenig Speicher reserviert. [ScHe01] Nicht so Java - dort sind die Größen aller Basistypen definiert.

Folgendes C Programm auf mehreren Compilern ausprobiert ergab sowohl " 1 2 4 4" als auch "1 2 2 4" als Ergebnis:

int main(void){
  printf("%d %d %d %d", sizeof(char),
  sizeof(short), sizeof(int), sizeof(long));
  return 0;
}

Char mit / ohne Vorzeichen

Der Basistyp char kann in C und C++ signed oder unsigned sein. Das hat ua. Auswirkungen in Zusammenhang mit der Methode "getchar()", die einen Integer-Wert liefert. "getchar()" liefert bei EOF den Wert -1. Ist char unsigned, wird im auskommentierten Fall der Wert von EOF in unsigned char konvertiert und man erhält als Ergebnis 255. Der Vergleich in unserem Beispiel muss dann also immer "false" sein. Besser ist daher für Vergleiche mit EOF den Wert wie im nicht auskommentierten Teil in einer int-Variable zwischenzuspeichern.

Int i;
Char s[MAX];
for (i=0; i<MAX-1; i++) {
  //if ((s[i]=getchar()) == EOF) break;
    if ((c=getchar()) == EOF) break;
  s[i] = c;
}
s[i] = '\0';

Speicher

Die Reihenfolge, in der die Elemente eines Structs im Speicher liegen, ist definiert. Die einzelnen Elemente können jedoch zum Beispiel je nach Compiler verschieden groß sein. Man erhält daher unterschiedliche Ergebnisse, wenn man versucht die Größe des Structs (links) auf diese zwei Arten zu ermitteln. Die erste Form ist wahrscheinlich die Gewollte.

Struct X{
  char c;
  int i;
};
  sizeof(Struct X)
 
sizeof(char)+sizeof(int)

4.2 Standards überlegt anwenden

Grundsätzlich sollte man mit dem Strom schwimmen und nur jene Features der Sprache verwenden, wo die Sprachdefinition eindeutig ist und gut verstanden wird. Sobald es sehr schwierig ist ein Konstrukt zu verstehen, sollte man davon ablassen.

Portabilität ist auf die Verwendung in der Zukunft ausgelegt. Daher sollte man "veraltete" Funktionen vermeiden, auch wenn jetzige Compiler sie noch verstehen müssen. Das erhöht die Sicherheit des Codes und die Wahrscheinlichkeit auch in Zukunft eine lauffähige Anwendung zu haben. [KePi99] Ein Beispiel aus der Java-API zeigt eine so gekennzeichnete Funktion.

java.util.Date
int getDay()

 
Deprecated. As of JDK version 1.1, replaced by
Calendar.get(Calendar.DAY_OF_WEEK)

Header und Libraries erweitern die Basissprache. Genau genommen sind sie jedoch nicht Teil der Sprache. Aber sie werden gemeinsam mit der Sprache entwickelt und man erwartet, dass sie von Programmierumgebungen, die diese Sprache unterstützen, bereitgestellt werden. Auch Bibliotheken können nicht-Portabilität enthalten, wenn sie zum Beispiel Features verwenden, die nicht im Standard definiert sind.
Trotzdem kann man in der Regel davon ausgehen, dass ein Programm, das nur Standard-Library-Funktionen benutzt, auch auf anderen Systemen das gleiche Verhalten zeigt. [KePi99]

4.3 Portabel denken

Portabel zu denken ist wohl der wichtigste Grundsatz wenn portable Software entwickelt werden soll. Bei der Implementierung sollte man sich stets der Problemantik bewusst sein und darauf achten, ob sich vielleicht diverse Konflikte ergeben können.

5. Metrik

Metriken sind hilfreich,

Ein mögliches Maß dafür, wie portabel eine bestimmte Software ist, ergibt sich (entsprechend der Definition), indem man die Kosten für die Portierung in Relation zu den Kosten für eine Neuentwicklung setzt. In einer Formel wird der Grad der Portabilität für eine Software-Einheit su folgendermaßen ausgedrückt:

DP(su) = 1 - (Cport(su,e2) / Crdev(req,e2))

Auf diese Weise erhält man einen Wert, dessen Maximum bei 1 liegend perfekte Portabilität repräsentiert. Kosteneffizient ist Portabilität nur, wenn der errechnete Wert größer 0 ist.

Die Kosten für Softwareentwicklung allgemein, für eine bestimmte Umgebung e1 und eine Spezifikation req, setzen sich (vereinfacht) aus den Kosten für Design (Cdes), Codierung (Ccod), Test und Debugging (Ctd) sowie der Dokumentation (Cdoc) der Software zusammen. Bei portablem Code kommen noch die Kosten für die Portabilitätsanalyse (Cpa) hinzu.

Cdev(req,e1) = Cdes(req) + Ccod(req,e1) + Ctd(req,e1) + Cdoc(req,e1)
Cdevp(req,e1) = Cdev(req,e1) + Cpa(req)

Die Kosten für Portierung sollen gegen die Kosten für Neuentwicklung abgewogen werden. Die Berechnung dafür erfolgt analog zur vorhergehenden Formel.
Durch Erfahrung, Wiederverwendung von Komponenten etc. sind diese Kosten in der Regel kleiner als ursprünglich.

Crdev(req,e2) <= Cdev(req,e1)

Um eine Software zu portieren wird sie soweit als nötig modifiziert (Cmod), in der neuen Umgebung getestet und Fehler entfernt (Cptd), sowie die Dokumentation (Cpdoc) ergänzt. [MoJa93]

Cport(su,e2) = Cmod(su,e2) + Cptd(req,e2) + Cpdoc(req,e2)

Perfekte Portabiliät wäre gegeben, wenn das Programm gänzlich ohne neue Kosten portiert werden kann. In der Praxis ist das aber kaum der Fall. [MoJa97]

6. Conclusion

Portierbarkeit ist nicht stets der beste Weg in der Softwareentwicklung. Es ist auch nicht immer leicht alle Aspekte der einzelnen Umgebungen sauber unter einen Hut zu bekommen. Erschwerend kommt hinzu, dass kaum Methoden, Referenzen, Metriken etc. existieren, die dabei helfen können. Selbst in einschlägiger Literatur findet man oft nur Empfehlungen. In der Praxis wird Portabilität daher meist durch "ad hoc"-Lösungen eingebaut.
Zudem verursacht, Portablilität in Software einzubauen, Kosten. Zur Entwicklung ist mehr Aufwand nötig und wahrscheinlich sinkt die Qualität der Software durch:

Denn das Programm nutzt nicht alle Möglichkeiten der einzelnen Umgebungen. Die Implementierung ist allgemeiner und nicht genau auf eine spezielle Umgebung zugeschnitten.

Doch auch wenn die Vorteile von portabler Software erst in der Zukunft [MoJa94] und nicht bereits bei der Implementierung offensichtlich sind, sind sie nicht von der Hand zu weisen:

Die Entscheidung ob und vor allem wie portiert werden soll, ist daher sorgfältig zu überlegen.

7. Literatur

[BlLi01] Blair, Liz: "Build to Spec!", Reprinted from Java Developer‘s Journal, 2001
[KePi99] Brian W. Kernighan, Rob Pike: "The Practice of Programming", Addison Wesley, 1999
[ChGe00] Chroust, Gerhard: "Software Engineering 1; Methoden, Vorgehen, Werkzeuge", Skriptum, 2000
[MoJa93] Mooney, James D.: "Issues in the Specification and Measurement of Software", Technikal Report, 1993
[MoJa94] Mooney, James D.: "Portability and Reusability: Common Issues and Differences", Technikal Report, 1994
[MoJa97] Mooney, James D.: "Bringing Portability to the Software Process", Technikal Report, 1997
[MoJa01] Mooney, James D.: "Developing Portable Software", Tutorial, 2001
[ScHe01] Schildt, Herbert: "C ent-packt", mitp-Verlag, 2001
[WeMa95] Weissenböck, Martin: "Borland C++ (Version 3.1)", Schriftenreihe der ADIM, Band 50, 1995
[WiDa98] Williams, David: "C++ portability guide", Version 0.8, 1998