DEV Community

Joshua Matthews
Joshua Matthews

Posted on

1 1 1 1 1

Automated Cleanup in C

Table of Contents:

Intro

House-keeping in C can be tedious with manually freeing memory and providing cleanup code at every logical branch of your functions. However, compilers have added attributes that you can take advantage of to help with memory cleanup nowadays. I will step through and show case some of the powerful ways to use __attribute__((cleanup(<function>))) within your code.

Documentation

This feature is supported by Clang and GCC and their documentation provide small descriptions on how the feature is meant to be used. The quick overview is that this attribute allows you to register a callback function to a variable definition and gets called when the variable leaves the scope it was defined in. The basic structure is as follows:

static void free_char(char **p) {
    if (p == NULL) return;
    if (*p == NULL) return;
    free(*p);
    *p = NULL;
}

int main(void) {
    __attribute__((cleanup(free_char))) char *test = malloc((sizeof(char)*14));
    strncpy(test, "Hello, World!", 13);
    test[14] = '\0';
    printf("test string = \"%s\"\n", test);
    // free_char is called when this function exits
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

We start with defining our cleanup function free_char. It accepts a char ** because the cleanup function passes the local variable as a pointer to the function. Since our variable is a pointer, it gets passed in as a double pointer.

We then attach the attribute to the variable we want the cleanup function to happen to. This attribute can be in a few places and it will work.
All of these definitions are valid:

  • __attribute__((cleanup(free_char))) char *test = malloc(/* size */);
  • char __attribute__((cleanup(free_char))) *test = malloc(/* size */);
  • char *test __attribute__((cleanup(free_char))) = malloc(/* size */);

When defining your cleanup function you could technically get away with something like:

static void generic_cleanup(void **p);
Enter fullscreen mode Exit fullscreen mode

But if you use a proper linter it'll throw warnings that the types don't match, so it's better to use explicit cleanup functions for your types.

Usage

With a basic overview of the feature understood, we'll cover a few more use cases.

Errors and Branching

The immediate use-case is clean up of allocated variables. This can be very advantageous for functions with lots of error handling or branching logic with multiple return statements.

static void free_char(char **p) {
    if (p == NULL) return;
    if (*p == NULL) return;
    free(*p);
    *p = NULL;
}

bool http_message_set_header(struct http_message_t *msg, const char *key,
                             char *value) {
  size_t key_len = strlen(key);
  __attribute__((cleanup(free_char))) char *normalized_key = normalized_cstr(key, key_len);
  // free previous value.
  char *out = NULL;
  http_message_get_header(msg, key, &out);
  if (out != NULL) {
    free(out);
  }
  // set new value, we copy the value to have ownership
  if (!hash_map_set(msg->headers, normalized_key,
                    str_dup(value, strlen(value)))) {
    fprintf(stderr, "failed to set header.\n");
    // normalize_key is freed here
    return false;
  }
  // normalize_key is freed here
  return true;
}
Enter fullscreen mode Exit fullscreen mode

In the above example we attach a clean up function to the normalized_key variable because it's only a temporary variable that we always want to free by the end of the function.

Inside Loops

Another example with temporary variables is cleanup with variables inside loops.

void debug_print(struct item_t *arr, size_t n) {
    for (size_t i = 0; i < n; ++i) {
        __attribute__((cleanup(free_char))) char *tmp = item_detailed_str(arr[i]);
        if (tmp != NULL) {
            printf("%s\n", tmp);
        }
        // tmp is freed here
    }
}
Enter fullscreen mode Exit fullscreen mode

This is a trivial example but can be quite useful to not deal with cleanup inside loops.

Structures

Cleaning up structures are made easy as well.

void item_free(struct item_t *item) {
    // free item internals
}

void item_operation(char *name, int quantity, char *label) {
    __attribute__((cleanup(item_free))) struct item_t tmp;
    if (!item_init(&tmp, name, quantity, label)) {
        // handle error
    }
    // performa operations
    // tmp is freed here.
}
Enter fullscreen mode Exit fullscreen mode

Deferring Operations

The last most useful example is deferred actions. This can range from needing DB operations called by the end of a scope, freeing mutexes, etc.

void state_free_mutex(struct state_t **s){
    pthread_mutex_unlock(&((*s)->mutex));
}

void state_update_list(struct state_t *s, struct item_t *items, size_t item_len) {
    __attribute__((cleanup(state_free_mutex))) struct state_t* local = s;
    pthread_mutex_lock(&local->mutex);
    // operate on items array and state

    // state's mutex is unlocked
}
Enter fullscreen mode Exit fullscreen mode

Macro Helpers

The use-cases showcase how powerful this attribute can be, but it does come with cumbersome syntax. However, we can alleviate this with simple macros.

The simplest approach is with the following macro:

#define DEFER(cb) __attribute__((cleanup(cb)))
Enter fullscreen mode Exit fullscreen mode

And that's it! Now we can change the original example to look like this:

int main(void) {
    DEFER(free_char) char *test = malloc((sizeof(char)*14));
    strncpy(test, "Hello, World!", 13);
    test[14] = '\0';
    printf("test string = \"%s\"\n", test);
    // free_char is called when this function exits
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

That's much easier to read and type.

You can take this a step further, if you'd like, and add predefined simple macros for common types.

extern void free_char(char **p);

#define AUTO_C DEFER(free_char)
Enter fullscreen mode Exit fullscreen mode

Which then allows the example to turn into this:

int main(void) {
    AUTO_C char *test = malloc((sizeof(char)*14));
    strncpy(test, "Hello, World!", 13);
    test[14] = '\0';
    printf("test string = \"%s\"\n", test);
    // free_char is called when this function exits
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The cleanup attribute is a versatile and powerful tool to utilize within your projects. Though C can be a tedious language to work with, compiler features like these help make working with C less problematic and provide a more robust way of interacting with the language.

Deploy Infra Like a Game: Spacelift Webinar

Deploy Infra Like a Game: Spacelift Webinar

Join Spacelift on Aug 6 and learn how to build a self-service portal for deploying infrastructure, inspired by Minecraft servers. Discover Blueprints, OpenTofu, and drift remediation.

Join the Webinar

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.

Gen AI apps are built with MongoDB Atlas

Gen AI apps are built with MongoDB Atlas

MongoDB Atlas is the developer-friendly database for building, scaling, and running gen AI & LLM apps—no separate vector DB needed. Enjoy native vector search, 115+ regions, and flexible document modeling. Build AI faster, all in one place.

Start Free

👋 Kindness is contagious

Take a moment to explore this thoughtful article, beloved by the supportive DEV Community. Coders of every background are invited to share and elevate our collective know-how.

A heartfelt "thank you" can brighten someone's day—leave your appreciation below!

On DEV, sharing knowledge smooths our journey and tightens our community bonds. Enjoyed this? A quick thank you to the author is hugely appreciated.

Okay