Project

General

Profile

Softwareentwicklung How-To

Allgemein

  • Mehrfach verwendete Funktionen - zumal wenn dies über die Grenzen eines Moduls geschieht- sollten an zentraler Stelle implementiert und gepflegt werden.
    Bei Funktionen/Klassen, die nichts mit DICOM zu tun haben, ist der beste Ort "ofstd".
  • Bevor eine low-level Systemfunktion benutzt wird, z.B. um die aktuelle Zeit zu ermitteln, sollte nachgeschaut werden, ob es nicht bereits eine entsprechende Kapselung im Modul "ofstd" gibt.
    Wenn nicht, sollte man überlegen, eine solche dort zu platzieren.
  • Globale Funktionen in einer Klasse (z.B. OFStandard) kapseln, um globalen Namensraum nicht zu verunreinigen.
  • Globalvariablen oder static-Variablen sind, soweit möglich, zu vermeiden. Wenn sie nötig sind, müssen sie auf jeden Fall für den Einsatz in Multithread-Umgebungen abgesichert werden.
    Dafür bieten "ofglobal.h" und "ofthread.h" geeignete Mittel an.

GIT

  • Nur Source Code, der zumindest auf dem Hauptentwicklungssystem (z.Z. 64 Bit Debian) kompiliert, darf in das git-Depot eingescheckt werden.
    • Außerdem soll der Code ohne irgendwelche Warnings kompilieren!
  • Format für Commit-Nachrichten in git:
    • Zunächst in die erste Zeile eine Kurzbeschreibung, die mit einem "." abschließt und insgesamt maximal 50 Zeichen haben sollte (ASCII).
    • Optional kann eine Langbeschreibung (ASCII) folgen, die über mehrere Zeilen (Unix Linebreak, d. h. "LF" und nicht "CR/LF", ...) gehen kann.
      Die Langbeschreibung wird mit einer vorangestellten Leerzeile von der Kurzbeschreibung getrennt.
      Die Langbeschreibung sollte nicht länger als 78 Zeichen pro Zeile sein. Beispiel Commit-Nachricht in git:
Support explicit item length denoting too many bytes.

Added flag that allows to ignore explicit item lengths that denote more
bytes than the contained elements actually contribute.

This closes DCMTK Feature #000.
  • Auf Basis der Log-Einträge werden automatisch die CHANGES-Dateien zwischen Snapshots/Releases erzeugt.
    Daher ist die Einhaltung der Zeichenlimits wichtig.
    Beispiel CHANGES-Eintrag:
**** Changes from 2013.10.31 (onken)

- Support explicit item length denoting too many bytes:
  Added flag that allows to ignore explicit item lengths that denote more
  bytes than the contained elements actually contribute.
  Affects: dcmdata/include/dcmtk/dcmdata/dcerror.h
           dcmdata/libsrc/dcerror.cc
           dcmdata/libsrc/dcitem.cc
           dcmdata/tests/tests.cc
           dcmdata/tests/tparser.cc
  • Wenn ein im Redmine-System eingetragener Bug/Feature/... geschlossen wurde, ist am Ende der Commit-Nachricht ein "Closes DCMTK Bug/Feature/... #nnn." anzufügen.
  • Die Windows-Plattform sollte auch getestet werden.
    Dies gilt insbesondere bei Systemabhängigkeiten wie bestimmten Systemfunktionen.
  • Soweit wie möglich auf Dateinamen mit "8.3" Zeichen und nur Kleinbuchstaben beschränken.
    Einzige Ausnahmen zur Zeit: CMakeLists.txt, dcm2avi2db.*
  • In allen Textdateien ausschließlich Zeilenumbrüche im Unix-Format verwenden,
  • Keine Tabulator-Zeichen *(stattdessen Leerzeichen).
  • Die "modules"-Datei (im gleichnamigen Modul) sollte immer aktuell gehalten werden.
  • Keine E-Mail-Adressen in den Source Code schreiben.
    Hinweise der Art "Thanks to ... <> for the bug report." sind ausschließlich in der CHANGES-Datei bzw. git-log (commit message) zugelassen.

