How to constrain lifetimes to a region of code?

Viewed 103

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 to f2 before calling f1 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.

1 Answers

You can define two structs:

struct FirstState { ... }
struct SecondState { ... }

Then you will be able to define methods that transform each into the other:

impl FirstState {
    // note: Takes ownership of self
    pub fn into_second(self) -> SecondState {
        api::f2();
        SecondState { ... }
    }
}

impl SecondState {
    // note: Takes ownership of self
    pub fn into_first(self) -> FirstState {
        api::f1();
        FirstState { ... }
    }
}

Since each conversion takes ownership of the object, this enforces that you must alternate between calling f1 and f2. Additionally you can define methods like this:

impl FirstState {
    pub fn get_internal_data(&self) -> &InternalData {
        ...
    }
}

The signature of get_internal_data enforces that the returned reference cannot outlive FirstState, even if the data is not stored inside FirstState itself.

Related