Safe Pointer Management With Rust
Table of Contents
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.