7 min

You’re debugging a production issue at 2am. Memory usage is climbing steadily, the system’s getting slower, and somewhere in a 500-line function there’s a malloc without a matching free. You’ve checked every early return. You’ve traced every error path. And you’re thinking: there has to be a better way to do this.

There is. It’s called RAII—“Resource Acquisition Is Initialisation.” The pattern originated in C++, where it’s built into the language. C doesn’t have the language features that make RAII automatic, but the underlying concept is still valuable and you can apply it with a bit of discipline.

The core idea is simple: tie the lifetime of a resource to the lifetime of an object. When the object is created, it acquires the resource. When the object is destroyed, it releases the resource. No separate cleanup step to forget, no resources left dangling.

The Problem RAII Solves

Consider a function that opens a file, does some work, and closes it:

int process_config(const char *filename) {
    FILE *f = fopen(filename, "r");
    if (!f) return -1;

    // Do some work...
    if (parse_failed) {
        return -1;  // Oops! We forgot to close the file.
    }

    // Do more work...
    if (validation_failed) {
        return -1;  // Forgot again.
    }

    fclose(f);
    return 0;
}

Every early return is a potential resource leak. You have to remember to call fclose() on every exit path. As functions grow more complex, with more error conditions and nested resources, this becomes a maintenance nightmare. You either end up with deeply nested code trying to avoid early returns, or a tangle of goto statements jumping to cleanup labels.

RAII addresses this by making cleanup automatic. The resource is released when execution leaves the scope, regardless of how it leaves—normal return, early return, or error.

RAII in C++: The Automatic Version

In C++, destructors make RAII straightforward. When an object goes out of scope, its destructor runs automatically:

// C++ version - destructor handles cleanup
void process_config(const char *filename) {
    std::ifstream file(filename);  // Constructor opens the file

    if (parse_failed) return;      // Destructor closes file automatically
    if (validation_failed) return; // Destructor closes file automatically

    // Normal exit - destructor still closes file automatically
}

You literally cannot forget to close the file. The language handles it.

C Doesn’t Have Destructors

C doesn’t have destructors, so we can’t get truly automatic cleanup. But we can still apply the RAII principle by being disciplined about how we structure code. The goal is to make resource lifetimes obvious and cleanup hard to forget.

There are a few approaches, each with trade-offs.

Approach 1: The Cleanup Label Pattern

This isn’t pure RAII, but it’s a common C idiom that achieves similar goals. You use a single cleanup section at the end of the function and goto to reach it from any error path:

int process_config(const char *filename) {
    int result = -1;          // Assume failure
    FILE *f = NULL;           // Initialise to NULL so cleanup is safe
    char *buffer = NULL;

    // Acquire resources
    f = fopen(filename, "r");
    if (!f) goto cleanup;

    buffer = malloc(1024);
    if (!buffer) goto cleanup;

    // Do work...
    if (parse_failed) goto cleanup;

    // More work...
    if (validation_failed) goto cleanup;

    // Success
    result = 0;

cleanup:
    // Release resources in reverse order of acquisition
    free(buffer);       // free(NULL) is safe, does nothing
    if (f) fclose(f);   // fclose(NULL) is not safe, so we check

    return result;
}

This centralises cleanup in one place. You can’t accidentally skip it because every exit path goes through the same label. The resources are initialised to safe values (NULL) at the top, so cleanup code can run safely even if acquisition failed partway through.

If you’ve ever looked at kernel code and wondered why Linus Torvalds, a man with strong opinions about code quality, tolerates goto statements everywhere, this is why. It’s a deliberate pattern that’s survived decades of battle-testing in one of the most critical codebases on Earth.

Approach 2: Wrapper Structs with Init/Destroy Functions

A more structured approach is to create types that bundle a resource with functions to acquire and release it. This is closer to the spirit of RAII:

// file_handle.h
typedef struct {
    FILE *fp;
    int valid;
} FileHandle;

// Initialise the handle and open the file (acquisition)
int file_handle_init(FileHandle *handle, const char *filename, const char *mode) {
    handle->fp = fopen(filename, mode);
    handle->valid = (handle->fp != NULL);
    return handle->valid ? 0 : -1;
}

// Close the file and clean up (release)
void file_handle_destroy(FileHandle *handle) {
    if (handle->valid && handle->fp) {
        fclose(handle->fp);
        handle->fp = NULL;
        handle->valid = 0;
    }
}

// Helper to check if the handle is usable
int file_handle_is_valid(const FileHandle *handle) {
    return handle->valid;
}

Now your function looks like this:

