Defer Macro
Warning: This is experimental, relies on GCC-specific extensions (__attribute__((cleanup)) and nested functions), and is not portable C. It’s just for fun and exploration.
While working on my operating system project, RetrOS-32, I ran into some of GCC’s less common
Here’s an example: While working on my operating system project, RetrOS-32, I ran into some of GCC’s less common function attributes . One that stood out to me was the cleanup attribute, which lets you tie a function to a variable. When the variable goes out of scope, that function is called automatically. At first this sounded like an easy way to manage resources in C, a language where you normally have to handle everything by hand.Here’s an example:
/* The function must take one parameter, * a pointer to a type compatible with the variable. * The return value of the function (if any) is ignored. */ void free_ptr(void* ptr) { free(*(void**)ptr); } int example(){ __attribute(cleanup(free_ptr)) char* ptr = malloc(32); return 0; }
That changed when I came across a This works, but it isn’t exactly safe. If malloc fails and returns NULL, the cleanup function will still be called, and there’s no simple way to add a guard inside free_ptr. Because of issues like that, I didn’t really use the feature at first.That changed when I came across a blog post by Jens Gustedt , where he showed how the cleanup attribute could be combined with another GCC feature, nested functions , to build something more practical: a defer mechanism, similar to what languages like Go provide.
Nested functions
Nested functions are not part of standard C, but GCC supports them. They allow you to declare a function inside another function, and the inner one has access to the variables of the outer scope.
int example() { int a = 1; void set(){ a = 2; } set(); return a; }
This ability to reach into the parent scope is what makes a defer macro possible. Jens’ approach uses cleanup to ensure the nested function is executed when leaving the scope, giving you a way to run code automatically at the end of a block.
Jens' defer macro is defined below:
#define __DEFER__(F, V) \ auto void F(int*); \ __attribute__((cleanup(F))) int V; \ void F(int*) #define __DEFER_(N) __DEFER__(__DEFER_FUNCTION_ ## N, __DEFER_VARIABLE_ ## N) #define __DEFER(N) __DEFER_(N) #define defer __DEFER(__COUNTER__)x
Going back to our previous example we could instead write:
int example() { char* ptr = malloc(32); defer { free(ptr); } return 0; }
To see how this worked in practice, I put the code into
To see how this worked in practice, I put the code into Godbolt and checked the generated assembly. The compiler creates a separate function for the defer block and sets up the stack frame for it:
# AT&T syntax __DEFER_FUNCTION_0.0: pushq %rbp movq %rsp, %rbp subq $16, %rsp movq %rdi, -8(%rbp) movq %r10, %rax movq %r10, -16(%rbp) movq (%rax), %rax movq %rax, %rdi call free nop leave ret example: pushq %rbp movq %rsp, %rbp pushq %rbx subq $40, %rsp leaq 16(%rbp), %rax movq %rax, -40(%rbp) movl $32, %edi call malloc movq %rax, -48(%rbp) # <--- return value set movl $0, %ebx leaq -20(%rbp), %rax leaq -48(%rbp), %rdx movq %rdx, %r10 movq %rax, %rdi call __DEFER_FUNCTION_0.0 movl %ebx, %eax # <--- return value restored movq -8(%rbp), %rbx leave ret
The key detail here is that the return value is set before the call to the defer function. That means even if your deferred block frees memory, the return value will already be saved. This is important, because it guarantees that early returns won’t break cleanup logic.
The downside is clear too: the setup is heavy. The pointer to ptr has to be explicitly passed to the defer function, and a full stack frame is built. That slows things down and clutters the generated code.
always_inline
To reduce the overhead, I tried forcing the nested cleanup function to always inline:
#define __DEFER__(F, V) \ auto inline __attribute__((always_inline)) void F(int*); \ __attribute__((cleanup(F))) int V; \ inline __attribute__((always_inline)) void F(int*) #define __DEFER_(N) __DEFER__(__DEFER_FUNCTION_ ## N, __DEFER_VARIABLE_ ## N) #define __DEFER(N) __DEFER_(N) #define defer __DEFER(__COUNTER__)
This removes the separate function call. Instead of generating a new stack frame, the compiler drops the defer code directly into the function at the right points.
Here’s a slightly bigger example where this becomes useful:
#include struct object { int value; }; int example(int val) { struct object* obj = malloc(sizeof(struct object)); if(obj == NULL) return -1; defer { free(obj); } obj->value = val; if(obj->value == 8){ return obj->value; } obj->value = 10; return obj->value; }
The assembly for this version is a lot cleaner:
# AT&T syntax example: pushq %rbp movq %rsp, %rbp pushq %rbx subq $56, %rsp movl %edi, -52(%rbp) leaq 16(%rbp), %rax movq %rax, -40(%rbp) movl $4, %edi call malloc movq %rax, -48(%rbp) movq -48(%rbp), %rax testq %rax, %rax jne .L2 movl $-1, %ebx jmp .L6 # <--- instant jump to return, no defer. .L2: movq -48(%rbp), %rax movl -52(%rbp), %edx movl %edx, (%rax) movq -48(%rbp), %rax movl (%rax), %eax cmpl $8, %eax jne .L4 movq -48(%rbp), %rax movl (%rax), %ebx jmp .L5 .L4: movq -48(%rbp), %rax movl $10, (%rax) movq -48(%rbp), %rax movl (%rax), %ebx .L5: leaq -28(%rbp), %rax movq %rax, -24(%rbp) movq -48(%rbp), %rax movq %rax, %rdi call free # <--- Defer'd free called before return nop .L6: movl %ebx, %eax movq -8(%rbp), %rbx leave ret
There are a few interesting points in this output:
The malloc failure check jumps straight to .L6, skipping defer entirely. This makes sense, since the defer was defined after the allocation.
Early returns still trigger the cleanup. At .L2 you can see the code checking for obj->value == 8. Instead of jumping out immediately, it goes through .L5, where the free call happens.
The return value is always stored before defer runs. Even when freeing the object, the function returns the right value without accessing freed memory.
If you tried to write this manually, you’d end up with temporary variables and gotos to ensure cleanup runs, which quickly gets messy. The defer macro keeps it all in one place.
How the code could look with gotos and temporary variables:
int example(int val) { struct object* obj = malloc(sizeof(struct object)); if (obj == NULL) return -1; obj->value = val; if (obj->value == 8) { int result = obj->value; goto cleanup; // jump to cleanup before returning } obj->value = 10; int result = obj->value; goto cleanup; cleanup: free(obj); return result; }