Lifetime issue with buffer when creating structure

Hello! I am trying to work with softbuffer and faced this wierd lifetime issue when I tried creating separate struct for Buffer and Window:

se std::{num::NonZeroU32, sync::Arc};

use winit::window::Window;

type Surface = softbuffer::Surface<Arc<Window>, Arc<Window>>;

type Buffer<'a> = softbuffer::Buffer<'a, Arc<Window>, Arc<Window>>;

struct Canvas<'a> {
    surface: Surface,
    window: Window,
    buffer: Option<Buffer<'a>>
}

impl<'a> Canvas<'a> {
    fn new(surface: Surface, window: Window) -> Self {
        Self { surface, window, buffer: None }
    }

    fn update(&mut self) {
        let width = self.window.inner_size().width;
        let height = self.window.inner_size().height;
        self.surface.resize(NonZeroU32::new(width).expect("Window size must be positive!"), NonZeroU32::new(height).expect("WIndow size must be positive!"));
        let buffer: = self.surface.buffer_mut().expect("Failed creating framebuffer");
        self.buffer = Some(buffer);
    }
}

But compiler says:

[rustc] lifetime may not live long enough assignment requires that `'1` must outlive `'a` [:20] let's call the lifetime of this reference `'1` [:15] lifetime `'a` defined here

I am confused because I can get Buffer and use it in code as long as I don’t drop surface, but I cannot add it to struct which will hold it some time till it will be replaced with next update().

Can someone please explain what am I doing wrong?

May this example could help you

In softbuffer, Surface::buffer_mut() returns a borrow of the Surface. You cannot store a value and a borrow of that value in the same struct — the type system cannot express it, particularly not the requirement that self.surface must remain unmoved and unaltered while self.buffer exists. (I wish Rust did support this pattern, but it doesn’t today.)

There are tools to do it anyway, like ouroboros, and it would be possible to make this work, but if at all possible, you should avoid this code organization instead. Create a Buffer only when you are going to start drawing a frame, and drop it when you finish, and do all this within the scope of one function call.

2 Likes

Is it ok to use this as a solution?

let buffer = unsafe {
    std::mem::transmute::<Buffer<'_>, Buffer<'a>>(buffer)
};

Before creating separate struct I just used render loop where I got buffer on each iteration. So I wanted the same behavior here, but add custom drawing function (eg. draw_pixel). Since AFAIK I can get buffer only once I need to store it inside struct. But why can’t I do this? For me the logic here is the same as just getting buffer to local variable and presenting + dropping it on iteration end.

How can this cause undefined behavior in reality?

No, this is unsound. If the Canvas is moved, then so is the Surface, and so any borrows of it or its fields that Buffer may contain are invalidated — and may cause dereference of dangling pointers, which is undefined behavior. (ouroboros works around this by heap-allocating the data which is referred to, so it never moves.)

You don’t need self-reference to do this — instead, make a struct that is just as temporary as buffer.

struct Canvas {
    surface: Surface,
    window: Window,
}

struct DrawingContext<'a> {
    buffer: Buffer<'a>,
}

impl Canvas {
    pub fn start_drawing(&mut self) -> DrawingContext<'_> {
        let buffer = self.surface.buffer_mut().unwrap();
        DrawingContext { buffer }
    }
}

impl DrawingContext<'_> {
    pub fn draw_line(&mut self, ...
}
2 Likes

Will wrapping Surface in Box prevent it from moving and invalidating Buffer?

No, it will still invalidate the borrows, even though that would prevent dangling pointers in the strictest sense. (The reasons why are rather esoteric and under debate; it might change in the future, but it hasn’t changed yet.)

I strongly recommend that you take the approach with two structs. It will save you a lot of trouble.

2 Likes

Rust calls this self-referential struct, and the borrow checking model doesn’t allow this. The borrow checker can’t verify safety of any structs having a shape like that

Additionally, semantics of the always-strictly-exclusive &mut technically mean such struct can’t exist with a usable surface field, because the same buffer data would be accessible via two different fields at the same time, which is a violation of the exclusivity. Forcing this through with unsafe could result in miscompilation caused by incorrect aliasing information

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.