LINUX.ORG.RU

Rust + WGPU + dyn trait

 ,


0

5

Есть небольшой рабочий набросок кода, который комплируются и запускается, создавая окно winit и instance + surface wgpu:

use std::sync::Arc;
use winit::event_loop::EventLoop;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, ControlFlow};
use winit::window::{Window, WindowId};

trait Renderer<'window> {
}

struct WGPURenderer<'window> {
	instance: wgpu::Instance,
	surface: wgpu::Surface<'window>
}

impl<'window> WGPURenderer<'window> {
	async fn new(window: Arc<Window>) -> anyhow::Result<Self> {
		let instance = wgpu::Instance::default();
		let surface = instance.create_surface(Arc::clone(&window))?;
		Ok(Self {
			instance,
			surface
		})
	}
}

impl<'window> Renderer<'window> for WGPURenderer<'window> {
}


#[derive(Default)]
struct App<'window> {
	window: Option<Arc<Window>>,
	renderer: Option<WGPURenderer<'window>>
}

impl ApplicationHandler for App<'_> {
	fn resumed(&mut self, event_loop: &ActiveEventLoop) {
		if self.window.is_none() {
			let window_attrs = Window::default_attributes();
			let window = Arc::new(
				event_loop.create_window(window_attrs).
					expect("Failed to create window")
			);
			self.window = Some(window.clone());
			let renderer = pollster::block_on(WGPURenderer::new(window))
				.expect("Failed to create renderer");
			self.renderer = Some(renderer);
		}
		event_loop.set_control_flow(ControlFlow::Wait);
	}
	
	fn window_event(
		&mut self,
		event_loop: &ActiveEventLoop,
		_window_id: WindowId,
		event: WindowEvent
	) {
		match event {
			WindowEvent::CloseRequested => {
				event_loop.exit();
			}
			_ => {}
		}
	}
}

fn main() {
	let event_loop = EventLoop::new()
		.expect("Failed to create event loop");
	let mut app = App::default();
	event_loop.run_app(&mut app)
		.expect("Failed to start event loop");
}

Проблема в том, что я хочу добавить косвенность - я не хочу прибивать гвоздями App к WGPURenderer, я хочу сделать возможными в будущем написать разные реализации трейта Renderer - например, VulkanRenderer, OpenGLRenderer и т. д.

Но стоит мне сделать:

#[derive(Default)]
struct App<'window> {
	window: Option<Arc<Window>>,
	renderer: Option<Arc<dyn Renderer<'window>>> // Тут может быть и Box
}

...

let renderer = pollster::block_on(WGPURenderer::new(window))
	.expect("Failed to create renderer");
self.renderer = Some(Arc::new(renderer));

Я получаю:

error: lifetime may not live long enough
  --> src/main.rs:29:25
   |
