Golang has a useful language construct called defer which allows the user to postpone a function's execution until the surrounding function returns. This is useful for ensuring that resources are safely destroyed while keeping the creation-destruction logic close together.
I am interested in implementing this through macros in C99. The following is what I have written so far, along with some example code to show the macro in action:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <math.h>
void* log_malloc(size_t size) {
void* ptr = malloc(size);
printf("Allocated %zu bytes: %p\n", size, ptr);
return ptr;
}
void log_free(void* ptr) {
free(ptr);
printf("Freed %p\n", ptr);
}
typedef void (*DeferFunc)();
typedef struct Defer {
DeferFunc f;
void* p;
} Defer;
#define PRE_DEFER() \
size_t num_defers = 0; \
Defer defers[32];
#define DEFER(func, param) \
defers[num_defers].f = func;\
defers[num_defers].p = param;\
num_defers++;
#define POST_DEFER() \
while(num_defers > 0) { \
defers[num_defers - 1].f(defers[num_defers - 1].p); \
num_defers--; \
}
void test() {
PRE_DEFER();
char* a = log_malloc(10);
DEFER(log_free, a);
char* b = log_malloc(20);
DEFER(log_free, b);
char* c = log_malloc(30);
DEFER(log_free, c);
POST_DEFER();
}
int main() {
test();
return 0;
}
This code works and, in my basic benchmarking compared to just manually ordering the destructor statements, has about a 10% performance overhead. However, it is limited in the number of DEFER macro calls that can be handled. I can of course pass in the number of defers I expect to perform in the PRE_DEFER macro, but this can be tedious and error-prone to count defer calls, especially if there is branching logic or loops containing DEFER statements. Is there a way to programmatically populate the size of the defers array (as a VLA) in the generated macro code?
Solutions I have already considered or am not interested in:
- Just using C++. This could be addressed with RAII. However, the destructor logic becomes implicit, and I am simply not interested in the additional complexity/feature-creep that C++ offers.
- Using compiler extensions. GCC (and clang I believe) have an
__attribute__ cleanupsyntax that can be abused that performs well but is not supported in MSVC and requires modifying the supplied destructor to use a pointer to the variable rather than the variable itself. - Using repeated
allocacalls at eachDEFER. This does work, but is slow (up to 2x slower than manually ordering the function calls or my currentdefersolution).
Any help would be much appreciated.
Update:
Thanks to @Dai for pointing out MSVC's __try and __finally compiler constructs. Using them, I get the following code:
#define DEFER(fini_call, body) \
__try{ body } \
__finally{ fini_call; }
void test() {
char* a = log_malloc(10);
DEFER(log_free(a), {
char* b = log_malloc(20);
DEFER(log_free(b), {
char* c = log_malloc(30);
DEFER(log_free(c), {})
})
});
}
This gets the job done and saves the need for PRE_DEFER and POST_DEFER macros. However, it is not compatible with my original macro's syntax. Also, it leads to nesting that was hidden in my original version, as the remainder of the function needs to be passed into the DEFER macro.
For the sake of completeness, I am including the gcc/clang attribute((cleanup)) version of the code. It is as follows:
#define DEFER(type, name, init_func, fini_func_name) \
type name __attribute__ ((__cleanup__(fini_func_name))) = init_func
void* log_malloc(size_t size) {
void* ptr = malloc(size);
printf("Allocated %zu bytes: %p\n", size, ptr);
return ptr;
}
void log_free(char** ptr) {
free(*ptr);
printf("Freed %p\n", *ptr);
}
int test() {
DEFER(char*, a, log_malloc(10), log_free);
DEFER(char*, b, log_malloc(20), log_free);
DEFER(char*, c, log_malloc(30), log_free);
return 0;
}
This of course requires yet another syntax for the macro that is incompatible with both the original version and the MSVC extension version. It also requires an awkward syntax that wraps and splits up the creation function call.
An ideal solution would reconcile all of these versions (vanilla C and various compiler extensions) into a single syntax for cross-platform sake.
Most likely this arises because of calling cleanup functions indirectly. The compiler cannot optimize indirect calls as well as it can direct ones.
Yes and no. Your implementation has a limit to the number of
DEFERcalls perPRE_DEFER, but you could put multiplePRE_DEFERs in the same function by nesting them within blocks. Or by providing a distinguishing label for each one, which you then use to form the names of the variables containing the wanted information. Of course, eachPRE_DEFERrequires its ownPOST_DEFER, too.Not ahead of the the appearance of the
DEFERcalls, no.You could consider something more like this:
That defers evaluation of the expression given by the argument to
DEFERuntil after completion of the next statement, which can be, but does not need to be, a compound one. You would use it similarly to your__try/__finallyexample, but the syntax is a bit less fraught:I'm not entirely sure what you mean by ...
... but the above uses only standard C99 features. It will work with any C99 or later implementation.
Honorable mention:
pthread_cleanup_push()andpthread_cleanup_pop(). These are not only standard C, but standardized themselves -- but by POSIX, not by the C language. They will work in conjunction with a pthreads implementation, but not more broadly.