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;
}
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);
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;
}
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
}
}
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.
}
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
}
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)))
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;
}
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)
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;
}
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.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.