17 |     fn resumed(&mut self, event_loop: &ActiveEventLoop) {
   |                --------- has type `&mut App<'1>`
...
29 |             self.renderer = Some(Arc::new(renderer));
   |                                  ^^^^^^^^^^^^^^^^^^ coercion requires that `'1` must outlive `'static`

Как правильно организовать архитектуру приложения, чтобы можно было иметь общий код App (где расположен код обработки событий ввода и т. п.) на все бекэнды рендера с учётом того, что WGPU требует лайфтайм окна?

★★★★★

Последнее исправление: KivApple (всего исправлений: 3)

Внезапно, сработало вот такое вот:

struct WGPURenderer {
	_window: Arc<Window>, // To be sure that window won't be destroyed before surface
	instance: wgpu::Instance,
	surface: wgpu::Surface<'static>
}

impl WGPURenderer {
	async fn new(window: Arc<Window>) -> anyhow::Result<Self> {
		let instance = wgpu::Instance::default();
		let surface = instance.create_surface(Arc::clone(&window))?;
		Ok(Self {
			_window: window,
			instance,
			surface
		})
	}
}

impl Renderer for WGPURenderer {
}

Соответственно, из Renderer уходит 'window и теперь его можно пихать с dyn хоть в Box, хоть в Arc без проблем.

Но меня удивляет, что Rust позволил мне это сделать без всяких unsafe (я сначала думал придётся делать transmute, чтобы избавиться от lifetime и, собственно, из-за этого вставил сохранение ссылки на окно в рендерер для перестраховки правильного порядка удаления). Возникает вопрос, а нужно ли сохранять вообще ссылку на окно для безопасного разрушения рендерера.

KivApple ★★★★★
() автор топика
Ответ на: комментарий от KivApple

Чего-то сложно у тебя получается. Если есть Arc, то зачем тебе явное время жизни?

Если у структуры указано время жизни, то это совсем не значит, что ты его должен явно указывать. Попробуй не указывать. Да и вообще, неявно, вообще, у всех объектов есть время жизни. Мы же их часто не указываем - они подхватываются автоматически.

Что касается ’static для Surface, то это очень сильное заявление. Попробуй пока просто все сделать через Arc<dyn…> и просто Arc<…> без явного указания времени жизни.

anonymous
()
Ответ на: комментарий от Silerus

Вот так кстати можно передавать между потоками:

#![allow(unused)]

use std::{
    sync::mpsc::{channel, Receiver, Sender},
    thread,
};

trait Bar {
    fn bar(&self);
}

struct Foo {
    foo: i32,
}

impl Bar for Foo {
    fn bar(&self) {
        println!("foo: {}", self.foo);
    }
}

fn main() {
    let foo = Box::new(Foo { foo: 1 }) as Box<dyn Bar + Send>;

    let (tx, rx): (Sender<Box<dyn Bar + Send>>, Receiver<Box<dyn Bar + Send>>) = channel();

    thread::spawn(move || {
        tx.send(foo).unwrap();
    });

    let sent = rx.recv().unwrap();

    sent.bar();
}

WatchCat ★★★★★
()

Не вдаваясь в вопросы архитектуры, ошибка происходит из-за того, что время жизни у trait-object выводится как 'static. Но ему явно задать можно и тогда будет компилироваться:

#[derive(Default)]
struct App<'window> {
	window: Option<Arc<Window>>,
	renderer: Option<Arc<dyn Renderer<'window> + 'window>> // Тут может быть и Box
}

Подробнее про вывод времени жизни у trait-object можно здесь почитать: https://doc.rust-lang.org/reference/lifetime-elision.html#default-trait-object-lifetimes

netrino
()
Ответ на: комментарий от netrino

renderer: Option<Arc<dyn Renderer<'window> + 'window>>

И это будет значить, что renderer содержит ссылку на объект, внешний по отношению к структуре App. То есть renderer не сможет использовать App::window.

Ну и ApplicationHandler предполагает, что окна будут создаваться динамически. Лайфтаймы на такое использование не рассчитаны. Так что тут только 'static

red75prim ★★★
()
Последнее исправление: red75prim (всего исправлений: 1)
Ответ на: комментарий от red75prim

И это будет значить, что renderer содержит ссылку на объект, внешний по отношению к структуре App. То есть renderer не сможет использовать App::window.

Так вон же использует, без проблем:

let renderer = pollster::block_on(WGPURenderer::new(window))
    .expect("Failed to create renderer");

Ну и ApplicationHandler предполагает, что окна будут создаваться динамически. Лайфтаймы на такое использование не рассчитаны. Так что тут только ’static

'static или не 'static в этом конкретном примере никакой роли не играет вообще. Время жизни, хоть и называется 'window, к окну никакого отношения не имеет и оно в любом случае шире, чем нужно renderer-у. Так что код эквивалентен с 'window и без него.

Но, как я сказал, мой коммент был конкретно про ошибку компиляции, а не про архитектуру. Часто встречал новичков, которые не знают про то, что trait-object имеет время жизни, которое можно явно указать.

netrino
()
Последнее исправление: netrino (всего исправлений: 1)
Ответ на: комментарий от netrino

Так вон же использует, без проблем:

Да, неверно выразился. Правильно будет: renderer не сможет использовать ссылку на Window, которое лежит в том-же самом инстансе структуры что и renderer. В расте нельзя выразить лайфтайм ссылок в self-referential структурах.

А компилируется потому что renderer использует не &Window, а Arc<Window> со 'static лайфтаймом.

red75prim ★★★
()
Последнее исправление: red75prim (всего исправлений: 1)