Best Practices to avoid "Self Referential" structs

A question for the class about rust lifetimes / "self-referential" structs: I have the following structs:

pub struct Renderer<'a> {
    surface: wgpu::Surface<'a>,
    config: wgpu::SurfaceConfiguration,
    device: Device,
    queue: Queue,
}

Renderer is what is sounds like. The lifetime param on the surface (from wgpu) is defined by the lifetime of the window I am creating (surface stores a reference to the window), it is created like this:

impl<'a> Renderer<'a> {
    pub fn new(window: &'a Window) -> Renderer<'a> {
        let size = window.inner_size();

        let instance = Instance::default();
        
        let surface = instance.create_surface(window).unwrap();
...

        Renderer {
            surface,
            device,
            queue,
            config,
        }
}

So far this is all fine, but I want to have a structure that stores both the window and my renderer:

pub struct Application {
    window: Window,
    renderer: Renderer,
}

Ofc this has the problem that the lifetime parameter 'a is not defined for Renderer, but as far as I can tell there is no way in rust to have renderer use the lifetime of window , the best I can do is this:

pub struct Application<'a> {
    window: winit::Window,
    renderer: Renderer<'a>,
}

but this has the problem that when I define Application like so:

        let window = winit::window::WindowBuilder::new()
            .build(&event_loop)
            .unwrap();

        let mut renderer = Renderer::new(&window);

        let mut app = Application {
            window,
            renderer
        };

I get the following error:

   |
21 |         let window = winit::window::WindowBuilder::new()
   |             ------ binding `window` declared here
...
25 |         let mut renderer = Renderer::new(&window);
   |                                          ------- borrow of `window` occurs here
...
28 |             window,
   |             ^^^^^^ move out of `window` occurs here
29 |             renderer
   |             -------- borrow later used here

which makes sense, because the lifetime of renderer is in no way tied to the lifetime of window in the struct definition, as it needs to be. I have found a variety of (what seem to me to be) hacks using Pin and unsafe stuff, which I could do, but part of my goal of learning rust it rethink the way I write programs. I think what I'm trying to do here might be very "C++", and I'm wondering whether there might be a different high-level approach I could use that would be more rust friendly.

I'm wondering if anyone might have any ideas. The reason I want Application to store both window and renderer is application is responsible for handling the update loop and handling window messages, so it needs access to the window (to handle the messages) and the renderer (to tell it to render, as well as to respond to some messages: e.g. a resize). I think maybe I could make renderer a singleton of sorts (not sure if there's a safe way to do this in rust, I think maybe not?) Or I can just pass through my renderer everywhere I guess but I don't love that (but maybe that's just C++ brain talking). Would love to hear anyone's thoughts on how to approach this kind of thing I'd really appreciate it!

It's a good question and I'm sure there will be some good responses!

But would you mind editing your post to add code formatting as described here?:
https://users.rust-lang.org/t/forum-code-formatting-and-syntax-highlighting/42214

3 Likes

You should put the Window in an Arc, which you then hand a clone of to create_surface(), instead of a reference. Then, the Surface and thus your Renderer need no lifetime shorter than 'static.

use std::sync::Arc;

pub struct Application {
    window: Arc<winit::Window>,
    renderer: Renderer,
}

pub struct Renderer {
    surface: wgpu::Surface<'static>,
    ...
}

...

let window = Arc::new(winit::window::WindowBuilder::new()
    .build(&event_loop)
    .unwrap());

...

let surface = instance.create_surface(window.clone()).unwrap();

In general, the answer to many lifetime problems is: don't use references. The self-referential struct problem only comes up unavoidably when you encounter a library that forces you to give it references, but for this case, wgpu is not one of those libraries.

7 Likes

You beat me to it, thank you!

1 Like

Thanks for the response! Putting the window in an Arc makes sense to me, but I'm not sure I get how we can use 'static for the surface? I am new to rust so sorry if I'm mistaken, but as far as I understand it 'static means that it will live for the remainder of the program. So this works as long as the window (and so Application) only goes out of scope at the end of the program?

So for example, this:

fn main() {
    {
        let app = Application::new(); 
        app.run();
    }
    
    {
        let app = Application::new();
        app.run();
    }
}

would not compile? I don't think I'd ever want to do this, just making sure I understand

as far as I understand it 'static means that it will live for the remainder of the program

This is a common misunderstanding. If you have a reference &'static T, then

  • the T will live for the rest of the program,
  • therefore the &'static T can live for the rest of the program, if its owner sees fit,
  • therefore the bound &'static T: 'static holds,
  • but also, the owner of a &'static T, or any other type U where U: 'static, can hold the U for as long as it likes and drop it whenever it likes.

In general, Foo: 'a holds whenever Foo contains only references that can live for 'a — which is trivially satisfied when it contains no references.

The lifetime parameter of Surface<'a> in particular, doesn't mean that the Surface holds a &'a Window, but that the Surface holds something meeting the 'a bound. So, a Surface<'static> can contain any kind of WindowHandle that can live for 'static, of which an Arc<Window> is one example.

The Surface can drop its internal window handle whenever it likes (in practice: only when it is dropped), and you can drop it whenever you like.

4 Likes

This is a common misconception because the first few things you encounter that are 'static are things that live for the remainder of the program.

'static in a type like this means that all references contained inside the type can live as long as the type needs them to live; that can mean that they borrow from something that lives for the remainder of the program, but it can also mean that this type owns everything it refers to, and thus there's nothing being borrowed from.

In your case, Arc<winit::Window> is owned by wgpu::Surface<'static>; wgpu::Surface<'static> can thus ensure that it doesn't drop the Arc<winit::Window> until it's OK for the window to cease existing. Thus, your example will compile and run, since the first time the Application goes out of scope, it'll drop the Window it has access to, and then the second run will create a new Window.

4 Likes

That makes a lot of sense, thank you!

1 Like

Thanks for the clarification, I think I get it now :slight_smile:

1 Like

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.