Bounding lifetimes of raw pointers from FFI

I'm writing a Rust interface to a C++ library. Rust lifetimes are a good fit for this use case, but I'm struggling to give them appropriate bounds in a single unsafe function instead of relying on every piece of Rust code to handle the lifetimes correctly.

I am working with a struct that looks like this in Rust:

struct EventLoop<'a>(&'a ffi::EventLoop);

struct Thingie<'a>(
  Box<ffi::Thingie>,
  PhantomData<&'a mut ffi::EventLoop>,
);

impl<'a> EventLoop<'a> {
  fn make_thingie(&mut self) -> Thingie<'a> {
    Thingie(unsafe { self.0.MakeThingie() }, PhantomData)
  }
}

to create Rust objects that look like this:

struct X<'a> {
  event_loop: EventLoop<'a>,
  thingie: Thingie<'a>,
}

impl<'a> X<'a> {
  fn new(mut event_loop: EventLoop<'a>) -> Self {
    let thingie = event_loop.make_thingie();
    Self { event_loop, thingie }
  }
}

// exported for C++ to call via a wrapper
unsafe fn make_x(event_loop: *mut ffi::EventLoop) -> Box<X<'static>> {
  Box::new(X::new(EventLoop(&mut *event_loop)))
}

The caller of make_x is responsible for ensuring the event_loop pointer lives as long as the returned object. That means everything that works with a generic X<'a> will obey the correct lifetime restrictions (no references will escape the generic lifetime being passed in, which could be anything as short as the lifetime of the containing X).

The return value of make_x is being passed back to C++-land which doesn't use lifetimes, so I think just calling it 'static is fine. The other option that comes to mind is unsafe fn make_x<'a>(event_loop: *mut ffi::EventLoop) -> Box<X<'a>> where the (C++) caller can still choose an arbitrary lifetime up to and including 'static, but I don't see what difference that makes. The calling C++ code handles the lifetimes by conventions and comments anyways.

However, I would like for this to not compile when following the same pattern:

struct Y {
  event_loop: EventLoop<'static>,
}

impl Y {
  fn new(event_loop: EventLoop<'static>) -> Self {
    Self {event_loop }
  }
}

unsafe fn make_y(event_loop: *mut ffi::EventLoop) -> Box<Y> {
  Box::new(Y::new(EventLoop(&mut *event_loop)))
}

This code is pretty obviously wrong, but there are more subtle ways to accidentally force the Rust compiler to conclude lifetimes deriving from the raw pointer are 'static when they should not be. As far as I know, there is no way to directly attach a lifetime to a dereferenced raw pointer. The compiler creates the lifetime arbitrarily.

The only approach I can think of is something like this:

unsafe fn make_object<F, R>(
    event_loop: *mut ffi::EventLoop,
    f: F,
  ) -> R<'static>
  where any<'w> F: FnOnce(EventLoop<'w>) -> R<'w>
{
  f(EventLoop(&mut *event_loop))
}

unsafe fn make_x_safe(event_loop: *mut ffi::EventLoop) -> Box<X<'static>> {
  Box::new(make_object(event_loop, X::new))
}

which takes a function that does the correct things with lifetimes, and then calls it with 'static. Unfortunately that syntax doesn't exist. where any<'w> F: FnOnce(EventLoop<'w>) -> R + 'w EDIT: this is something very different, it parses as {FnOnce(EventLoop<'w>) -> R} + 'w, not FnOnce(EventLoop<'w>) -> {R + 'w} gets close, but I can't figure out how to create a function that actually satisfies that bound. Also I don't know how to declare make_object as returning the 'static version of the return type.

I looked at using Generic Associated Types to write a trait and implement it for appropriate FnOnce types, with an associated Result<'w> type corresponding to the return type. However that still leaves me unable to write out a bound for a type parameter that does the correct operations with the lifetimes that X::new actually satisfies.

Additional bits to make my examples compile, and playground link for the parts that compile:

use core::marker::PhantomData;

mod ffi {
  pub struct EventLoop;
  pub struct Thingie;

  impl EventLoop {
    pub unsafe fn MakeThingie(&mut self) -> Box<Thingie> {
      /* stubbed out */ panic!()
    }
    pub unsafe fn GetInt(&self) -> &i32 {
      /* stubbed out */ panic!()
    }
  }
}

Some additional context, if it helps: The library is built around C++ classes that look like this:

struct X {
  X(EventLoop *event_loop) : thingie_(event_loop->MakeThingie()) {}

  std::unique_ptr<Thingie> thingie_;
};

I would like to implement classes like that in Rust. I'm using autocxx to generate the direct wrapper functions for function calls in both directions. Those wrapper functions work in raw pointers and arbitrary lifetimes, so it's up to the Rust code immediately bordering those to enforce appropriate lifetimes for the rest of the Rust code. It's got some quirks, but so far they're manageable, and that's not what I'm asking for help with.

TLDR: I think I answered my own question (need higher kinded types), but if anybody has other suggestions I'd love to hear them.

Today I had another idea, something like this:

trait SafeMake<'a, T> {}
impl<'a, F, T: 'a> SafeMake<'a, T> for F
where F: FnOnce(EventLoop<'a>) -> T {}

and then use for<'a> SafeMake<'a, T> as a bound. However, that requires a single T for all 'a, which isn't what I'm trying to constrain it to. This is the same fundamental limitation as using FnOnce directly: I need some kind of "lifetime-generic generic parameters", along the same lines as generic associated types but for generic parameters themselves. And now that I write that out, that's higher kinded types!

I think I might be able to adapt some of the ways I see of doing higher kinded types to abstract over lifetimes (might need generic associated types, not sure), but that ends up being a lot of tricky code. It also requires implementing some trait for X and Y, which means something equivalent to a custom derive, and at that point there are easier ways to solve this.

Once I realized my stumbling point is higher kinded types, the solution is obvious: make one of the types explicit. For X specifically, this:

trait DoMake<R> {
    fn make(self, event_loop: EventLoop<'static>) -> R;
}
impl<F, R> DoMake<R> for F
where F: FnOnce(EventLoop<'static>) -> R {
    fn make(self, event_loop: EventLoop<'static>) -> R {
      self(event_loop)
    }
}

unsafe fn do_make_x<F>(
    event_loop: *mut ffi::EventLoop,
    f: F,
  ) -> X<'static>
  where F: for<'a> FnOnce(EventLoop<'a>) -> X<'a>,
  F: DoMake<X<'static>>,
{
  f.make(EventLoop(&mut *event_loop))
}

unsafe fn make_x_safe(event_loop: *mut ffi::EventLoop) -> Box<X<'static>> {
  Box::new(do_make_x(event_loop, |e| X::new(e)))
}

captures the restrictions I'm looking for. Probably be cleaner to put the FnOnce type I'm not calling in the trait, to just call f directly, but that doesn't really change anything. I can easily write a declarative macro to do that, or maybe a custom derive if I want to learn procedural macros. That's a perfectly fine solution for this use case.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.