RAII Pattern

Spoiler: To avoid resource management problems (acquisition and release), the RAII pattern is a must known of object-oriented programming. To avoid problems (bugs and maintenance), and if your language allows it, initialize in constructors, release in destructors, and handle errors with exceptions.

If there is one problem that any developer encounters, it is resource management. The memory, the files, the network connections, the databases, the libraries to be initialized (openssl, opengl, …), the mutexes,… The list is long because indeed, any structure is a resource.

rihaij @ pixabay

Anywhere in the code where we need a resource (its scope, which can be seen as a small universe bubble), the management of a resource comes down to the following three phases:

  1. Acquisition: Did you obtain a valid resource?
  2. Handling: does the resource remain valid?
  3. The release: what happens if you do not release the resource?

First (bad) Example

Let’s take a simple example of incrementing the value saved in a file. The following code, in language C, will perform the necessary operations:

For simplicity, I consider that the functions that read and write values are provided elsewhere and we will not dwell on them.

// Read / Write functions
void read(int fd, int * value) ;
void write(int fd, int value) ;

// Ressource handling
void increment(const char * filename)
{
    int fd = open(filename, O_RDWR | O_CREAT, 0666);
    flock(fd, LOCK_EX) ;
    int value ;
    read(fd, &value) ;
    write(fd, value + 1) ;
    close(fd) ;
}

The problem in this version of the code is that no check is made on the validity of the resource (the file descriptor). What will happen if the file does not exist? If the mutex is not acquired? In the event of an error, the execution can exit the function without the file being closed…

You can believe me, it is always when you forget one of these checks that the case occurs. With luck, the quality department will tell you (and you can take your time to fix it), otherwise, it will be the support team because an unhappy customer has found a bug (and your time will be according to customer’s patience).

Second (better) Example

In this new version, still in language C, we will therefore carry out the necessary checks and be careful to free the resource in the event of a problem. This time, the functions return an error code (-1) when something goes wrong.

int read(int * fd, int * value) ;
int write(int * fd, int value) ;

int increment(const char * filename)
{
    int fd = open(filename, O_RDWR | O_CREAT, 0666);
    if (fd == -1) {
        // Error while opening the file
        return -1 ;
    }

    if (! flock(fd, LOCK_EX)) {
        // Error while trying to acquire a lock
        fclose(fd) ;
        return -1 ;
    }

    int value ;
    if (! read(fd, &value)) {
        // Error while reading the file content
        fclose(fd) ;
        return -1 ;
    }

    if (! write(fd, value + 1)) {
        // Error while writing the new value
        flose(fd) ;
        return -1 ;
    }
    
    if (! close(fd)) {
        // Error while closing the file
        return -1 ;
    }
    
    return 0 ;
}

What was clear is now complicated. From less than 10 lines, we go to about 30, the cyclomatic complexity is multiplied by 6. And from a resource management problem, we move to one of maintainability because for any change to the code will require checking the return codes, releasing the resource and returning an error code.

You can be sure someone will eventually change your code and forget a check or a release, it will be drama and hours of debugging ahead.

The solution : RAII

Demonstrating once again the superiority of object-oriented programming, the RAII pattern solves these management problems in a very elegant way.

RAII is a programming idiom that comes to us from C ++ and whose goal is to guarantee that an acquired resource is valid and that its release will be automatically carried out when it is no longer in range (return, stop, exception, …).

The concepts are indeed very simple and rather classic in OOP:

Example in C++

Improving the previous example, we are now going to encapsulate the management of the file in a File class which will handle the opening in the constructor, the closing in the destructor and will provide methods for the other operations. All with exceptions to make it cleaner.

The following code, in C ++, is an example of a minimal implementation of such a class. We could improve this class, or use the STD classes or boost classes instead but the goal is illustrative. To do things really well, we could even use templates to implement the mutex as a policy but that would be quite beyond the point.

class File
{
private:
    int fd ;

    // Make File not copyable
    File( const File& other );
    File& operator=( const File& );

public:

    File(std::string const & filename)
    {
        fd = open(filename.c_str(), O_RDWR | O_CREAT, 0666);
        if (fd == -1) {
            throw std::exception("Opening file failed") ;
        }
    }

    ~File()
    {
        close(fd) ;
        // No check since one can not throw in destructor
    }

