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
f1
needs to be followed by a call tof2
before callingf1
again. 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.