libdebugmod is a very small C library to easily manage debug output in console or embedded applications.
Author: André Colomb [email protected]
Copyright (C) 2014 André Colomb
libdebugmod is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
libdebugmod is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with this program. If not, see http://www.gnu.org/licenses/.
Small programs, especially on embedded microcontrollers, often have different needs for runtime debugging output than large-scale desktop or server applications, where minimal runtime overhead or compiled binary size is less of a concern. Outputting debug messages during the program's execution is often the most valuable source of information for developers.
The traditional approach for controlling the level of debug verbosity is to assign each possible message a severity level and then filter out everything above a certain, runtime-configured level. This level can even be limited to a lower value for production builds with smaller binary size and better runtime performance.
With libdebugmod, messages are instead enabled per translation unit
(called a code module in this context). A translation unit is
typically one source file (.c or .cpp), or whatever the compiler
treats as the scope of a static
variable. This is coupled with a
decentralized architecture, where no central component needs to have a
list of all subsystems. Referencing modules via hard-coded string
identifiers makes it possible to still easily control the verbosity
during runtime, for example through text commands or a console.
The overhead is kept minimal and configurable to allow small executable sizes on embedded platforms. Production builds are easily stripped of almost all debugging code (see "Limitations" below). In contrast to traditional approaches, where debug output code is hidden by the preprocessor conditions, libdebugmod keeps the code visible to the compiler. Such debugging code is less likely to break during restructuring and leads to compilation errors before releasing broken debug code which the compiler has never had a chance to validate.
- Standard C streams as I/O descriptors make logging to files or console easy.
- Suitable for embedded systems, leaving the choice of output function
to the user (
fprintf()
not mandatory). - No wrapper functions, allowing the compiler to e.g. validate printf format strings.
- Zero runtime overhead for production builds.
- Enable / disable debug output for individual modules at runtime.
- Minimal runtime overhead when debugging is disabled.
- Decentralized architecture for independent code module subsystems.
- Function context is provided for each debug output.
- Complete control over output prefixes via callback function.
- Debugging code is always compiled and therefore consistent with other code changes (e.g. renamed variables).
- Small implementation with optional features.
- Relies on optimizing compiler for zero-overhead production builds
- Slight binary size overhead for strings used as module identifier or function context (compiler-dependent)
- Not thread-safe
There are no required dependencies for building the library. Since
the code uses features from the C99 language standard, the compiler
must support this standard and it might have to be explicitly enabled
with a command line switch (e.g. -std=c99
).
The source code and interfaces are well documented and prepared to
generate documentation with Doxygen. A handy configuration named
Doxyfile
is included.
A simple Makefile is included under the src
directory, compatible at
least with GNU make. It contains rules to create the static library
and a test program demonstrating the API. In addition, the dump
target outputs a disassembly of the test program to verify the library
overhead.
The number of debug modules which can be registered in a program is
limited during compilation of debug_mod.c
. This maximum can be
checked through the variable debug_mod_max
. To override it, define
the DEBUG_MOD_MAX
macro to a numeric value during compilation. The
default is four modules.
By default, compilation is done with the build machine's standard
tools like cc
and objdump
. To test the library with this default
toolchain, use the host
target:
make host
As another example usage, the avr
target provides a convenient way
to cross-build using the avr-gcc toolchain and then dump the
resulting disassembly for inspection:
make avr
To use the library in your own code, there are two possible build
scenarios. Either way, the include
directory should be added to the
preprocessor include path for your project.
When building a native host application or if you just want a binary
file for use in projects, make the lib
target in the provided
Makefile. The standard variables from the environment or provided on
the command line can control the toolchain options (CC, LD, CPPFLAGS,
CFLAGS, LDFLAGS). Example:
make -C libdebugmod/src/ CFLAGS="-g -O1" clean lib
Make sure to also configure optional library API features during this step, as explained under the section "Runtime management API" below. The built static library can then be regularly linked to your project by providing the linker with a library path:
cc -Ilibdebugmod/include/ -Llibdebugmod/src/ \
myproject.c -ldebugmod -o myproject
Or by directly referencing the archive file:
cc -Ilibdebugmod/include/ \
myproject.c libdebugmod/src/libdebugmod.a -o myproject
If the library source resides within your project directory, it may be easier to just include the source file for the API functions in your build process. This way, your toolchain configuration is always consistent with the libdebugmod build. Keep in mind that a C99 mode compiler is required, though.
cc -Ilibdebugmod/include/ \
myproject.c libdebugmod/src/debug_mod.c -o myproject
If your project uses a Makefile, simply add the include path to your
CPPFLAGS
variable, then add libdebugmod/src/debug_mod.c
to the
source files. Example Makefile excerpt:
DEBUGMOD_DIR = libdebugmod
CPPFLAGS += -I$(DEBUGMOD_DIR)/include
myproject: myproject.c $(DEBUGMOD_DIR)/src/debug_mod.c
To employ any libdebugmod functionality in one of your code modules, you need to:
#define DEBUG_MOD_ENABLE // This is optional
#include <debug_mod.h>
DEBUG_MOD_INIT("my module")
Without the DEBUG_MOD_ENABLE
macro, the debug output code is still
visible to the compiler, but will be removed by the optimizer. In
practice, it may be easier to define the DEBUG_MOD_ENABLE
macro via
command line flags to the preprocessor during a debug build instead of
hard-coding it into each source file.
The debug_mod.h
header file contains most of the API declaration and
provides the magic to separate debug channels for different
translation units. It must be included once per translation unit.
Just look at its contents or the generated Doxygen documentation for
details.
The DEBUG_MOD_INIT()
macro must also be used once per translation
unit, before any debug output. It defines the static storage used to
identify and register the current code module. Lazy people with a
good C preprocessor may use the magic constant __FILE__
for the
module identifier:
DEBUG_MOD_INIT(__FILE__)
Actual debug output is most conveniently generated with one of the
macros DEBUGF()
and DEBUGL()
. They are designed to provide great
flexibility around the stream I/O functions from the C Standard
Library, namely fprintf()
, fputs()
, fputc()
, etc. Your average
debug message may look like this:
fprintf(stderr, "Houston, we have %d problems.", count);
fputc('\n', stderr);
With libdebugmod, the calls should look like this:
DEBUGF(fprintf, "Houston, we have %d problems.", count);
DEBUGL(fputc, '\n');
The DEBUGF()
and DEBUGL()
macros expand to a code block which
- Is optimized away when
DEBUG_MOD_ENABLE
was not defined. - Evaluates a configured callback function to decide whether debug output is desired for the current module.
- Calls the given output function with the configured stream as first or last argument, respectively.
- Passes all remaining arguments to the output function verbatim.
The first two steps above can also be reused for code other than debug
output, for example to open a log file only when debugging is enabled.
The DEBUG_CONDITION
macro expands to an if (...)
statement with
the described behaviour.
The callback function serves two main purposes: a) Controlling whether
the specific debug statement should be executed by returning a
non-zero value and b) "preparing" the debug output, such as writing a
prefix (timestamp, etc.) to the output stream. In addition to the
current module's debug configuration, it is passed a string describing
the calling context. By default, the __func__
magic preprocessor
macro is used, but can be overridden setting DEBUG_MOD_CONTEXT
to
any expression evaluating to a string at runtime.
The library allows to flexibly reconfigure each module at run-time.
The callback function deciding whether to execute the debug statements
can be exchanged, or entirely suppressed. Switching the output stream
passed in DEBUGF()
and DEBUGL()
statements is also supported.
These configuration changes can be done from within each module for its own settings, as well as through a module-external API to allow for centralized management of debug facilities by the user.
Each module's own current output stream can be accessed directly
through debug_mod_get_stream()
and modified using
debug_mod_set_stream()
. The default output stream at initialization
is always stderr
.
Initially, each module's configuration is lazily initialized to use
the callback function address stored in debug_mod_default_func
. Set
the latter during early program initialization to provide a default
callback instead of disabling all debug modules.
Replacing the callback function is done via debug_mod_set_func()
,
where a function pointer of type debug_mod_f
must be passed in.
Setting the callback to NULL
disables debugging output for this
module altogether, which is equivalent to debug_mod_disable_self()
.
In some setups, a central module list may not be desired, but at some
point in time a complete enumeration of all registered modules is
needed. However, the lazy initialization only happens upon the first
evaluation of the DEBUG_CONDITION
, which implies all side-effects of
the respective callback function. To avoid these, use
debug_mod_register_self()
within each module, forcing registration
early in the program startup. The incremental_string_search.c
demo
program (explained below) includes such a usecase.
Other modules can control the debug output experience using the API
provided in debug_mod_control.h
. To access the respective API, you
need to:
#define DEBUG_MOD_DYNAMIC // This is optional (see below)
#define DEBUG_MOD_SAVE // This is optional (see below)
#include <debug_mod_control.h>
This header file also pulls in the standard debug_mod.h
header, so
the basic API for debug output can be used regularly after
initialization with DEBUG_MOD_INIT()
(see above). In addition the
function debug_mod_register()
is declared and can be used to
configure a debug module in advance.
Each module's configuration is lazily initialized during the first
evaluation of the DEBUG_CONDITION
, for example when first reaching a
DEBUGF()
call. At this time, the debug_mod_init()
function
arranges two things:
- Make sure the module's identifier is registered in a central list
using
debug_mod_register()
. - Apply the
debug_mod_default_func
address (typicallyNULL
), unless the module has already been registered, in which case the pre-recorded configuration is used.
This logic allows to enumerate modules and their individual settings
from a central place using debug_mod_register()
during program
initialization, like in the following example.
// Example output preparation: Prefix with function context
char cb_context(debug_mod* self, const char* context) {
fputs(context, self->stream);
fputs("(): ", self->stream);
return 1;
}
int main(void) {
// Enable debug output to stderr by default for any module
debug_mod_default_func = cb_context;
// Override a specific module's default stream
debug_mod dm = { cb_context, stdout, "my module" };
debug_mod_register(&dm);
/* main program logic */
return 0;
}
This part of the control API is optional and only available if the
macro DEBUG_MOD_DYNAMIC
was defined before including
debug_mod_control.h
. Remember to arrange this also when building
the static library, for example:
make -C libdebugmod/src/ CPPFLAGS=-DDEBUG_MOD_DYNAMIC clean lib
Additional functions exposed are debug_mod_update()
and
debug_mod_disable()
. In contrast to debug_mod_register()
, they
can be used to alter modules' configurations anytime after they have
been registered and used, from outside the affected module.
// Update callback and stream for all known modules
debug_mod_update(NULL, cb_context, stdout);
// Disable a single module's output again:
debug_mod_disable("my module");
// which is equivalent to:
debug_mod_update("my module", NULL, NULL);
Specifying a NULL
value for the module identifier applies the call
to all modules that have already been registered (explicitly or
automatically).
This part of the control API is optional and only available if the
macro DEBUG_MOD_SAVE
was defined before including
debug_mod_control.h
. Remember to arrange this also when building
the static library, for example:
make -C libdebugmod/src/ CPPFLAGS=-DDEBUG_MOD_SAVE clean lib
Additional functions exposed are:
debug_mod_list()
to iterate through all known modulesdebug_mod_save()
to conveniently save the current modules' configuration to an arraydebug_mod_restore()
to apply a previously stored configuration
debug_mod my_config[debug_mod_max]; // Note the maximum size
debug_mod_save(my_config, sizeof(my_config) / sizeof(*my_config));
// "my_config" may be copied to non-volatile memory now
memcpy(wherever, my_config, sizeof(my_config);
// ... during next run, load my_config again, then:
debug_mod_restore(my_config, sizeof(my_config) / sizeof(*my_config));
The source code includes a small demo program illustrating all of the
features named above. Inside the src/
directory, look at the files
test_debug_mod.c
and test_ext_module.c
. The Makefile
includes a
target to compile and run this demo using the host toolchain. Note
that the preprocessor macros to enable features must be defined
during this compilation step.
make -C libdebugmod/src/ \
CPPFLAGS="-DDEBUG_MOD_ENABLE -DDEBUG_MOD_DYNAMIC -DDEBUG_MOD_SAVE" \
clean test
Two compilation units are tested, using the preprocessor's special
__FILE__
macro for the module identifier. The test_debug_mod.c
module contains different example implementations for setup functions,
prepending the stream output with varying context information.
The test calls for API functions are all documented and use the
optional API if enabled. The main()
function demonstrates debug
output from an external module and its own, with various configuration
changes in between.
Another example lives in test_incremental_search.c
and shows a more
sophisticated usage of the runtime management API. It implements a
rather efficient incremental string search algorithm to match a
(partial) input against the list of registered debug modules,
exercising the debug_mod_list()
function. Use the following
Makefile
target to run the example code with appropriate macro
definitions already included.
make -C libdebugmod/src/ clean test-search