    void lock()
    {
        if (flock(fd, LOCK_EX) == -1) {
            throw std::exception("Locking file failed") ;
        }
    }

    int read() { ... }

    void write(int value) { ... }
}

Thanks to this abstraction, the increment function becomes much simpler since it no longer needs to manage the various error cases but only to define a file and use its methods.

void increment(std::string const & filename)
{
    File f(filename) ;
    f.lock() ;
    f.write(f.read() + 1) ;
}

Example in PHP

The implementation in PHP is similar and differs only by the syntax, the functions used are almost the same and the principle therefore does not change.

The subtle difference is that where the C ++ destroys the objects in a block when execution exits, PHP uses a garbage collector which destroys the objects when they are no longer referenced (cf. the documentation on Constructors and Destructors). It is the same most of the time but in some subtle cases it can be a problem.

<?php
class File
{
    private $fd ;

    public function __construct($filename)
    {
        $this->fd = fopen($filename, "rw") ;
        if ($this->fd === false) {
            throw new Exception("Opening file failed") ;
        }
    }

    public function __destruct()
    {
        if (fclose($this->fd) === false) {
            throw new Exception("Closing file failed") ;
        }
    }

    public function lock()
    {
        if (! flock($this->fd, LOCK_EX)) {
            throw new Exception("Locking file failed") ;
        }
    }

    public function read() { ... }

    public function write($value) { ... }
}

function increment($filename)
{
    $f = new File($filename) ;
    $f->lock() ;
    $f->write($f->read() + 1) ;
}

Which languages supports RAII?

This technique works with all languages supporting exceptions AND ensures that your destructors are called when execution leaves the scope of your objects. This is the case with real object languages such as C++ and PHP, of which we have just seen two examples.

For other languages, it’s a bit more complicated. They often provide a finalize method called on when freeing but since they cannot guarantee that you will release your items as soon as you exit blocks, it won’t help.

Some languages have chosen to cheat with a syntactic trick to fill this gap. With this particular syntax, you can define a method that will be called when the execution exits the corresponding particular block.

Other languages have outright snubbed the principle and considered it unnecessary. As in node.js where you will have to define your macros yourself and hope not to forget an error case.

Example (dangerous) of false RAII

The problem with these methods, apart from the syntactic inconsistency, is that they are not guaranteed by the class which provides the resource but by the one which uses it.

Applied to website access control, it’s like letting visitors decide for themselves whether or not they can see a page. As long as they are playing the game, everything is working fine.

The following example, in python illustrates the problem, the File class provides the same kind of method as before. The goodOne function shows the use of with. The problem is that nothing forces you to use this construct, as the evilOne function shows.

class File:

    def __init__(self, filename):
        self.fd = open(filename, "rw")

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        self.fd.close()

    def lock(self):
        fcntl.flock(self.fd, LOCK_EX)

    def read(self):
        # ...

    def write(self, value):
        # ...

def goodOne(filename):
    with File(filename) as f:
        f.write(f.read() + 1)

def evilOne(filename):
    f = File(filename)
    f.write(f.read() + 1)

So yes, you can answer me that we could delegate the opening of the file descriptor in the __enter__ function and add exceptions in the other methods if the descriptor is not available and therefore force the use of with:

class File:

    def __init__(self, filename):
        self.filename = filename
        self.fd       = None
        
    def __enter__(self):
        self.fd = open(filename, "rw")
        return self

    def __exit__(self, type, value, traceback):
        self.check()
        self.fd.close()

    def check(self):
        if (self.fd is None):
            raise Exception("File have not been correctly created")

    def lock(self):
        self.check()
        fcntl.flock(self.fd, LOCK_EX)

    def read(self):
        self.check()
        # ...

    def write(self, value):
        self.check()
        # ...

But this solution is far from being as practical as a true RAII:

  1. it complicates the File class by imposing a systematic check() call. Let one be forgotten or deleted and your will gain hours of debugging,
  2. it only detects forgetting the with at runtime, so unless you have 100% code coverage by automatic testing, it will happen in production…

In short, you will understand, it remains only DIY to imitate the pros.

And next

If your languages allow the use of this technique, I can only advise you to abuse it because it allows both to simplify the business codes but above all it makes them more secure by avoiding the problems of error management and release.