skip to content
CodeFade

Cleaning scoped pointers in C

/ 5 min read

Cleaning scoped pointers in C

C gives you the freedom and finer control over memory but also gives you enough chances to shoot yourself in the foot. Both allocations and de-allocations needs to be done manually. This article presents a way to reduce the chances (Of course! you can’t fully get away with it).

Pitfalls

Everyone uses malloc and free or some form of it. What happens when a function you’re calling allocates memory and then it does not free it? Leaaaks! Now, this can be due to several reasons, one more common one is that memory pointer is being returned to the caller for further use. Let’s look at the following code.

// A very trivial example
int* allocator_fn(int size) {
int * ptr = malloc(sizeof(int)*size);
// do some initialization
return ptr;
}
int main() {
// step 1: allocate the memory
int* ptr = allocator_fn(2);
// step 2: do something with ptr
// step 3: free ptr
free(ptr);
return 0;
}

Now it’s easy to see that the function allocates memory, when the function is literally named allocator_fn but in a larger code base, these allocator functions might appear multiple times, may return pointers several call stack up and the name might not be an obvious one.

Let’s look at one more example

int maker(int decision) {
int* ptr = allocator_fn(2);
switch( decision ) {
case 1:
*ptr = 1;
case 2:
*(ptr+1) = 1;
case 3:
*(ptr+1) = 1;
case 4:
// invalid case for some reason
return 0;
}
// bunch of other processing
free(ptr);
}
int main() {
int result = maker(3);
// do something
return 0;
}

When the value of decision is 4, the maker function returns early and misses the free for the dynamically allocated ptr.

Now, there are no smart pointers in C, neither do we have defer mechanism like we have in Zig.

C++ has this concept of RAII1, which essentially means that when an object is created, the necessary memory (or any other resource) is allocated in the constructor and freed at the time of destruction of that object. The resource lifetime is tied to the lifetime of the object.

In the example above, the lifetime of the memory allocated by the allocator_fn should be tied to the ptr variable and should be cleaned up when it goes out out scope.

Fixing this in C

There is a way to get this behavior in C as as well i.e. tying the lifetime of a resource to a variable. Let’s have a look at the cleanup compiler extension.

#include<stdio.h>
void clean(int *value) {
printf("Cleanup value: %d\n", *value);
}
int main() {
printf("Main Begin\n");
{
printf("Scope Begin\n");
int value __attribute__((cleanup(clean))) = 3;
printf("Value: %d\n", value);
printf("Scope End\n");
}
printf("Main End\n");
return 0;
}
// Output:
// Main Begin
// Scope Begin
// Value: 3
// Scope End
// Cleanup value: 3
// Main End

The cleanup function is called at the time of the end of scope of the variable declared on the stack. This function takes in the pointer to the address of variable declared. We can use this mechanism, to free memory pointed by a pointer when it goes out of scope.

#include<stdio.h>
#include<stdlib.h>
void clean(int **value) {
printf("Cleanup value: %d\n", **value);
free(*value);
}
int main() {
printf("Main Begin\n");
{
printf("Scope Begin\n");
int* value __attribute__((cleanup(clean))) = (int*)malloc(sizeof(int));
printf("Value: %d\n", *value);
printf("Scope End\n");
}
printf("Main End\n");
return 0;
}

Let’s make it look clean by using a macro.

//...
#define WITH_CLEANUP_FN(fn) __attribute__((cleanup(fn)))
int main() {
printf("Main Begin\n");
{
int* value WITH_CLEANUP_FN(clean) = (int*)malloc(sizeof(int));
}
printf("Main End\n");
return 0;
}

Here we’re allocating memory inline using malloc. Let’s make it look even cleaner with two functions, one that allocates memory and another one that cleans up2.

void* allocator(int n) { return malloc(sizeof(int)*n); }
void* dealloc(void **ptr) { free(*ptr); *ptr = NULL; }
#define WITH_CLEANUP_FN(fn) __attribute__((cleanup(fn)))
#define WITH_SCOPED_CLEANUP_FN(var, alloc, dealloc) void* var WITH_CLEANUP_FN(dealloc) = alloc
int main() {
printf("Main Begin\n");
{
printf("Scope Begin\n");
WITH_SCOPED_CLEANUP_FN(value, allocator(3), dealloc);
printf("Value: %d\n", *(int*)value); // need to convert the pointer everywhere
printf("Scope End\n");
}
printf("Main End\n");
return 0;
}

This macro is written generically (using void*) to be used everywhere. We’d need to convert the pointer every single time. We can copy the pointer to another variable after casting but that’d open another possibility of use after free3. Ideally, we’d want to use the same pointer. Let’s make it more seamless and more readable.

void* allocator(int n) { return malloc(sizeof(int)*n); }
void* dealloc(void **ptr) { free(*ptr); *ptr = NULL;}
#define WITH_CLEANUP_FN(fn) __attribute__((cleanup(fn)))
#define WITH_SCOPED_CLEANUP_FN(var, alloc, dealloc) alloc; void* dummy WITH_CLEANUP_FN(dealloc) = var
int main() {
printf("Main Begin\n");
{
printf("Scope Begin\n");
int* value = WITH_SCOPED_CLEANUP_FN(value, allocator(3), dealloc);
printf("Value: %d\n", *value); // no need to convert the pointer
printf("Scope End\n");
}
printf("Main End\n");
return 0;
}

The code is a bit more readable because we can see the new pointer we’re creating explicitly. Also, there is no need to typecast it everywhere. We’re doing this by using a new dummy pointer on stack but this would be hidden away.

This can also be used for scoped resource allocation where resource like file or a socket is closed when the scope of the accessor variable end. Here’s an example

void file_cleanup(int *fd) {
close(*fd);
printf("File Closed!\n");
}
#define WITH_CLEANUP_FN(fn) __attribute__((cleanup(fn)))
#define WITH_FILE_AS(f, fname, flags) int f WITH_CLEANUP_FN(file_cleanup) = open(fname, flags);
int main() {
printf("Main Begin\n");
{
WITH_FILE_AS(f, "filename.txt", O_RDWR | O_CREAT);
write(f, "hello", 6);
}
printf("Main End\n");
return 0;
}

Caveats

This cleanup is part of the compiler extension supported by both gcc and clang. There is an alternative for MSVC as well called try-finally. It’d be good to use if you’re sticking to a single platform and not writing extremely portable code (looking at you FFmpeg!).

Footnotes

  1. Resource Aquisition Is Initialization (RAII)

  2. How does free know how much to free

  3. Only if we reassign it to a larger scoped variable. If the smaller scoped variable goes out of the scope, the resource would be freed. Accessing the other pointer would be disastrous is this case.