🔄 Overview
This module implements Go/Zig-style defer statements for C using Clang libTooling source-to-source transformation. Defer allows cleanup code to run automatically at function exit, regardless of the exit path (return, goto, end of function).
Key Design Decision: No runtime library is needed. The transformer directly inlines cleanup code at every exit point, making defer zero-overhead at runtime.
Implementation: src/tooling/defer/tool.cpp
Key Features:
- Automatic cleanup at function exit
- LIFO (last-in-first-out) execution order
- Zero runtime overhead (cleanup inlined at compile time)
- Automatic detection and transformation of files using
defer()
- CMake integration via
cmake/tooling/Defer.cmake
Architecture
Source-to-Source Transformation (No Runtime)
The defer tool transforms source code by directly inlining cleanup code at every function exit point. No runtime library is needed.
Original Code:
void process_file(const char *path) {
FILE *f = fopen(path, "r");
defer(fclose(f));
if (!f) return;
return;
}
Transformed Code:
void process_file(const char *path) {
FILE *f = fopen(path, "r");
if (!f) {
{ fclose(f); }
return;
}
{ fclose(f); }
return;
}
Transformation Rules:
- Defer detection: Match
defer(expression); statements
- Exit point injection: Insert cleanup block
{ expression; } before:
- Every
return statement
- End of function (implicit return)
- Before
goto targets outside the defer scope
- LIFO order: Multiple defers execute in reverse order (last-in-first-out)
- Remove defer statement: The original
defer() call is removed
Header Stub (lib/tooling/defer/defer.h)
The header provides a no-op macro so IDEs and editors can parse code that uses defer():
#define defer(action) \
do { } while (0)
This allows:
- Syntax highlighting to work correctly
- IDE code completion and error checking
- Code to compile (as a no-op) without transformation for quick testing
Build Integration (cmake/tooling/Defer.cmake)
Defer transformation is enabled automatically when files contain defer() calls.
# Defer is automatically enabled when defer() is detected in source files
# No CMake option needed - just build normally
cmake -B build
cmake --build build
How it works:
- CMake scans source files for
defer( usage
- Only files containing
defer() are transformed
- Transformed files are written to
build/defer_transformed/
- Build uses transformed sources instead of originals
Key features:
- Incremental builds: Only re-transforms files that changed
- Cached tool: The defer tool binary is cached in
.deps-cache/defer-tool/
- No runtime library: Cleanup code is inlined directly
Usage Examples
Basic Cleanup
void example() {
char *buffer = malloc(1024);
defer(free(buffer));
}
Multiple Resources (LIFO order)
void multi_resource() {
FILE *f1 = fopen("file1.txt", "r");
defer(fclose(f1));
FILE *f2 = fopen("file2.txt", "w");
defer(fclose(f2));
}
Lock/Unlock Pattern
void critical_section(mutex_t *mtx) {
mutex_lock(mtx);
defer(mutex_unlock(mtx));
}
Error Handling
asciichat_error_t process_data(void) {
int fd = open("data.bin", O_RDONLY);
if (fd < 0) {
return SET_ERRNO_SYS(ERROR_CONFIG, "Failed to open file");
}
defer(close(fd));
uint8_t *buffer = SAFE_MALLOC(4096, uint8_t*);
defer(free(buffer));
if (read(fd, buffer, 4096) < 0) {
return SET_ERRNO_SYS(ERROR_OPERATION_FAILED, "Read failed");
}
return ASCIICHAT_OK;
}
Implementation Notes
Supported Expressions
The defer tool supports any single expression:
- Simple function calls:
defer(fclose(file));
- Expressions with side effects:
defer(printf("cleanup\n"));
- Multiple arguments:
defer(cleanup_resource(res, flags));
- Compound expressions:
defer((void)(count++, cleanup()));
Scope Handling
Defer applies to function scope, not block scope:
void example() {
{
FILE *f = fopen("file.txt", "r");
defer(fclose(f));
}
}
Performance
Since cleanup code is inlined:
- Zero runtime overhead - No function call indirection
- No memory allocation - No scope tracking structures
- Compiler optimizations - Cleanup code can be optimized with surrounding code
- Slight code size increase - Cleanup duplicated at each exit point
Comparison to Other Approaches
vs Manual Cleanup
Defer Advantages:
- Cleanup code near allocation site (better readability)
- Automatic handling of all exit paths (no missed cleanup)
- LIFO order matches allocation order naturally
Manual Disadvantages:
- Easy to miss cleanup paths (early returns, errors)
- Cleanup far from allocation (hard to track)
- Error-prone with complex control flow
vs Cleanup Attributes (GCC/Clang)
void example() {
}
__attribute__((constructor))
Register fork handlers for common module.
Defer Advantages:
- More flexible (can defer any expression, not just declarations)
- Explicit control over cleanup order
- Works with any compiler after transformation
- No need to write separate cleanup functions
Attribute Disadvantages:
- Compiler-specific (non-portable without transformation)
- Tied to variable declarations only
- Requires separate cleanup function for each type
Build System Details
File Locations
- Tool source:
src/tooling/defer/tool.cpp
- Tool CMake:
src/tooling/defer/CMakeLists.txt
- Integration:
cmake/tooling/Defer.cmake
- Header stub:
lib/tooling/defer/defer.h
- Cached binary:
.deps-cache/defer-tool/ascii-instr-defer
- Transformed output:
build/defer_transformed/
Troubleshooting
Defer not working:
- Check that the file contains
defer( (CMake only transforms files with defer usage)
- Verify build completed:
cmake --build build
- Check transformed output:
cat build/defer_transformed/path/to/file.c
Tool not building:
- Requires LLVM/Clang development libraries
- Check:
ls .deps-cache/defer-tool/ascii-instr-defer*
- Force rebuild:
rm -rf .deps-cache/defer-tool && cmake --build build
IDE shows errors:
- Include
lib/tooling/defer/defer.h to provide the macro stub
- The header makes
defer() a valid no-op for parsing
References