Coding Style

  • Auf einen gemeinsamen Coding Style für das gesamte Toolkit wird man sich wohl kaum einigen können. Es sollte aber pro Datei (möglichst sogar Modul) einigermaßen einheitlich sein.
  • Wenn eine Klasse Member-Variablen vom Typ Pointer enthält, sollten der Copy-Konstruktor sowie der Assignment-Operator entweder implementiert oder als "private" deklariert (und nicht implementiert) werden.
    Dies gewährleistet, daß der Compiler keine Defaults für diese Methoden generiert, die eine "flache" Kopie eines Objektes erzeugen und damit ggf. Speicherprobleme erzeugen.
  • Beim Übersetzen mit gcc ist es hilfreich, relativ "scharfe" Optionen für die Warnings einzuschalten - und, soweit möglich, zu beherzigen:
COMMONFLAGS += -Wall -Wshadow -Wpointer-arith -Wsign-compare \
               -Wwrite-strings -Wconversion -pedantic
CFLAGS   += $(COMMONFLAGS) -Wstrict-prototypes -Wmissing-prototypes
CXXFLAGS += $(COMMONFLAGS) -Wold-style-cast -Woverloaded-virtual -Wsynth
  • Noch schärfer wären folgende Flag, allerdings wird dies schon recht störend:
    CXXFLAGS += -Weffc++
    

Bezeichner

  • Keine Bezeichner für Variablen und dergleichen wählen, die zu Namenskonflikten führen können, wie z.B. "index", "string", "list", "stack".
    Typische "Kandidaten" für Namenskonflikte sind:
    • Systemfunktionen wie index()
    • Klassen im namespace std wie std::string, std::list, std::stack.

Programmiersprachen-Features

  • Kein RTTI (d.h. auch kein dynamic_cast<>) und keine C++ Exceptions verwenden.
  • Weitgehender Verzicht auf C++ Namespaces - bisherige Ausnahme "std".
  • Es sollten die in ofcast.h definierten Typecast Operatoren verwendet werden, also:
    OFconst_cast, OFstatic_cast, OFdynamic_cast, OFreinterpret_cast.
    Hilfreich unter gcc ist bei der Umstellung von "altem" Code der Compiler-Schalter "-Wold-style-cast".
  • Keine STL-Klassen direkt verwenden, da DCMTK auch dort übersetzen soll, wo die Standard Template Library noch nicht verfügbar ist.
    Für std::list, std::stack, std::string gibt es mit OFList, OFStack und OFString plattformunabhängige Varianten,
    die ggf. durch die DCMTK-Makefiles automatisch durch die entsprechenden STL-Klassen ersetzt werden können.
  • Datenstrukturen, die mit new[] angelegt werden, müssen mit delete[] wieder abgeräumt werden.
    Das gilt auch für PODs ("einfache Datentypen"):
  char *c = new char[100];
  delete[] c;  // "delete c;" ist verboten (undefined behaviour)!

Includes

  • Jeder Header muß durch einen "Guard" vor mehrfachem #include geschützt werden:
      #ifndef FILENAME_H
      #define FILENAME_H
      /* hier kommt der eigentliche Header */
      #endif /* FILENAME_H */
    

