Search code examples
cmacrosfunction-pointersalloca

Implementing the defer keyword in C


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__ cleanup syntax 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 alloca calls at each DEFER. This does work, but is slow (up to 2x slower than manually ordering the function calls or my current defer solution).

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.


Solution

  • has about a 10% performance overhead.

    Most likely this arises because of calling cleanup functions indirectly. The compiler cannot optimize indirect calls as well as it can direct ones.

    it is limited in the number of DEFER macro calls that can be handled.

    Yes and no. Your implementation has a limit to the number of DEFER calls per PRE_DEFER, but you could put multiple PRE_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, each PRE_DEFER requires its own POST_DEFER, too.

    Is there a way to programmatically populate the size of the defers array (as a VLA) in the generated macro code?

    Not ahead of the the appearance of the DEFER calls, no.

    You could consider something more like this:

    #define DEFER(cleanup) for (_Bool done_ = 0; !done_; (cleanup), done_ = 1)
    

    That defers evaluation of the expression given by the argument to DEFER until 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 / __finally example, but the syntax is a bit less fraught:

    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));
            }
        }
    }
    

    I'm not entirely sure what you mean by ...

    An ideal solution would reconcile all of these versions (vanilla C and various compiler extensions) into a single syntax for cross-platform sake.

    ... but the above uses only standard C99 features. It will work with any C99 or later implementation.


    Honorable mention: pthread_cleanup_push() and pthread_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.