I am trying to figure out how to apply Rust lifetimes to add some compile-time enforcement to Erlang NIF modules. NIF modules are shared libraries normally written in C that provide extensions.
A simplified prototype of the callback you would write in C looks like this:
Handle my_nif_function(Heap *heap, Handle handle);
You are provided a handle and a pointer to the heap that owns it. In your callback you may inspect the input handle, create more handles on the heap, and return one of them as the function return. The heap and all its handles become invalid after your callback returns, so you must not store copies of the heap or its handles during the callback. Unfortunately I’ve seen people do exactly this and it eventually results in a mysterious emulator crash. Can Rust enforce these lifetime constraints?
I think the heap can be easily managed by turning it into a reference.
fn my_nif_function(heap: &Heap, handle: Handle) -> Handle
But how can I link the lifetime of the input and output handles to the heap?
Another wrinkle to this is that you can also create your own heaps and handles which are allowed to live outside the scope of a callback invocation. In C++ I would use std::unique_ptr
with a custom destructor. What is the Rust equivalent? The [simplified] C API for managing heaps looks like this:
Heap *create_heap();
void destroy_heap(Heap *);
Reference: NIFs are described here: http://www.erlang.org/doc/man/erl_nif.html . The Erlang names for "heaps" and "handles" are "environments" and "terms". I used the names "heaps" and "handles" so that the question would be more broadly understood.
Rust 1.0
The various marker types have been unified into one: PhantomData
use std::ptr;
use std::marker::PhantomData;
struct Heap {
ptr: *const u8,
}
impl Heap {
fn new(c_ptr: *const u8) -> Heap {
Heap {
ptr: c_ptr
}
}
fn wrap_handle<'a>(&'a self, c_handle: *const u8) -> Handle<'a> {
Handle {
ptr: c_handle,
marker: PhantomData,
}
}
}
struct Handle<'a> {
ptr: *const u8,
marker: PhantomData<&'a ()>,
}
fn main() {
let longer_heap = Heap::new(ptr::null());
let handle = {
let shorter_heap = Heap::new(ptr::null());
let longer_handle = longer_heap.wrap_handle(ptr::null());
let shorter_handle = shorter_heap.wrap_handle(ptr::null());
// longer_handle // ok to return
// shorter_handle // error: `shorter_heap` does not live long enough
};
}
Original Answer
Here's an example of using ContravariantLifetime
. We wrap the raw heap pointer into a struct and then wrap raw handle pointers in another struct, reusing the lifetime of the heap.
use std::ptr;
use std::marker::ContravariantLifetime;
struct Heap {
ptr: *const u8,
}
impl Heap {
fn new(c_ptr: *const u8) -> Heap {
Heap {
ptr: c_ptr
}
}
fn wrap_handle<'a>(&'a self, c_handle: *const u8) -> Handle<'a> {
Handle {
ptr: c_handle,
marker: ContravariantLifetime,
}
}
}
struct Handle<'a> {
ptr: *const u8,
marker: ContravariantLifetime<'a>,
}
fn main() {
let longer_heap = Heap::new(ptr::null());
let handle = {
let shorter_heap = Heap::new(ptr::null());
let longer_handle = longer_heap.wrap_handle(ptr::null());
let shorter_handle = shorter_heap.wrap_handle(ptr::null());
// longer_handle // ok to return
// shorter_handle // error: `shorter_heap` does not live long enough
};
}
Lifetime markers
There are 3 lifetime markers. I won't attempt to replicate the reasonably good but dense documentation here, but can also point out the dense Wikipedia page, which might be some small assistance. I've listed them in the order that you are most likely to use them:
ContravariantLifetime
InvariantLifetime
CovariantLifetime