RAII in C

In my last post, I talked about resource management, and how RAII, Resource Acquisition is Initialization, is important in C++ because of the guarantees it provides. In this post, I’ll show how we can make the same guarantees in C.

In C++, RAII provides a solution to resource management and the issues arising with exceptions. Although C doesn’t support exceptions, many of the same challenges, problems, and motivation for proper resource management are present in C. Leaking or conflicted resource management will at best hurt performance, and may crash your application.

In C, we write our resource-management code in such a way as to guarantee complete execution of an initialization and finalization step. For the purposes of this idiom, functions start with initialization code, run through body code, and then end with finalization code.

Compared with C++, we can simplify our work by handling all resources as dynamic or transient resources — what we called case 3, later allocation, in the previous post.

The C implementation is further simplified because we don’t have exceptions. We can guarantee execution will run through all the initialization and finalization code we write.

There are three distinct parts to this pattern: initialization, body, and finalization.

Initialization

Near the top of a C function that implements the RAII pattern, we start with initialization code. Initialization code must follow a simple rule that all initialization code is guaranteed to execute. There are no early returns, nor are there go-tos or constructs that skip initialization code.

The best way to handle this is to write initialization code that cannot fail, and simply clears resource references. Code that could fail, like allocations or file-opening, are best moved into the body. If the initialization code does run into a failure, a failure result should be returned, but that failure must not prevent other initialization code from running.

For example, if we have three resources, A, B, and C, the preferred initialization code would only clear the references to each, and postpone allocation of the resource until later. Further, if an error condition was detected during the initialization of A, we must continue with the initialization of B and C.

When working with multiple resources, this rule guarantees that all resource references are initialized.

Body

The body code follows initialization. Whereas the initialization and finalization steps are guaranteed to run on entry and exit, the body isn’t so linear. Execution may run through the body in multiple paths. The only requirement is that the finalization code is always called after the body is run so the body cannot return out of the function.

Your resources dictate how your body handles them. Where some resources may be dynamic or transient, and require code to check for validity before use, other resources remain valid after their allocation or binding.

For example, a network connection is considered transient, and may be lost and re-connected at any time. Your body code must work with the initialization and finalization code to manage the resource.

Another example, a memory allocation is dynamic and may fail at allocation, but will remain valid until freed.

The body code will often start with startup or setup code. This code runs through attempted memory and resource allocations. If any of the allocations fail, the body code may jump directly to the finalization.

Finalization

Like initialization, finalization is guaranteed to run. As the C++ RAII destructor balanced the constructor, design your C finalization code to balance the initialization.

Write your finalization code to check to see if the resource reference is valid, and release the resource if it is valid.

This code will handle any path the execution took through the body, including skipping the body completely.

Architecture

Granularity

In C++, RAII-objects are setup to handle one resource per object; necessary with exceptions. In C, we have more freedom, since we are guaranteed complete initialization and finalization. Write your C systems at a similar granularity — don’t overload a single system with too many resources.

A C programmer will benefit from thinking a bit like a C++ programmer when designing resource access: encapsulate the resource initialization, finalization, and access functions in an object-like way.

For example, the interface to a configuration resource may include:

typedef struct { /* config fields */ } Config;

void ConfigInit(Config *cfg); // initialization

void ConfigFinal(Config *cfg); // finalization

int ConfigProcess(Config *cfg,const char *path); // read into cfg

Order Of Initialization

Avoid order-dependent initialization code. Code with fewer dependencies is easier to reuse. Where there are dependencies, for example, initialization of the memory system, comment that in your code.

Even when your code is order-independent, it’s a good idea to set up the initialization and finalization in stack-based order: That is, you should release resources in the reverse order they were acquired during the initialization step. Releasing in reverse order is good practice as it unwinds the top layer dependencies first, child systems before parent systems, and the base systems last.

Structure 1 shows an example of the pattern that sets up the initialization phase with calls to system initializations, a call to the body function, and then the finalization phase, with calls to system finalizations.

 

=== STRUCTURE 1 ===

 

// INIT

MemoryInit(); // must be first

FileSystemInit();

SerialPortInit();

 

// BODY

Body();

 

// FINAL

SerialPortFinal();

FileSystemFinal();

MemoryFinal();

Setup / Start

For many systems, more work has to be done after initialization and before use. This work includes actions like allocating buffers, opening files, scanning for, and locating system resources.

Because all of these actions may fail, they may not be written into the initialization code. Failure of some of these actions may make running the program infeasible. Other failures may be recoverable.

This setup / start code needs to be completed before the program may drop into its main loop, and is written as the first part of the body code, after the initialization.

Structure 2 shows use of a goto that jumps to the finalization phase. This limited use of goto guarantees we run the finalization code when we run into failures in the body. For these examples, a negative return signals an error.

 

=== STRUCTURE 2 ==

 

// INIT

MemoryInit(); // must be first

FileSystemInit();

SerialPortInit();

 

// BODY-STARTUP

if(ReadAllConfigFiles()<0)

goto FINAL;

if(OpenDebugUART()<0)

goto FINAL;

if(AllocateBuffers()<0)

goto FINAL;

 

// BODY-MAIN

ApplicationBody();

 

FINAL: // FINAL

SerialPortFinal();

FileSystemFinal();

MemoryFinal();

Summary

RAII is an important C++ idiom for resource management. Notably, RAII provides a structural idiom for proper resource management with exceptions.

The power of the idiom is in the guarantees it provides. Properly used, the destructor for your RAII-object is guaranteed to be called to allow you to free resources.

In a future post, I’ll compare the code and compiler output from the C++ and C approaches to RAII.

Previous
Next