How can I use lifetimes to convey the following constraints to the compiler?

Hello,

I've been trying to port some more c++ code to rust and I ran into the following issue: Playground link.


error[E0597]: `*app` does not live long enough
  --> src/main.rs:38:18
   |
38 |     let device = app.create_device();
   |                  ^^^ borrowed value does not live long enough
...
42 |         device,
   |         ------ cast requires that `*app` is borrowed for `'static`
...
45 | }
   | - `*app` dropped here while still borrowed

error[E0505]: cannot move out of `app` because it is borrowed
  --> src/main.rs:41:9
   |
38 |     let device = app.create_device();
   |                  --- borrow of `*app` occurs here
...
41 |         app,
   |         ^^^ move out of `app` occurs here
42 |         device,
   |         ------ cast requires that `*app` is borrowed for `'static`

The gist of what is happening is that I have something that implements the App trait which then allows the user to create an implementation of the Device trait which is valid for that App implementation. Both of those are then placed in the Instance type and used throughout the program.

The main issues I'm trying to solve is that I'd like to provide clues to the compiler that Box<dyn Device> should not live longer than Box<dyn App>. I would like to catch the problem where in Instance, if I don't declare the fields in the correct order, Box<dyn App gets dropped before Box<dyn Device>`.

I have also tried explicitly add the lifetime parameter to the trait (Playground link) but I end up with the same error.

Shouldn't this not be an issue since both dyn App and dyn Device are allocated on the heap and moving them should be fine?

If I remove the + '_ from the Box<dyn Device> return type, everything compiles, but there's no way to enforce the drop to happen in the correct order.

Is there any way I can solve this?

Your Instance is self-referential as the device field stores a reference to the app field. It's not possible to express this in Rust without unsafe.

In this case, I'd do away with the Instance type and instead require devices to provide access to their corresponding app:

trait Device: Debug {
    fn get_app(&self)->&dyn App;
}

struct FooDevice<'a>{
     app: &'a BarApp
}

impl<'a> Device for FooDevice<'a>{
    fn get_app(&self)->&dyn App { self.app }
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=bc5330dbe84efb1bed2550e046eda9b9

Thanks for the suggestion. Sadly in this case I need both App and the Device to be available. The app provides some other features for the user that are unrelated to the device.

In the original code the App provides input handling , OS window management among other things. The Device is a rendering device Vulkan or OpenGL.

Edit: To further clarify, the app is created first, used to create a window, then from that window one creates a Device.

My suggestion was essentially to make App available through any extant Device, so they will indeed both be available, though perhaps not in a way that's convenient for the rest of the program.

You obviously have more criteria in mind for evaluating potential solutions than you've listed here. Can you elaborate some on the program structure, especially how these Apps and Devices are created, stored, used, and destroyed?

I do get that, but then I would have to figure out a way where to store the app instance without it being a global of some sorts :thinking:.

Certainly. You can find the full code here: GitLab Repo.

The function in question is ThreedExampleRunner::new which creates a new instance of ThreedExampleRunner where both app and the device are stored.

Another option would be to put the App inside Rc or Arc, and give a copy to the device. Then, the app won't be dropped until the device has dropped its reference to the app.

Hmm, good point. That would definitely work :+1:. Thanks!

That being said, I'm still wondering if there is a way to solve this without having to resort to Rc/Arc :thinking:

You can't have any active references to a value when it gets moved between stack frames. Here, that happens when you return the owned App from new(), so the device can't hold a reference to the app at that point. If new() took a reference to the app as a parameter instead of creating it, you could then return a Device that is tied to that app via lifetimes.

I see. Thanks for the insights!