Safe Pointer Management With Rust

· 3min

One of Rust's killer features is its amazing memory management system via the vaunted "Borrow Checker". We can use this awesome sauce to enhance the ergonomics and safety of interoperating with other languages, as well!

In my professional life I spend a lot of time writing Rust code that interfaces with native APIs on Windows, MacOS, and Linux. What this means in practice is that I have to leave the comfortable confines of Rust and venture into the wild and untamed lands of C and C++. FFI with rust is easy to do but can be somewhat tricky to get "right".

Foreign Function Interface

The Rustonomicon has a great section on FFI with examples on how to call C from Rust and the reverse. So what exactly is FFI? Well, simply put, it is the invocation of code outside the language you are currently working in. Because of this, occasionally memory is allocated by some other language and Rust is unable to leverage the Borrow Checker directly to free it when necessary.

Here's an example of calling a C function that allocates memory that we (the developer) are now responsible for:

use windows_sys::Win32::{
    Foundation::{CloseHandle, FALSE, HANDLE},
    System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION},
};

unsafe {
    let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);

    // use the handle ...

    CloseHandle(handle);
}

Now this might work fine if the flow of the application is this simple. But what happens in the scenario where you need to pass this handle around or its' lifetime cannot be completely guaranteed? Foregoing the CloseHandle call will lead to a memory leak and, depending on your application, could have serious side effects.

Rust to the Rescue

Under "normal" circumstances, Rust's Borrow Checker can map the lifetime of a piece of data at compile time and determine when it must be freed. In the case of this FFI call, that process handle is allocated somewhere and we are now responsible for that allocation's maintenance.

One really nice and ergonomic way to deal with this is to create a Rust wrapper around the FFI type and leverage the languages features to help us manage the deallocation.

#[derive(Clone)]
struct HandleWrapper(pub HANDLE);

impl Drop for HandleWrapper {
    fn drop(mut self) {
        unsafe {
            CloseHandle(self.0);
        }
    }
}

We can now pass a HandleWrapper around in place of a HANDLE and be assured that when it goes out of scope the wrapped value will be freed appropriately.

Minor Optimizations

What if we want to limit the calls to CloseHandle? Our wrapper implements Clone which means we could end up with many instances of our handle. It isn't always optimal to call a close of free function repeatedly, especially after it has already been handled.

We can solve this problem in a number of ways. We could use an internal explicit counter, or we can use some extra Rust goodness to help us out.

#[derive(Clone)]
struct HandleWrapper(Rc<HANDLE>);

impl HandleWrapper {
    pub fn new(handle: HANDLE) -> Self {
        HandleWrapper(Rc::new(handle))
    }

    pub fn as_ptr(&self) -> *const HANDLE {
        self.0.as_ptr()
    }
}

impl Drop for HandleWrapper {
    fn drop(mut self) {
        if Rc::strong_count(&self.0) == 1 {
            unsafe {
                CloseHandle(*self.0.as_ref());
            }
        }
    }
}

If this structure needs to be Send/Sync we can replace some types here with things like Arc and even change HANDLE to something like AtomicISize and do some casting when needed.

Conclusion

Rust is such a great enhancement over other "systems" programming language. The safety and security the memory management model provides allows for a lot of pain free development. Corner cases like Foreign Function Interfaces can seem daunting at the outset, but any hurdle can end up being cleared using Rust's language features.