I'm writing Rust bindings for a C API. This particular C API has two functions: f1 and f2. f1 returns a handle that references internal data that's valid until f2 is called1.
What are my options for modeling the handle's lifetime constraints? Preferably enforceable at compile time, though if that is not possible at all I can live with establishing correctness at runtime as well.
A solution can assume the following restrictions:
- Each call to
f1needs to be followed by a call tof2before callingf1again. In other words, there cannot ever be two or more consecutive calls to either function. - All functions are called from the same thread.
Things I tried
I had looked into using the PhantomData marker struct, though that won't work here as I don't have access to the underlying data referenced by the handle.
Another option I had played around with was removing f2 from the public API surface altogether, and have clients pass a function into f1 that can safely assume a valid handle:
pub fn f1(f: fn(h: &Handle) -> ()) {
let h = unsafe { api::f1() };
// Execute client-provided code
f(&h);
unsafe { api::f2() };
}
While that works in enforcing lifetime constraints by never allowing the Handle to escape f1 (I think), it feels like it's taking away too much control from clients. This is library code and I'd rather not turn this into a framework.
Another alternative I had considered was having clients move the handle into f2 to transition ownership back into the library implementation:
pub fn f2(_h: Handle) {
unsafe { api::f2() };
}
That, too, seems to work (I think), although it introduces a seemingly unrelated parameter into f2's signature, making for a somewhat confusing API.
Question
What's the (canonical) solution here that I cannot see?
1 f2 isn't strictly cleanup code. It is called for different reasons, and only invalidates the reference returned by f1 as a side effect.