RAII, Drop and foreign libraries

RAII, or Resource Acquisition Is Initialization, is a programming policy that was popularized with C++, and one that Rust proudly puts at the forefront of its standard library and conventions. This core tenet of API design ensures that well-written Rust code has no dependency on global state and that everything has a well-defined lifetime. Considering that one of Rust's unique features is an elaborate borrow checker that explicitly handles the lifetime of resources across function and thread boundaries, it is doubly important that APIs respect that policy to give the most control to the programmer leveraging it.

However, Rust is a pretty young language, and it doesn't have the same library support that more mature systems languages like C and C++ have. So, many developers opt to wrap around existing C libraries for practical reasons; there's no reason to reimplement a library in pure Rust just for ideological reasons. That means those wrappers have to preserve Rust's safety invariants when working across library boundaries. This can range from very simple, for libraries like libpng that depend on very little internal state, to extremely difficult, especially in the realm of computer graphics where OpenGL is entirely global state.

Let's take a practical example: SDL, a library commonly used for video games to be ported to multiple platforms. It provides thin layers over common multimedia functionality, as well as a GL binding, for several platforms.

SDL requires initialization of the library, and then initialization of each desired subsystem before you can use any of the functions in each system. Initialization and shutdown of system implementations is managed by internal state in the library. To avoid the situation where a subsystem is initialized multiple times and then quit prematurely when other libraries don't expect it to, SDL increments a reference counter each time a subsystem is called to be initialized, and decrements it when Quit is called. Once that hits zero, the resources used by that subsystem (and eventually SDL as a whole) are disposed, by the implementation's definition.

The naive way to map these system state dependent functions to Rust would be to simply return Result<_, String> for everything. Clearly, these functions will fail if the subsystems are not initialized, and we can use SDL_GetError to get a C string representation of the last error. But this doesn't model that concern within the type system. We know that the function can fail on two overarching conditions: the subsystem isn't initialized, or an internal error occurred. But, we cannot tell the difference easily without introspecting on the String returned. How can we leverage the type system so that we simply can't call the function at all when the subsystem isn't initialized?

We can choose to model the subsystems as types, such that when they are dropped naturally, the raw quit function will be called. This way, much like the lifetime of references, the lifetime of our subsystems is managed entirely by scoping rules in Rust. For the sake of brevity, our example won't use the exact process of initialization and shutdown.

use std::marker::PhantomData;  
use ::raw as ll;

pub struct MySubsystem {  
    pd: PhantomData<()>

impl MySubsystem {  
    pub fn new() -> Self {
        unsafe { ll::myLibrary_Init() };
        MySubsystem { pd: PhantomData }

impl Drop for MySubsystem {  
    fn drop(&mut self) { unsafe { ll::myLibrary_Quit() }; }

Pretty simple. You can explicitly dispose of a resource by calling drop as in drop(MySubsystem::new()), but it will automatically be disposed when the binding goes out of scope. The struct, and what it represents, cannot be constructed without calling new, preserving RAII rules.

Now, we can implement our function wrappers as member functions of MySubsystem. They can never be called without a valid reference to a MySubsystem, so now we can use the Result<_,String> result type to meaningfully model that "This function can fail" specifically for internal reasons, rather than having ambiguity. That can be further expanded on by returning a type that implements std::error::Error. It becomes a syntax error to call those functions at all without an explicit resource acquisition.