DCMTK 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:
- Table of contents
- DCMTK Development How-To
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 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: Applications 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.
- 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)
Applications¶
- 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!
- TODO: Update/enhance
- Use exit codes consistently.
C++ Features¶
- DCMTK allows use of some C++11 language features
- In particular DCMTK implements some data structures / features that were made available in C++11 through legacy implementations which are enabled per default. If C++11 or later is enabled DCMTK can also be enabled to make use of the corresponding C++11 counterparts. See this documentation page for details.
- DCMTK does not make use of namespaces
- The only exception so far: "std".
- There are few exceptions but don't introduce new without team discussion
- 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.
- We rarely use RTTI : * You can use dynamic_cast() in the form of OFdynamic_cast() when it is applicable. * So far we neither use the typid() operator nor the type_info class. If you feel that is necessary for your use case, you should discuss it with the team.
- Avoid the use of C++ exceptions.
- 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)!
C++ Standard Library¶
- 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 if enabled in the build configuration
Strings¶
- Prefer OFString (C ++ String class) over C strings (const char*) wherever possible.
- 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 (use OFStandard::snprintf() instead).
Alternative: OFOStringStream, see "ofstream.h"
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.
Logging¶
DCMTK logs all message using its own logging library "oflog", which itself is a modified copy of log4cplus .
Log Levels¶
DCMTK only uses log levels:- TRACE: Very fine-grined log messages that are usually only interesting during development or when chasing bugs.
- DEBUG: Gives detailed information what the application/libraries are doing right now, but more than it is useful in normal operation.
- INFO: Logs messages that are useful to be seen in normal operation conditions. Make sure to not print too many of them.
- WARN: Logs warning messages
- ERROR: Log error messages
- FATAL: Log fatal error messages that lead to the "immediate" exit of the current application.
Note in particular the difference between ERROR and FATAL: FATAL is currently only being used in application (not library) code and should only be used if the error is not recoverable (and therefore leads to exiting the application).
Coding Style¶
Variables and Methods¶
- 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.
- 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.
- 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 and make sure that it works at least on Linux, Windows and MacOS
- 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 multi-threaded environments. "ofglobal.h" and "ofthread.h" offer suitable means for such variables.
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 including 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. Also look how it was handled in other files (eg # ifdef's, EXTERN_C, etc.).
- Sometimes you may want to use Unix / Posix header files that are 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"
Exit Codes¶
When exiting the application, return 0 means "no error". If an error occurs, a value between 1 and 127 must be used. So far, most applications have their own list of error codes that are sometimes even only spread throughout source code and are not documented.
However, some applications already rely on the error codes (constants) defined in dcmtk/ofstd/ofexit.h and if you write a new application, you should try to use these in your application whenever possible.
Formatting¶
- 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).
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
- Use Unix line breaks, 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 (TODO: Remove CHANGES example, not relevant?):
**** 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.
- 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... <mail@domain.org> for the bug report." are only allowed in git-log (commit message).
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> 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.
Multi-threading¶
- If multi-threading support is detected during DCMTK configuration (cmake, autoconf), DCMTK allows usage of thread features
- Use OFThread and related abstractions (OFMutex, ...?) instead of using system-dependent implementations.
- If _REENTRANT is defined during compilation, 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!
- Use thread-safe ofConsole or a variable of type OFConsole instead of using cout and cerr directly - see also "Input / Output Streams".
Cross-Platform¶
- TODO Update/Enhance
- 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
Testing¶
Before checking new code into DCMTK, you must test it accordingly:- 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 ++
- The Windows platform (in particular) should also be tested. This is especially true for system dependencies such as certain system functions.
Dashboard¶
- Once changes have been pushed to DCMTK's testing branch on caesar, they will undergo cross-platform compilation and (unit) testing on the DCMTK Dashboard .
- The build usually happens overnight so on the day after the commit you should be able to see all warnings and errors produced on the dashboard platforms
- Fix warnings as errors as possible. If something appears not to be "fixable" or should not be fixed in your opinion, inform/ask team about it.
Unit Tests¶
- 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.
- The tests all use a basic OFTest framework that you should also follow. Mostly this requires:
- 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)
Integration Tests¶
- The DCMTK team internally uses a set of integration tests that call the various command line tools and systematically exercise all options and features accessible via the command line
- These tools are not available to DCMTK users outside the core DCMTK developer team, sorry.
- The git repository is located at /share/dicom/git-depot/dcmtk-integration-tests.git on server "caesar".
- Whenever a new command line tool, command line option or application feature is implemented, a set of test cases should be added
- Note that the integration tests are different from the unit tests in DCMTK: The unit tests contain specific test routines testing certain API functions. The integration tests test complete applications.
- The execution of the program under test is controlled by one out of two "driver" applications:
- The "client/server driver" starts a server application, waits until the server port is accessible via TCP/IP, and then starts the client tool. After termination of the client it runs the evaluation script. This driver is used to test network tools such as storescp/storescu.
- The "sequential driver" performs a number of command line calls sequentially. This driver is used to test tools operating on files, such as dcmdump, dcmsign or dcmcjpeg.
- The test scripts themselves are written in CMake. The best way to learn how to do this is to look at the existing tests. Here is an example of a sequential test:
add_dcmjpeg_test(decode_lossless_sv1_compressed_image DELETE_BEFORE_TEST "*.raw" COMMAND_01_COMMANDLINE DCMTK::dcmdjpeg -v "${CMAKE_CURRENT_SOURCE_DIR}/data/decoder/jpeg_lossless_sv1.dcm" jpeg_lossless_sv1_d.dcm COMMAND_02_COMMANDLINE DCMTK::dcmdump --write-pixel . jpeg_lossless_sv1_d.dcm EVALUATION_SCRIPT "command_status_in(01 0)" "dicom_dumps_equal(\"${CMAKE_CURRENT_SOURCE_DIR}/data/decoder/jpeg_lossless_sv1_d.dump\" \"command02.log\")" "files_equal(\"${CMAKE_CURRENT_SOURCE_DIR}/data/decoder/jpeg_lossless_sv1_d.dcm.0.raw\" \"jpeg_lossless_sv1_d.dcm.0.raw\")" )
- This test first deletes all files named "*.raw" in the working directory for this test case;
- It then calls dcmdjpeg and decompresses a file that is provided in the integration test suite. The output is written to the working directory;
- It then calls dcmdump on the decompressed file with an option that extracts the pixel data to a separate file;
- It then executes the evaluation script, which consists of two CMake macros. These compare the output of dcmdump (captured in "command02.log") with a pre-defined output and the raw pixel data with a file provided as part of the test suite;
- The test case will fail if either macro reports a failure.
- The macros available for the evaluation script are defined and documented in dcmtk-integration-tests/drivers/CMake/DCMTKIntegrationTestsEvaluationScript.cmake.
- Here is an example of a client/server test:
add_dcmnet_test(echoscu_to_storescp_calling_aet CLIENT_COMMAND DCMTK::echoscu --aetitle INTEGRATION_T1 -d localhost $<TCP_PORT> SERVER_COMMAND DCMTK::storescp -d $<TCP_PORT> EVALUATION_SCRIPT "client_status_in(0)" "file_matches(\"server.log\" \"Calling Application Name: +INTEGRATION_T1\")" )
- This test first starts storescp using a port number assigned by the client/server driver
- The driver then tries to connect to that port using TCP/IP, and will repeat this until the server is reachable
- Then echoscu is started as a client and instructed to connect to storescp
- Finally the evaluation script is run. It checks whether echoscu returned a zero (success) return code and whether the correct calling AEtitle is found in storescp's log output.
- The execution of test cases can be made conditional depending on the availability of certain tools or features. For example, the following test will be executed when "make" is run:
if (TARGET DCMTK::dcmcjpeg) DCMTKFunctionalTests_query_optional_component_nofail("dcmjpeg" "$<TARGET_FILE:DCMTK::dcmcjpeg>" "--version" "ZLIB" "- ZLIB, (Version[ ]+[0-9\\\\.]+)" "DCMTK_COMPILED_WITH_ZLIB" ) else() file(REMOVE "DCMTK_COMPILED_WITH_ZLIB") endif()
- This test executes dcmcjpeg (if known) with the --version option and checks if ZLIB is mentioned there. Depending on the result, it either creates or deletes a file named "DCMTK_COMPILED_WITH_ZLIB".
- For each test case, dependencies are defined using set_tests_properties(). The following property would prevent execution of the client server test described above if DCMTK is not compiled with ZLIB support:
set_tests_properties( dcmnet_echoscu_to_storescp_calling_aet PROPERTIES REQUIRED_FILES "${CMAKE_CURRENT_BINARY_DIR}/DCMTK_COMPILED_WITH_ZLIB" )
- Before committing and pushing a new test case to the upstream git repository, make sure that the test case successfully executes on your development platform.
- All test cases will be executed automatically for many of the nightly builds, on many different platforms: https://support.dcmtk.org/dashboard/index.php?project=DCMTK+Functional+Tests
- make sure that you monitor the test execution for a couple of days and fix test failures, which may be caused by a problem in the test definition, or by failure of your new tool/option/feature on a certain platform.
- Note: the machines that run these tests are under heavy load. Sometimes, a test failure is simply caused by a timeout because the system is overloaded