Features


A Cleanup Framework in C

Stephane Grad

Use this simple framework to ensure proper resource cleanup.


Constructors and destructors are very important tools for C++ developers. When these tools are used properly, they reduce implementation time and source code complexity. When combined with automatic variables, constructors and destructors give the programmer a powerful mechanism for guaranteeing the initialization and de-initialization of resources. Since constructors and destructors do not exist in C, C programmers have to be careful with resource cleanup. Cleanup is often difficult to manage in the middle of a busy project. In this article, I propose a simple framework that offers C programmers a general approach to the cleanup problem.

The Framework

The framework is simply based on the concept of a cleanup stack. A cleanup stack is a stack whose elements are function addresses with optional parameters. In the implementation of a function, when you allocate a resource, you can push it to the stack together with the address of a cleanup function. Later, when the function leaves, the stack is automatically popped and the cleanup function deallocates the resource.

Structures

An element of a cleanup stack is a pair consisting of a function address and an optional pointer, which is usually the resource to clean up. This struct definition is then typedefed to get rid of the struct keyword, which gives the new type tCleanupElm:

struct sCleanupElm
{
  void (*clean)(void*);
  void* ptr;
};

A cleanup stack consists of a constant integer that is the stack’s capacity (max), an array of the previously defined elements (tCleanupElm), and an index that is the position of the current free element in the stack (index). This struct definition is also typedefed for convenience (tCleanupStack):

struct sCleanupStack
{
  const unsigned int max;
  tCleanupElm* stack;
  unsigned int index;
};

For simplicity, I define the stack as basic arrays. The programmer fixes the maximum number of entries at the start of each function that needs the stack.

tCleanupElm stack[n];
tCleanupStack cleanup_stack = {n, stack, 0};

Functions

To manage a stack, you at least need the push and pop functions:

void _cl_push(tCleanupStack* aStack,
  void* aPtr, void (*aClean)(void*));

void _cl_pop(tCleanupStack* aStack,
  unsigned int n);

push adds an element (resource plus cleanup function) to the stack. (In other words, aStack->stack[aStack->index]->ptr is set to the resource’s address, aStack->stack[aStack->index]->clean is set to the cleanup function’s address, and the stack index, aStack->index, is incremented.)

pop removes n elements from the stack. Removing an element from the stack means executing the cleanup function on its associated resource and decrementing the stack’s array index. For convenience, I also added a popa function that pops all the elements from the stack.

All the ingredients are now available to finalize the framework. To simplify using stack functions, I wrapped them in macros.

Macros

INITCL defines a cleanup stack with a maximum of n elements for the current function:

#define INITCL(n) \
tCleanupElm _cl_current_stack[(n)]; \
tCleanupStack _cl_cleanup_stack = \
{ (n), _cl_current_stack, 0}; \
if (0) goto _cl_l_cleanup

The first three lines of this macro declare the stack and the structure that contains its related data. The last line is only there to raise a compile-time error if the user forgets to use the DONECL() macro at the end of the function that was started by INITCL.

#define DONECL() \
_cl_l_cleanup: \
_cl_popa(&_cl_cleanup_stack)

If the user forgets to use DONECL, the compiler will not find the _cl_l_cleanup label, which will stop the compilation. This trick is necessary to guarantee the call to the _cl_popa function at the end of the function that uses the stack. The purpose of DONECL is thus to pop all the elements of the stack when the function reaches its end. But this is not enough; the function can exit anywhere thanks to the keyword return. return therefore needs special treatment:

#define return \
{_cl_popa(&_cl_cleanup_stack);} \
return

This new return guarantees that the stack will be called prior to leaving the function. From its definition, you see that the new return replaces the old one and does not require any adaptation to the code that will use it.

Actually, this solution is fine for functions that use INITCL because in this case the variable _cl_cleanup_stack exists and the compiler is happy. But for the other functions that don’t use INITCL, the compilation fails unless a global (dummy) stack _cl_cleanup_stack exists in the source file. To solve this minor issue, you can declare a dummy stack in the header file cleanup.h (the header file that defines all the structures, functions, and macros that make up the framework).

I advise including cleanup.h only in source files (not header files) and preferably at the end of the #include section to avoid replacement of return keywords in inline functions or macros that are defined in the different header files.

Example

You are now ready to use the cleanup framework. Consider the following example:

void f()
{
  INITCL(2);
  {
    int *pi,*pj;
    pi=malloc(1000);
    if (!pi)
      return FAILED;
    PUSHCL(pi, free);
    pj=malloc(1000);
    if (!pj)
      return FAILED;
    PUSHCL(pj, free);
    // use pi and pj
    return OK;
  }
  DONECL();
}

In this example, the function f uses a cleanup stack that is declared to contain up to two elements. Once pi is correctly allocated, it is pushed to the stack together with its cleanup function, which is the standard function free. The same scenario is repeated for pj. If pj cannot be allocated, the function returns FAILED after having popped pi from the stack, which helps prevent a memory leak. But if it succeeds, the function continues and uses pi and pj. At the end, the function can return directly, since the stack takes care of freeing pj and pi.

Note: since this framework is generic, you can use a stack for different purposes. Instead of malloc, you could have a function that acquires a semaphore, and the cleanup function would release it. Or you could just push a function you want to execute to the end of the function (e.g., a debug trace function).

Debugging

This approach works great until your program becomes complex and bugs appear. For example, suppose you declare a too small stack for function g. During the execution of your program, the stack overflow has to be detected. The basic way to detect stack overflow is to use assertion to check array indexes. Thus, when your program crashes, you can use a debugger to back trace to the function that did the lethal push operation: g. You can then fix the problem by extending the stack capacity.

Unfortunately, a debugger is not always available, especially in embedded systems. In such environments, crashes due to stack errors (overflow or underflow) are difficult to locate. To ease debugging in these situations, I added debug information to the stack structures and functions. The macros and the functions are declared and implemented with additional parameters that are grouped in preprocessor variables.

Here is the definition of the macro PUSHCL:

#define PUSHCL(ptr, clean) \
_cl_push(&_cl_cleanup_stack, \
(ptr), (clean) \
DEBUGCL_EXTRA_INFO)

When the framework is not configured in debug mode, the preprocessor variable DEBUGCL_EXTRA_INFO is empty; otherwise, it is defined like this:

#define DEBUGCL_EXTRA_INFO \
, __FILE__, __LINE__

Thus, each use of stack macros by the user is logged into the stack itself. Of course, in order to do so, the function _cl_push has to be updated to store this extra information to the stack. Actually, in debug mode, data structures sCleanupElm and sCleanupStack depend on another preprocessor variable that is equal to:

#define DEBUGCL_EXTRA_STRUCT \
const char* filename; \
int lineno;

The same work is done for all the other stack operations. Thus, in case of stack overflow or underflow, you can stop the program after it has displayed:

In the complete implementation (available for download at <www.cuj.com/code>), all this debug information is available once you define the macro DEBUGCL_PRINT to be your favorite printf-like function.

Stephane Grad has a Bachelor of Science in Mathematics and has been working as a software developer for two-and-a-half years. His computer-related interests are C++ programming, 3-D algorithms, and Linux.