Storing two objects in a wrapper struct where one's lifetime depends on the other

I'm trying to create three objects: a parent, a child whose lifetime depends on the parent, and a wrapper who is storing both at the same time.

The wrapper below is declared with lifetime 'a and owns both the Parent and the Child.

use std::marker::PhantomData;

struct Parent;

impl Parent {
    fn new() -> Self {
        Self {}
    }
    
    fn create_child<'parent>(&'parent self) -> Child<'parent> {
        Child::new()
    }
}

struct Child<'parent> {
    _marker: PhantomData<&'parent ()>
}

impl<'parent> Child<'parent> {
    fn new() -> Self {
        Self {
            _marker: PhantomData,
        }
    }
}

struct Wrapper<'a> {
    parent: Parent,
    child: Child<'a>,
}

impl<'a> Wrapper<'a> {
    fn new() -> Self {
        let parent = Parent::new();
        let child = parent.create_child();
        
        Self {
            parent,
            child
        }
    }
}

fn main() {
    let _ = Wrapper::new();
}

(Playground)

It seems like this should work just fine, except the compiler complains that parent doesn't live long enough even though it is being moved into the same place as child.

   Compiling playground v0.0.1 (/playground)
error[E0515]: cannot return value referencing local variable `parent`
  --> src/main.rs:37:9
   |
35 |           let child = parent.create_child();
   |                       --------------------- `parent` is borrowed here
36 |           
37 | /         Self {
38 | |             parent,
39 | |             child
40 | |         }
   | |_________^ returns a value referencing data owned by the current function

error[E0505]: cannot move out of `parent` because it is borrowed
  --> src/main.rs:38:13
   |
32 |   impl<'a> Wrapper<'a> {
   |        -- lifetime `'a` defined here
...
35 |           let child = parent.create_child();
   |                       --------------------- borrow of `parent` occurs here
36 |           
37 | /         Self {
38 | |             parent,
   | |             ^^^^^^ move out of `parent` occurs here
39 | |             child
40 | |         }
   | |_________- returning this value requires that `parent` is borrowed for `'a`

Some errors have detailed explanations: E0505, E0515.
For more information about an error, try `rustc --explain E0505`.
error: could not compile `playground` due to 2 previous errors

How do I fix this error so that Wrapper owns both Parent and Child together?

The fundamental problem here is that lifetimes on references don't just mean “the referent will continue to exist for this period”, they mean “the referent will not move for this period”. When Self { parent, child } executes, that's moving the Parent from the local variable parent to the field of Wrapper.

Your Wrapper<'a> also doesn't make sense to the compiler because it has a lifetime parameter 'a, which means the caller gets to pick the lifetime. But, new() then tries to provide a child which is a borrow of a local variable, whose lifetime is therefore less than that of the new() stack frame and therefore always less than 'a (which is a parameter of the type, not even the new() function).

If you really need to do this, you can do it with ouroboros, which uses macros to set up a self-referencing struct via some special tricks (including Boxing fields that are referred to, and emulating a special 'this lifetime name for the self-referential borrows).

However, if possible, you should prefer to avoid needing to do this. For example, it is possible that your goal can be achieved without Child actually containing a reference, or pointer of any sort. Or, perhaps you can use Arc<Parent> instead of &Parent.

It will be easier to advise if you describe your original problem, with relevant existing code if you have it — you've written up a very tidy abstract example of a failed attempt at self-reference, but the concrete details of what you want to use this for will help to determine the best solution to use instead.

3 Likes

Thank you for your reply.

Indeed I did try to write the simplest possible example here, apologies for not including a broader description of my problem.

I am using a crate that implements Rust wrappers around the SDL library. One of the features of the crate implements rendering of text.

A Sdl2TtfContext object must first be initialized, like so.

let ttf_ctx: Sdl2TtfContext = sdl2::ttf::init().expect("failed to initialize TTF subsystem");

The context implements a function called load_font, which can be used to load a font object from disk.

pub fn load_font<'ttf, P: AsRef<Path>>(
    &'ttf self,
    path: P,
    point_size: u16
) -> Result<Font<'ttf, 'static>, String>

As you can see, the function imposes lifetime restrictions on the returned Font object. It must live at least as long as the Sdl2TtfContext object does. I think this is because Font is backed by system resources that are destructed when Sdl2TtfContext is dropped.

I want my own struct to wrap both the Sdl2TtfContext as well as the Font object by creating them both in its constructor. For example:

struct MyApp {
    ttf_ctx: Sdl2TtfContext,
    font: Font<'?>
}

impl MyApp {
    fn new() -> Self {
        let ttf_ctx: Sdl2TtfContext = sdl2::ttf::init().expect("failed to initialize TTF subsystem");
        let font = ttf_ctx.load_font("/path/to/font", 0, 32).expect("failed to load font");
        
        Self {
            ttf_ctx,
            font,
        }
    }
}

But alas, I cannot figure out how to initialize MyApp such that it owns both the context and the font that it loaded.

Your suggestion of changing Child (a.k.a. Font) will not work unfortunately since this external crate is what is imposing this lifetime restriction, not my code. Therefore I must figure out how to make it work without modifying Font or Sdl2TtfContext.

Unfortunately, I've only found one architecture that's convenient to use for the SDL crate: Initialize the SDL systems you need in the outermost function of the graphics thread and pack them up into your own context object, and then pass a reference to that context everywhere.

struct Context {
    sdl: sdl2::Sdl,
    ttf: sdl2::ttf::SdlTtfContext,
    ...
}

struct MyStruct<'sdl> {
    font: Font<'sdl>,
    ...
}

fn graphics_main(...)->Result<(), ...> {
    let ctx = Context {
        sdl: sdl2::init()?,
        ttf: sdl2::ttf::init()?,
        ...
    };

    loop {
        ...
        let x = MyStruct::new(&ctx);
        ...
    }
}
2 Likes

This is a case where, arguably, the SDL Rust bindings are taking the easy path rather than the useful one. (Disclaimer: I haven't studied SDL inside or outside of Rust.) It's an easy situation to end up in: if an C-style API says "This object must not be deallocated as long as any children exist", which is fairly common, then the easiest way to enforce that and produce a safe Rust interface is to add a lifetime to the child. Not only is it easy, but it's also an API design that works fine for small example programs that define every resource as a local variable in main(), and then fails on large ones that want to abstract things into structs.

Some options:

  • As I already described, use ouroboros. (You'll probably want to put the Sdl2TtfContext inside an Arc, so that multiple fonts can share it.)

  • As @2e71828 suggests, create the context inside a stack frame that will live long enough. (This is the “normal” way borrows are supposed to be used in Rust.)

  • Contribute a feature to the SDL bindings that fixes this problem for everyone, by defining something like

    struct RcFont {
        context_to_keep_alive: std::sync::Arc<Sdl2TtfContext>,
        raw: *const ffi::TTF_Font, // the same pointer type that's inside `Font`
    }
    

    that has the same operations but doesn't need a lifetime.

3 Likes

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.