Project

General

Profile

Software Development How-To

This document describes guidelines for internal DCMTK development. It explains how DCMTK is structured and what you have to consider if you want to change DCMTK code or even want to add your own library.

The document is structured as follows:

If you look for help how to use DCMTK from an external project (i.e. code that just uses DCMTK functionality but does not change DCMTK's source code), you should look at the howtos provided here (especially under "General" and "Compiling").

Introduction

DCMTK consists of several libraries and command line programs that demonstrate the features of the libraries. Libraries and apps are grouped together in so-called "modules" that relate to a specific DICOM feature or utility feature. Modules reside in their own sub directory in the DCMTK source code's main directory. Here is a current list:

config
dcmdata
dcmect
dcmfg
dcmimage
dcmimgle
dcmiod
dcmjpeg
dcmjpls
dcmnet
dcmpmap
dcmpstat
dcmqrdb
dcmrt
dcmseg
dcmsign
dcmsr
dcmtls
dcmtract
dcmweb
dcmwlm
oficonv
oflog
ofstd

Note that there are a few other subdirectories (such as "CMake") which contain documentation, scripts and the like and which are not considered "modules".

Module Structure

A DCMTK module usually has the same internal structure consisting of the following subdirectories:
  • apps: Aapplications source code that demonstrate the capabilities of this module
  • data: Data files such as example data for the given module and/or its applications
  • docs: Man pages (manuals) for the applications in the apps folder. May also contain further documentation files.
  • etc: Configuration files used by the applications in the apps folder
  • include: Include (".h") files of the source code
  • libsrc: Source code (".cc" for C++ or ".c" for C) files. Often these files together are compiled into a single library (like "dcmimage.lib" for "dcmimage/libsrc").
  • tests: Unit tests that test the library code in libsrc.
Some of the subdirectories might be missing if the module does not have any files for this category. The "core" structure that nearly every DCMTK module uses as a minimum is:
  • apps
  • include
  • libsrc
  • tests

In some cases it can make sense to deviate from the general directory structure layed out above. For example, the dcmsr module has an additional library folder "dcmsr/libcmr" containing a second, separate library apart from the "dcmsr.lib" that is produced by "dcmsr/libsrc" but you should discuss planned deviations with the team -- maybe they are not necessary at all.

Header locations

The "include" directory has a common sub structure, here shown for the include folder of the "dcmimage" module

├── dcmimage
│   ├── apps
│   ├── data
│   ├── docs
│   ├── etc
│   ├── include
│   │   ├── dcmtk
│   │   │   └── dcmimage
│   │   │       ├── dcmicmph.h
│   │   │       ├── diargimg.h
│   │   │       ├── ...


Note that the dcmimage/include/dcmtk/dcmimage subdirectory name (in the back) is always identical to the module name (in the front). This allows us later to install all include files from all modules into a common include installation folder:
└── dcmtk
    ├── config
    ├── dcmdata
    │   ├── cmdlnarg.h
    │   ├── dcbytstr.h
    │   ├── ...
    ├── dcmect
    │   ├── def.h
    │   ├── enhanced_ct.h
    │   └── ...
    ├── dcmfg
    │   ├── concatenationcreator.h
    │   ├── concatenationloader.h
    │   └── ...
    ├── dcmimage
    │   ├── dcmicmph.h
    │   ├── diargimg.h
    │   └── ...
    ...

This has the advantage that the 3rd party app can access the DCMTK headers using the scheme:

#include "dcmtk/<module>/<include>.h" 

e.g.
#include "dcmtk/dcmdata/include.h" 

(TODO: Probably this should go to a section like "Using DCMTK" later)

General

  • Regularly used functions - especially if this happens beyond the limits of a module- should be implemented and maintained centrally.
    "ofstd" is the best place for functions and classes that have nothing to do with DICOM.
  • Before using a low-level system function, e.g. to determine the current time, please check if there is not already a corresponding encapsulation in the module "ofstd".
    Otherwise, you should consider placing one there.
  • Encapsulate global functions in a class (e.g. OFstandard) so as not to contaminate the global namespace.
  • Global variables and static variables should be avoided as far as possible. If necessary, If they are absolutely necessary, they must be secured for use in multithreaded environments.
    "ofglobal.h" and "ofthread.h" offer suitable means for such variables.

GIT

  • Only source code that compiles on at least the main development system (currently 64 bit Debian), may be inserted in the git depot.
    • In addition, the code should compile without any warnings!
  • Format for commit messages in git:
    • First of all, the first line must be a short description ending with a "." and with a maximum of 50 characters (ASCII).
    • Optionally, a long description (ASCII) may follow over several lines
      (Unix linebreak, ie "LF" and not "CR / LF", ...).
      The long description is separated from the short description by a leading blank line.
      The long description should not be longer than 78 characters per line. Example commit message in git:

Support explicit length 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.
  • Based on the log entries, the CHANGES files are automatically generated between snapshots / releases.
    Therefore, compliance with the line limits is quite important.
    Example CHANGES entry:
**** Changes from 2013.10.31 (onken)

- Support explicit length 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
  • If a bug / feature / ... entered in the Redmine system has been closed, add "Closes DCMTK Bug / Feature / ... #nnn." at the end of the commit message.
  • The Windows platform should also be tested.
    This is especially true for system dependencies such as certain system functions.
  • Limit as far as possible to file names with "8.3" characters and only lowercase letters.
    Only exceptions at the moment: CMakeLists.txt, dcm2avi2db.*
  • Use only line breaks in Unix format in all text files,
  • no tab characters * (spaces instead).
  • The "modules" file (in the module of the same name) should always be up to date.
  • Do not write e-mail addresses in the source code.
    Notes such as"Thanks to... <> for the bug report." are only allowed in the CHANGES file or git-log (commit message).

Coding Style

  • We probably won't agree on a common coding style for the entire toolkit.
    However, it should be fairly consistent within each file (even module if possible).
  • If a class contains pointer-type member variables, the copy constructor and the
    Assignment operator should either be explicitly implemented or declared as "private" (and not implemented).
    This ensures that the compiler does not generate any defaults for these methods that produce a "flat" copy of such objects and thus possibly create memory problems.
  • When translating with gcc, it is helpful to turn on relatively "sharp" options for the warnings - and, as far as possible, to interiorize:
COMMONFLAGS + = -Wall -Wshadow -Wpointer-arith -Wsign-compare \
               -Write strings -Wconversion -pedantic
CFLAGS + = $ (COMMONFLAGS) -Wrict prototypes -Wmissing prototypes
CXXFLAGS + = $ (COMMONFLAGS) -Wold-style-cast -Woverloaded-virtual -Wsynth
  • The following flag would be even sharper but this gets quite disturbing:
     CXXFLAGS + = -Weffc ++
    

Identifiers

  • Do not select identifiers for variables and the like that can lead to naming conflicts, such as "index", "string", "list", "stack".
    Typical "candidates" for naming conflicts are:
    • System functions like index ()
    • Classes in namespace std like std::string, std::list, std::stack.

Programming language features

  • Avoid RTTI (i.e. also dynamic_cast <>) and avoid the use of C ++ exceptions.
  • Avoid C ++ namespaces as far as possible- only exception so far: "std".
  • The typecast operators defined in ofcast.h should be used, ie:
    OFconst_cast, OFstatic_cast, OFdynamic_cast, OFreinterpret_cast.
    The compiler switch "Wold-style-cast" is helpful when changing from "old" code.
  • Do not use STL classes directly because DCMTK should also translate when the standard template library is not yet available.
    For std::list, std::stack, std::string there are platform-independent variants: OFList, OFStack and OFString, which may be automatically replaced by the appropriate STL classes by the DCMTK makefiles.
  • Data structures created with new [] must be cleared with delete [].
    This also applies to PODs ("simple data types"):
  char * c = new char [100];
  delete [] c; // "delete c;" is forbidden (undefined behavior)!

Includes

  • Each header must be protected by a "guard" before using multiple #include:
  #ifndef FILENAME_H
  #define FILENAME_H
  / * here comes the actual header * /
  #endif / * FILENAME_H * /

Attention: Preprocessor symbols should not start with "_", so avoid"__FILENAME_H" and the like. These are reserved symbols.

  • Each C ++ file (whether header or implementation) must include "osconfig.h" first
    (before any other declaration except for the "Guards").
  • Each header should contain all necessary #includes for data structures and functions,
    contained in the header.
    Whether a header is "complete" can be checked relatively easily by includi the header directly after "osconfig.h" in the associated implementation file:
  #include "osconfig.h" / * always first * /
  #include "filename.h" / * Header associated with this file <filename.cc> * /
  #include ... everything else ...
  • Always include only the minimum necessary. This is especially true for the #include statements in header files.
    If a class only appears in the header as a pointer or reference, then a forward declaration is sufficient:
  class MyClass;
  ...
  void myFunction (MyClass * parameter);
  • Be very attentive with system libraries: first check if there is a corresponding variant in ofstdinc.h.
    Otherwise, look how it was handled in other files (eg # ifdef's, EXTERN_C, etc.). Avoid particularly:
  <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>

All of these headers can be addressed in a platform-independent manner via "ofstdinc.h", "ofstream.h", "ofstring.h", "oflist.h" and "ofstack.h".

  • Unix / Posix header files except the ones mentioned above are usually pure C code.
    If these are to be included, use the macros BEGIN_EXTERN_C and END_EXTERN_C to cling.
    Furthermore, such header files should always be checked by a configure test.
    Summary:
      BEGIN_EXTERN_C
      #ifdef HAVE_UNISTD_H
      #include <unistd.h>
      #endif
      END_EXTERN_C
    
  • If NULL is used in the header, make sure that NULL is also defined.
    Include <unistd.h>, if available (s.o.); also cstdlib:
      #define INCLUDE_CSTDLIB / * defines NULL on ANSI / ISO C ++ platforms * /
      #include "ofstdinc.h" 
    

I / O streams

  • Include when using C ++ IO streams ofstream.h.
  • Use C ++ streams wherever possible, not FILE *.
  • Use COUT / CERR instead of stdin / stderr in main programs.
    For libraries, the global object ofConsole, or better, a member variable of type OFConsole, which can be set by setLogStream ().
    The OFConsole class offers reentrance, so it is MT-safe.

Strings

  • Prefer OFString (C ++ String class) over C strings (const char*) wherever possible.
  • Don't use std::string.
  • If C-strings are used, make sure that there is no buffer overflow.
    Therefore, do not use strcpy (), strncpy, or strcat (), but use OFStandard :: strlcpy () and OFStandard :: strlcat ().
  • No sprintf () or sscanf () with float variables, since here the decimal separator is locale-dependent.
    Instead, use OFStandard :: ftoa () or atof ().
  • Sparing and careful use of sprintf () for possible buffer overflows.
    Alternative: OFOStringStream, see "ofstream.h"

Documentation

  • API-Documentation for all classes, implemented methods and declared member variables, other functions and global variables with the help of Doxygen.
    Generation of HTML documentation via "make html". Please observe warnings in "htmldocs.log" and adapt the code if necessary.
  • Additionally there is a general toolkit description in doxygen / htmldocs.dox (based on README)
    and per module a <module name> .dox in the respective docs directory (see dcmdata / docs / dcmdata.dox),
    which is used when creating the HTML documentation. If necessary, adjustments should also be made here.
  • For each command line program in an apps directory, a corresponding MAN file must exist in the docs directory (same name but suffix "man").
    This should always be kept up to date. Please also manpages in "doxygen / manpages" update (make man) and check in.
  • Note on creating the man pages (make man in dcmtk or dcmtk / doxygen): A sample template can be found in the file dcmdata / docs / dcmdump.man.
    Please note:
    • Headings versus TXT files tw. changed (for example DESCRIPTION)
    • new general sections have been added (for example COMMAND LINE and COPYRIGHT)
    • Highlighting program names by \ b (bold), filenames, environment variables, and the like by \ e or <em> (emphasized)
    • Formatting of the OPTIONS section changed compared to TXT files
  • If toolkit-wide changes are made, see if any the general documentation
    (INSTALL, README, ..., docs / *, config / docs / *).
  • Preprocessor symbols that allow you to change the behavior of the code
    (turn certain features on or off) must be documented in config / docs / macros.txt.
  • Environment variables that affect the behavior of the code must be documented in config / docs / envvars.txt.

Command line tools

  • Each command line program has certain standard options. Minimal are: --help and --version.
    Other common options: --verbose, --debug. Look at other options first,
    if another program is not already using one. In this case, pay attention to consistency!

Multi-threading

  • If _REENTRANT is defined, the library (not necessarily the command-line programs) should be MT-safe.
    Therefore, with system functions, make sure you make the right selection:
    automatically given under Windows,
    if configured in the compiler accordingly; under Unix there is often a corresponding _r function, e.g. strerror_r ().
  • Attention: under different Unix variants this can be different!
  • Using ofConsole or a variable of type OFConsole instead of cout and cerr - see also "Input / Output Streams".

Testing

  • If you write new library functionality, add related unit tests to the modules tests directory.
    • The tests all use a basic OFTest framework that you should also follow. Mostly this requires:
      In your test file <module>/tests/<test_file>.cc
        #include "dcmtk/ofstd/oftest.h" 
        OFTEST($TEST_NAME)
        {
          OFCHECK($CONDITION)
          OFCHECK(...)
          ...
        }
      

      where $TEST_NAME is the name of the test you want to write, and OFCHECK is used to check a test condition (i.e. $CONDITION should evaluate to true or false). On top, you need to register $TEST_NAME in the <module>/tests/test.cc file that looks like this:
      #include "dcmtk/config/osconfig.h" 
      #include "dcmtk/ofstd/oftest.h" 
      
      OFTEST_REGISTER($TEST_NAME);
      // add more OFTEST_REGISTER calls for other tests
      
      OFTEST_MAIN("$MODULE") // $MODULE is the module name
      
    • The easiest way to do all this is to look at existing tests and just copy tests.cc and a test case file, and then adapt it to your needs.
  • Commenting out tests that are not running on some platforms is not an adequate solution.
    • Of course this does not hold for platform-specific tests
  • If you want to test on the application level (i.e. whether your applications in the module's apps directory) work as expected, add them to the integration test suite (see /share/dicom/git-depot/dcmtk-integration-tests.git on caesar)

Platforms

  • Which # ifdef's for which platform (for example Windows, Cygwin, MinGW) ...
  • Querying the compiler version via #ifdef is definitely not a solution (there are configure tests for that).
  • Similar problem: Query of the compiler (eg MinGW) is not a solution
    as the capabilities of a compiler change / expand over time, the #ifdefs are never modified again.

Therefore, configure tests are the better solution