int process_config(const char *filename) {
    FileHandle handle;

    if (file_handle_init(&handle, filename, "r") != 0) {
        return -1;
    }

    // Do work with handle.fp...

    file_handle_destroy(&handle);  // You still have to remember this
    return 0;
}

This is cleaner, but you still have to remember to call the destroy function. The benefit is that the pairing is obvious—init and destroy go together, and code review can easily spot a missing destroy.

Approach 3: Scope-Bound Resource Management with GCC/Clang

If you’re using GCC or Clang, there’s a feature that almost nobody talks about, buried in the compiler documentation, that gives you proper RAII in C. It’s been there for years. I’m genuinely not sure why it isn’t more widely known.

It’s the cleanup attribute. It lets you specify a function to call automatically when a variable goes out of scope:

// Define a cleanup function
static void auto_close_file(FILE **fp) {
    if (*fp) {
        fclose(*fp);
    }
}

// Macro to make it easy to use
#define AUTO_CLOSE __attribute__((cleanup(auto_close_file)))

int process_config(const char *filename) {
    AUTO_CLOSE FILE *f = fopen(filename, "r");
    if (!f) return -1;

    if (parse_failed) {
        return -1;  // f is automatically closed!
    }

    if (validation_failed) {
        return -1;  // f is automatically closed!
    }

    // Normal return - f is automatically closed!
    return 0;
}

This is proper RAII in C. The file is closed automatically when f goes out of scope, no matter how that happens.

Notice the cleanup function takes FILE **, not FILE *. That trips people up the first time. The cleanup function receives a pointer to the variable itself, so it can access (and modify) the variable’s value.

You can create a family of these for different resource types:

// For malloc'd memory
static void auto_free(void **ptr) {
    free(*ptr);
}
#define AUTO_FREE __attribute__((cleanup(auto_free)))

// For file descriptors
static void auto_close_fd(int *fd) {
    if (*fd >= 0) close(*fd);
}
#define AUTO_CLOSE_FD __attribute__((cleanup(auto_close_fd)))

// For mutexes
static void auto_unlock(pthread_mutex_t **mutex) {
    pthread_mutex_unlock(*mutex);
}
#define AUTO_UNLOCK __attribute__((cleanup(auto_unlock)))

Usage becomes very clean:

int do_something(void) {
    AUTO_FREE char *buffer = malloc(1024);
    AUTO_CLOSE FILE *f = fopen("data.txt", "r");
    AUTO_CLOSE_FD int sock = socket(AF_INET, SOCK_STREAM, 0);

    if (!buffer || !f || sock < 0) {
        return -1;  // Everything cleaned up automatically
    }

    // Work with resources...

    return 0;  // Everything cleaned up automatically
}

The catch? It’s a compiler extension, not standard C. If your code needs to compile on some obscure embedded compiler from 2003, you’re out of luck. But if you’re targeting GCC or Clang—which covers Linux, macOS, BSDs, and most ARM toolchains—you’ve had this superpower available the whole time.

Approach 4: Scope Guards with Macros

This next technique is either brilliant or cursed, depending on your tolerance for macro wizardry. I’ll show it to you, but I’m not sure I’d recommend it.

You can use macros to create a scope that handles cleanup:

#define SCOPED_FILE(var, filename, mode) \
    for (FILE *var = fopen(filename, mode), *_once = (FILE*)1; \
         _once && var; \
         _once = NULL, fclose(var))

void process_config(const char *filename) {
    SCOPED_FILE(f, filename, "r") {
        // Use f here...
        // File is closed when this block exits
    }
    // f no longer exists here
}

The for loop runs exactly once (controlled by the _once variable), and the cleanup happens in the loop’s increment expression. It works, but I’d reach for the cleanup attribute if it’s available.

The Underlying Principle

Whichever approach you use, the principle remains the same: make resource lifetimes explicit, tie acquisition and release together, and structure your code so that cleanup is hard to skip. In C++, the language enforces this through destructors. In C, you enforce it through convention, discipline, and sometimes compiler extensions.

The cleanup attribute gives you the closest thing to automatic RAII in C. The goto-cleanup pattern is portable and battle-tested. Wrapper structs with init/destroy functions make the resource lifecycle explicit even if the calls aren’t automatic. Pick the approach that fits your constraints and stick with it consistently.

C doesn’t hold your hand. It never has. But that doesn’t mean you have to juggle resources manually and hope you never drop one. RAII is a principle, not a language feature, and with a bit of structure, you can make cleanup something you rarely think about because it’s already handled.

As always, thanks for joining me on this journey into embedded development!