
[Defer Macro] “Warning: This is experimental, relies on GCC-specific extensions (__attribute__((cleanup)) and nested functions), and is not portabl...
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.
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 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 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;
}# 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#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__)Here’s a slightly bigger example where this becomes useful:
#include <stdlib.h>
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:
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;
}Full example on Godbolt: https://godbolt.org/z/7xn5qqdT7
Testing with Jen's macro this was based on, and found that the always_inline was redundant under even -O1 (https://godbolt.org/z/qoh861Gch via the examples from N3488 as became the baseline for the TS for C2y, which has recently a new revision under N3687), so there's an interesting trade-off between visibly seeing the `defer` by not not-inlining within the macro under an -O0 or similar unoptimised build, since with the inlining they are unmarked in the disassembly. But, there's an interesting twist here, as "defer: the feature" is likely not going to be implemented as "defer: the macro", since compilers will have the keyword (just `defer` in TS25755, or something else that uses a header for sugared `defer`) and may see the obvious optimised rewrite as the straightforward way of implementing it in the first place (as some already have), meaning we can have the benefit of the optimised inline with the opportunity to also keep it clearly identifiable, even in unoptimised and debug builds, which would certainly be nice to have!
Then there is the proposal to add standard `defer` to C2y[0]
[0] https://thephd.dev/c2y-the-defer-technical-specification-its...
It's been implemented in GCC(in review)[1], onramp[2] and my own slimcc[3]!
[1] https://patchwork.ozlabs.org/project/gcc/list/?series=470822
Jen's macro that this was based on was an implementation of his own proposal (N3434) for `defer`, which was one of a few preceding what finally became TS25755! So, yes, C2y is lined up to have "defer: the feature", but until then, we can explore "defer: the macro" (at least on GCC builds, as formulated).
Slightly off-topic, but:
The fact that go "lifts" the deferred statement out of the block is just another reason in the long list of reasons that go shouldn't exist.
Not only is there no protection against data-races (in a language all about multithreading), basically no static checking for safety, allocation and initialization is easy to mess up, but also defer just doesn't work as it does in C++, Rust, Zig, and any other language that implements similar semantics.
What a joke.
One of those languages being D, which invented it (well, Andrei Alexandrescu did) under the name `scope(exit)` (there's also `scope(failure)` which is like Zig's `errdefer` and `scope(success)` which no other language has AFAIK).