RAII-like Error Handling and Resource Management in C

Error handling and resource management are pervasive in programs. RAII originated in C++ is great. With RAII, it is much easier to write easy-to-read code that allocats/deallocats resources in the constructor/destructors. By representing a resource with a local object, we are sure that local object’s destructor will release the resource and will not forget to release the resource even there are exceptions. One example is as follows.

void foo() {
    // a and b can acquire resources, such as lock, file
    // or memory.
    // They can also throw exceptions for error handling.
    A a();
    if (!a.ok())
       return;

    B b();
    if (!b.ok())
       return;

    // do something here

    // resources will be deallocated automatically in the 
    // reverse order b -> a for objects that have been
    // constructed: 
    return;
}

In C, can we achieve similar semantics with some code idiom that are easy to write and read?

An idiom in C may achieve the similar goal, which I come across here. Here, I wrote the code which does the similar thing as the code in C++ following RAII above.

void foo()
{
    /* acquire resources */
    A *a = acquireA();
    if ( !a )
        goto exit;

    B *b = acquireB();
    if ( !b )
        goto cleanupA;
    
    /* do something here */
    
    /* release resources */
cleanupB:
    releaseB(b);
cleanupA:
    releaseA(a);
exit:
    return;
}

The code is not as clear or simple as the C++ code but can do what we want correctly. Yes, goto is used. However, goto is not an evil if it is used correctly. For this situation, goto actually makes the code clearer. Try to write a version of the code that does not use goto and scale it to process more resources like 3 or 4 acquireA-like functions.

On the other hand, GCC supports the cleanup variable attribute as an non-standard extension for supporting RAII:

cleanup (cleanup_function) The cleanup attribute runs a function when the variable goes out of scope. This attribute can only be applied to auto function scope variables; it may not be applied to parameters or variables with static storage duration. The function must take one parameter, a pointer to a type compatible with the variable. The return value of the function (if any) is ignored.

If -fexceptions is enabled, then cleanup_function is run during the stack unwinding that happens during the processing of the exception. Note that the cleanup attribute does not allow the exception to be caught, only to perform an action. It is undefined what happens if cleanup_function does not return normally.

To achieve the similar semantics above, the C code with cleanup can be as follows.

void foo()
{
    __attribute__((cleanup(releaseA))) A *a = acquireA();
    if ( !a )
        return;

    __attribute__((cleanup(releaseB))) B *b = acquireB();
    if ( !b )
        return;

    /* do something here */

    /* cleanup (works as): */
    /* if b is allocated, call releaseB() */
    /* if a is allocated, call releaseA() */
    return;
}

The code is clearer but requires the GCC extension. And the cleanup_function should be carefully written to return normally. Otherwise, the behavior is undefined.

To see how the cleanup works, check this example code as follows, compile it with a modern (tested on gcc 4.8.2) version of gcc and run it. The acquire{A,B} functions randomly decide whether the resource allocation “succeed”. Run it multiple times and see what happens.

// An illustration of the cleanup attribute:

#include<stdio.h>
#include<stdlib.h>
#include<time.h>

typedef int A;
typedef int B;

A g_a;

A* acquireA()
{
    printf("acquireA\n");
    int r = rand();
    if (r % 2 == 0) {
        return NULL;
    } else {
        return &g_a;
    }
}

B* acquireB()
{
    printf("acquireB\n");
    int r = rand();
    if (r % 2 == 0) {
        return NULL;
    } else {
        return &g_a;
    }

}

void releaseA()
{
    printf("releaseA\n");
}

void releaseB()
{
    printf("releaseB\n");
}

void foo()
{
    __attribute__((cleanup(releaseA))) A *a = acquireA();
    if ( !a )
        return;

    __attribute__((cleanup(releaseB))) B *b = acquireB();
    if ( !b )
        return;

    printf("foo()\n");

    return;
}

int main(int argc, const char *argv[])
{
    srand(time(NULL));
    foo(); 
    return 0;
}

Eric Zhiqiang Ma

Eric is interested in building high-performance and scalable distributed systems and related technologies. The views or opinions expressed here are solely Eric's own and do not necessarily represent those of any third parties.

Leave a Reply

Your email address will not be published. Required fields are marked *