Achtung: Präprozessorsymbole dürfen nicht mit "_" beginnen, also nicht "__FILENAME_H" o.ä. verwenden. Dies sind reservierte Symbole.

  • Jede C++ Datei (ob Header oder Implementierung) muß als erstes (vor jeder anderen Deklaration mit Ausnahme des "Guards") "osconfig.h" includieren.
  • Jeder Header sollte alle notwendigen #includes für Datenstrukturen und Funktionen, die im Header verwendet werden, enthalten.
    Ob ein Header "komplett" ist, läßt sich relativ leicht dadurch prüfen, daß man in der zugehörigen Implementierungsdatei den Header direkt nach "osconfig.h" einbindet:
  #include "osconfig.h" /* immer als erstes */
  #include "filename.h" /* Header, der zu dieser Datei <filename.cc> gehört */
  #include ...alles andere...
  • Immer nur das minimal notwendige inkludieren. Dies gilt insbesondere für die #include-Anweisungen in Header-Dateien.
    Wenn eine Klasse im Header nur als Pointer oder Referenz auftaucht, reicht eine Forward-Deklaration aus:
  class MyClass;
  ...
  void myFunction(MyClass *parameter);
  • Bei System-Bibliotheken sehr aufmerksam sein: zunächst nachschauen, ob es in ofstdinc.h eine entsprechende Variante gibt.
    Ansonsten suchen, wie das in anderen Dateien gehandhabt wurde (wg. #ifdef's, EXTERN_C, usw.) Verboten sind insbesondere:
      <iostream> <ios> <fstream> <iomanip> <sstream> <strstream>
      <iostream.h> <fstream.h> <strstrea.h> <strstream.h> <sstream.h>
      <iomanip.h> <string> <stack> <list> <algorithm> <assert.h> <cassert>
      <cctype> <cerrno> <cfloat> <ciso646> <climits> <clocale> <cmath>
      <csetjmp> <csignal> <cstdarg> <cstddef> <cstdio> <cstdlib> <cstring>
      <ctime> <ctype.h> <cwctype> <errno.h> <float.h> <iso646.h>
      <limits.h> <locale.h> <math.h> <setjmp.h> <signal.h> <stdarg.h>
      <stddef.h> <stdio.h> <stdlib.h> <streambuf.h> <streambuf> <string.h>
      <strings.h> <time.h> <wctype.h>
    
Alle diese Header können in plattformunabhängiger Art über "ofstdinc.h", "ofstream.h", "ofstring.h", "oflist.h" und "ofstack.h" angesprochen werden.
  • Unix/Posix-Header-Files mit Ausnahme der oben genannten sind in der Regel reiner C-Code.
    Wenn diese eingebunden werden sollen, mit den Makros BEGIN_EXTERN_C und END_EXTERN_C klammern.
    Desweiteren sollten derartige Header-Files immer durch einen Configure-Test geprüft werden.
    Ergo:
      BEGIN_EXTERN_C
      #ifdef HAVE_UNISTD_H
      #include <unistd.h>
      #endif
      END_EXTERN_C
    
  • Falls NULL im Header verwendet wird, sicherstellen, dass NULL auch definiert ist.
    Dazu <unistd.h> einbinden, wenn vorhanden (s.o.); ausserdem cstdlib:
      #define INCLUDE_CSTDLIB  /* defines NULL on ANSI/ISO C++ platforms */
      #include "ofstdinc.h" 
    

Ein-/Ausgabe-Ströme

  • Bei der Verwendung von C++ IO-Streams ofstream.h inkludieren.
  • Wo immer es geht, C++ Streams verwenden und nicht FILE*.
  • Statt stdin/stderr in Hauptprogrammen COUT/CERR verwenden.
    Bei Bibliotheken das globale Objekt ofConsole oder besser eine Member-Variable vom Typ OFConsole, die per setLogStream() gesetzt werden kann.
    Die OFConsole-Klasse bietet Reentranz, ist also MT-safe.

Zeichenketten

  • Wo immer es geht OFString (C++ String-Klasse) verwenden.
  • Wenn C-Strings verwendet werden, ist sicherzustellen, daß es keinen Buffer Overflow geben kann.
    Daher nicht strcpy(), strncpy oder strcat() verwenden, sondern OFStandard::strlcpy() und OFStandard::strlcat().
  • Kein sprintf() bzw. sscanf() mit float Variablen, da hier das Dezimaltrennzeichen locale-abhängig ist.
    Stattdessen OFStandard::ftoa() bzw. atof() verwenden.
  • Sparsamer und vorsichtiger Umgang mit sprintf() wegen möglicher Buffer Overflows.
    Alternative: OFOStringStream, siehe "ofstream.h"

Dokumentation

  • API-Dokumention aller Klassen, implementierten Methoden und deklarierten Member-Variablen, sonstigen Funktionen und globalen Variablen mit Hilfe von Doxygen.
    Erzeugung der HTML-Dokumentation per "make html". Bitte Warnungen in "htmldocs.log" beachten und Code ggf. anpassen.
  • Zusätzlich gibt es eine allgemeine Toolkit-Beschreibung in doxygen/htmldocs.dox (basiert auf README) sowie pro Modul eine <Modulname>.dox im jeweiligen docs-Verzeichnis (siehe dcmdata/docs/dcmdata.dox), die bei der Erstellung der HTML-Dokumentation verwendet wird. Hier sind ggf. ebenfalls Anpassungen vorzunehmen.
  • Zu jedem Kommandozeilenprogramm in einen apps-Verzeichnis muss eine entsprechendes MAN-Datei im docs-Verzeichnis existieren (gleicher Name, aber Endung "man").
    Diese sollte immer aktuell gehalten werden. Bitte auch manpages in "doxygen/manpages" aktualisieren (make man) und einchecken.
  • Hinweis zur Erstellung der man pages (make man in dcmtk oder dcmtk/doxygen): Eine Beispielvorlage findet sich in der Datei dcmdata/docs/dcmdump.man.
    Bitte beachten:
    • Überschriften gegenüber TXT-Dateien tw. geändert (z.B. DESCRIPTION)
    • neue allg. Abschnitte hinzugekommen (z.B. COMMAND LINE und COPYRIGHT)
    • Hervorhebungen von Programmnamen durch \b (bold), von Dateinamen, Environmentvariablen und dergleichen durch \e bzw. <em></em> (emphasized)
    • Formatierung des OPTIONS-Abschnittes gegenüber TXT-Dateien geändert
  • Wenn Toolkit-weit Änderungen durchgeführt werden, nachschauen, ob evtl. die allgemeine Dokumentation
    (INSTALL, README, ..., docs/*, config/docs/*) angepasst werden muß.
  • Präprozessorsymbole, mit denen man das Verhalten des Codes gezielt verändern kann
    (bestimmte Features ein- oder ausschalten), sind in config/docs/macros.txt zu dokumentieren.
  • Umgebungsvariablen, die das Verhalten des Codes beinflussen, sind in config/docs/envvars.txt zu dokumentieren.

Kommandozeilen-Programme

  • Jedes Kommandozeilen-Programm hat gewisse Standard-Optionen. Minimal sind dies: --help und --version.
    Weitere übliche Optionen: --verbose, --debug. Bei anderen Optionen zunächst nachschauen,
    ob nicht bereits ein anderes Programm eine derartige benutzt. In diesem Fall auf Konsistenz achten!

Multi-threading

  • Wenn _REENTRANT definiert ist, sollte die Bibliothek (nicht unbedingt die Kommandozeilen-Programme) MT-safe sein.
    Daher bei Systemfunktionen auf die richtige Auswahl achten:
    unter Windows automatisch gegeben,
    wenn im Compiler entsprechend konfiguriert; unter Unix gibt es häufig eine entsprechende _r Funktion, z.B. strerror_r().
    Achtung: unter unterschiedlichen Unix-Varianten kann dies unterschiedlich sein!
  • Verwendung von ofConsole bzw. einer Variablen vom Typ OFConsole statt cout und cerr - siehe auch unter "Ein-/Ausgabe-Ströme".

Testen

  • Das Auskommentieren von Tests, die auf bestimmten Plattformen nicht laufen, ist keine adäquate Lösung. (Ausnahme: der atof-Test mit VC6)
  • ... regression / unit testing ...

Plattformen

  • Welche #ifdef's für welche Plattform (z.B. Windows, Cygwin, MinGW) ...
  • Das Abfragen der Compiler-Version per #ifdef ist jedenfalls keine Lösung (dafür gibt es configure-Tests).
  • Ähnliches Problem: Das Abfragen des Compiler (z. B. MinGW) ist auch keine Lösung,
    da sich die Fähigkeiten eines Compilers über die Zeit ändern/erweitern, die #ifdef's aber nie wieder angefasst werden.

Daher sind configure-Tests die bessere Lösung