Ensuring resources are cleaned up in C can be a challenge. Alison Chaiken shows how cleanup macros can help.
Perhaps the trickiest part of writing correct C code is resource management, particularly in error handlers. A quick scan of Common Vulnerabilities and Exceptions (CVE) lists finds many double-frees and use-after-frees, and bugtrackers are full of lock-contention problems and memory-leak failures caused by sloppy error-handling. If only the C language had scoped locks and destructors!
ISO C still does not support these features, but there’s good news: the GCC and clang compiler front-ends do. GCC implemented the __cleanup__ compiler attribute in 2003 [gcc], and clang added the feature in 2009 [clang]. Several prominent code bases written in C make extensive use of the __cleanup__ attribute, notably systemd, Linux kernel and glibc, but many large, prominent C-language projects (git, gstreamer, U-Boot for example) do not. The purpose of this article is to encourage more C programmers to take advantage of the __cleanup__ functionality and remove some of the notorious goto labels.
How does __cleanup__ work and how does one use it? As described in the GCC [gcc-1] and clang documentation [clang-1], a cleanup function is bound to a local variable at the time of its declaration, and runs automatically when the variable goes out of scope, not unlike a C++ destructor. The clang documentation illustrates the point as shown below:
static void foo (int *) {... }
static void bar (int *) {... }
void baz (void) {
int x __attribute__((cleanup(foo)));
{
int y __attribute__((cleanup(bar)));
}
}
Here bar() will run when the first closing brace is reached and foo() upon reaching the second. Note that the compiler’s generated code passes the cleanup function a pointer to the variable to which it is bound. The cleanup function need not actually make use of the pointer. The Linux init system systemd’s code offers many real-world examples like that below [systemd]:
static inline void freep(void *p) {
free(*(void **) p);
}
#define _cleanup_free_ _cleanup_(freep)
The cast inside the cleanup function is necessary since, as mentioned above, the cleanup function receives a pointer to the variable to which it is bound. In the common use case below, freep() is invoked with a char** parameter [systemd-1]:
_cleanup_free_ char *path = strdup(e + 1);
As of this writing, systemd’s code base has 5852 call-sites for _cleanup_free_, compared to 2633 for goto. Although C has not gained new features with the speed of C++, it is not in fact entirely static! Other systemd cleanup functions decrement reference counters, close file descriptors and unlock locks, in keeping with expectations from C++. Systemd also defines destructors for static variables [systemd-2].
An obvious question is, suppose the function where a cleanup attribute is declared succeeds, and the release of the resource is therefore undesirable? The Linux kernel simply checks if the pointer parameter is NULL before freeing it. Listing 1 shows an excerpt from a kernel interrupt-chip driver in which the sole purpose of the variable dev is to signal the runtime whether or not a reference to the device should be released [linux].
void put_device(struct device *dev);
DEFINE_FREE(put_device, struct device *,
if (_T) put_device(_T))
static int rzg2l_irqc_common_init(... ) {
struct device *dev __free(put_device)
= pdev ? &pdev->dev : NULL;
[...]
/ * On the successful path we don't actually
want to "put" dev. */
dev = NULL;
}
|
| Listing 1 |
Suppose that setting the variable with the cleanup handler to NULL is not readily possible? The kernel project has a no_free_ptr() macro for that case. In the following example from a GNSS device driver [linux-1], the ubx_probe() function upon success passes the pointer to which the cleanup handler is bound to another function. The no_free_ptr() wrapper in Listing 2 insures that the pointer is not freed.
DEFINE_FREE(free_serial, struct gnss_serial *,
if (_T) gnss_serial_free(_T))
static int ubx_probe(struct serdev_device
*serdev)
{
struct gnss_serial *gserial __free(free_serial)
= gnss_serial_allocate(serdev,
sizeof(*data));
[ ...> ]
ret =
gnss_serial_register(no_free_ptr(gserial));
}
|
| Listing 2 |
The Linux macros which define no_free_ptr() and its relatives are nested and a bit difficult to understand. The best course of action for decoding them is to examine the compiler’s preprocessor output. Userspace preprocessor output is accessible via the -E compiler flag. For the kernel, set KCFLAGS and KBUILD_CFLAGS to --save-temps. The preprocessed no_free_ptr() line above becomes:
ret = gnss_serial_register(((typeof(gserial))
__must_check_fn(({ __auto_type __ptr =
&(gserial); __auto_type __val = *__ptr; *__ptr
= ((void *)0); __val; }))));
where __auto_type is a compiler extension that is essentially equivalent to C++’s auto keyword rather than C’s [gcc-2]. The actual no_free_ptr() function is delimited by curly braces in a way which may remind C++ readers of lambda functions. The construct is a statement expression, about which GCC notes [gcc-3]:
A compound statement enclosed in parentheses may appear as an expression in GNU C. This allows you to use loops, switches, and local variables within an expression.... Recall that a compound statement is a sequence of statements surrounded by braces... The last thing in the compound statement should be an expression followed by a semicolon; the value of this subexpression serves as the value of the entire construct.
Accordingly, the code makes a copy of the pointer which should not be freed, sets the original pointer to NULL, and leaves the copied pointer for the enclosing function to evaluate.
The kernel further provides a DEFINE_GUARD macro and guard() pseudofunction in analogy to DEFINE_FREE and __free() above. The guard macros support automatic unlocking, including _trylock forms. The nested macros underlying DEFINE_GUARD even include a DEFINE_CLASS [linux-2].
Nonetheless, it is the Glibc system library which contains the apotheosis of C cleanup methods in its pthread implementation. There we find the split-macro pair pthread_cleanup_push(ROUTINE, ARG) and pthread_cleanup_pop() [glibc]. As the names suggest, they are used to manage a stack of cleanup handlers which run in order when a thread is canceled, or hard-exits, possibly by throwing an exception. As shown below in Listing 3, the push-pop pair of macros must bracket the cleanup code since push() starts with do { and pop() ends with } while (0).
# define pthread_cleanup_push(routine, arg) \
do { \
struct __pthread_cleanup_frame __clframe \
__attribute__ ((__cleanup__ \
(__pthread_cleanup_routine))) \
= {.__cancel_routine = (routine), \
.__cancel_arg = (arg), \
.__do_it = 1 };
/* Remove a cleanup handler installed by the matching pthread_cleanup_push. If EXECUTE is non-zero, the handler function is called. */
# define pthread_cleanup_pop(execute) \
__clframe.__do_it = (execute); \
} while (0)
|
| Listing 3 |
pthread_cleanup_push() and pthread_cleanup_pop() are best understood by reading their POSIX manual pages [manpages].
One point of caution concerns functions which contain multiple cleanup handlers. If a particular lock must be held when a particular cleanup action executes, then the declarations of the variables should be ordered accordingly. The implication is (gasp!) that some variable declarations must appear in the middle of the function body at first point of use, just as in C++.
Given the widespread acknowledgement that resource management in C error handlers without destructors is difficult, why isn’t the adoption of cleanup handlers universal? For marquee C-language projects like Gstreamer and git, the answer is that they must compile for Windows. MSVC does not support __cleanup__, but favors its own __try/__finally variants [meneide]. The ISO C Committee is in the process of drafting a technical specification for a new defer keyword in the hope that all major toolchains will support it. defer would presumably be based on the cleanup attribute where it is supported. The draft goes into some detail in an attempt to constrain behavior and discourage anti-patterns. Whether the Committee will adopt the specification or toolchain vendors will implement it is, as always, an open question.
In the meantime, developers and maintainers of complex C code bases are encouraged to give cleanup macros a try. The three projects highlighted here are replete with helpful examples. Removing the last goto statement from a long function is a satisfying feeling, plus it’s much less work than reimplementing the whole project in Rust.
References
[clang] https://github.com/llvm/llvm-project/commit/d277d790e0f6f23043397ba919619b5c3e157ff3
[clang-1] https://clang.llvm.org/docs/AttributeReference.html#cleanup
[defer-TS] https://thephd.dev/_vendor/future_cxx/papers/C%20-%20Improved%20__attribute__((cleanup))%20Through%20defer.html
[gcc] https://github.com/gcc-mirror/gcc/blob/58735cd75df57ca423dd6189e5f79f4d4686dfa2/gcc/ChangeLog-2003#L20790
[gcc-1] https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html#index-cleanup-variable-attribute
[gcc-2] https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Auto-Type.html
[gcc-3] https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html
[glibc] https://github.com/bminor/glibc/blob/c9188d333717d3ceb7e3020011651f424f749f93/sysdeps/nptl/pthread.h#L585
[linux] https://github.com/linux4microchip/linux/blob/4d72aeabedfa202d12869e52c40eeabc5401c839/drivers/irqchip/irq-renesas-rzg2l.c#L596
[linux-1] https://github.com/chaiken/linux4microchip/blob/34307c7489f685d16a5a64f89699b4dea2e48fca/drivers/gnss/ubx.c#L1180
[linux-2] https://github.com/torvalds/linux/blob/39d3389331abd712461f50249722f7ed9d815068/include/linux/cleanup.h#L44
[manpages] https://www.man7.org/linux/man-pages/man3/pthread_cleanup_push.3.html
[meneide] https://thephd.dev/c2y-the-defer-technical-specification-its-time-go-go-go
[systemd] https://github.com/systemd/systemd/blob/51190631968f2a69acf5da3e3412b003805538f2/src/boot/util.h#L18
[systemd-1] https://github.com/systemd/systemd/blob/51190631968f2a69acf5da3e3412b003805538f2/src/basic/cgroup-util.c#L564
[systemd-2] https://github.com/systemd/systemd/blob/3e2a5dc2e17aae334d312d63e95159aff9ecaae5/src/basic/static-destruct.h#L9
is a systems programmer and Linux kernel engineer who has recently relocated from Silicon Valley to Berlin, where she would be delighted to meet over a beer or a coffee to discuss C++/C or compilers.









