SDL2 bindings and a borrowing issue / question

Hello!

I'm using the SDL2 bindings (rust-sdl2) for a project in Rust, and being fairly new to Rust I've run into a bit of an ownership / borrow checker problem. I've been able to kludge around it with some unsafe code, but I am hoping to learn the proper Rust way of dealing with this.

What I want is to have a struct to hold my textures. In rust-sdl2, a Texture is created using a TextureCreator, and must hold a reference to it. A Texture is dropped if the TextureCreator goes out of scope.

My current implementation is to have separate TextureHolder and Renderer structs, where the Renderer holds the SDL context itself (including the TextureCreator), and TextureHolder holds each Texture. I understand I cannot have them in the same struct, as each Texture holds a reference to the TextureCreator, and having struct members refer to other parts of the same struct is not allowed in Rust. (I may, in the future, try using std::pin to work around this.)

However, the issue I run into is that when I load the first Texture into TextureHolder, it borrows the Renderer struct so it can use the TextureCreator contained therein, but it doesn't give it back. Therefore, creating any single Texture takes ownership / borrows the Renderer, and locks rendering completely. I assume the Renderer gets handed to the Texture and used to make the reference that the Texture needs to hold to the TextureCreator.

I am able to work around this by using std::mem::transmute() in an unsafe block to create a new, unblocked Texture, but I appreciate I'm deliberately breaking the Rust ownership model in doing so. Here's a simplified version of the relevant code (which exists inside of a function that borrows renderer):

let mut result = HashMap::new();
for (name, path) in images_to_load {
  let texture = renderer.texture_creator.load_texture(path).expect("Could not load texture.");
  // Unsafe kludge below
  // If this weren't here, the Renderer would be kidnapped by the HashMap
  let texture = unsafe{
    std::mem::transmute::<_,sdl2::render::Texture<'static>>(texture)
  };
  result.insert(String::from(name), texture);
}
return result;

I appreciate this may be avoidable if the TextureCreator (and the entire SDL context) was created in main(), and was passed by itself rather than as part of a struct. This seems to be the approach used in a lot of the examples online, which get away with being simple imperative programs. But this is not appropriate for my application - the rendering logic needs to be abstracted and encapsulated, both for architectural reasons and because SDL is unlikely to be the only rendering back-end.

I'm sure there's a simple solution here, as I cannot imagine the rust-sdl2 crate is intended to work in this way, and as someone new to Rust I'm probably just missing something incredibly obvious. What would the best, most idiomatic way of implementing something like this in Rust be?

It seems like the API is quite intentionally designed to prevent a Texture from being alive after the TextureCreator is destroyed. I cannot tell you why that is, but it's quite clear from the API.

Working around this would require some unsafe, but std::pin is an unrelated feature and it is neither necessary nor helpful in working around it.

1 Like

It's probably due to the fact that the texture may reside in video memory and must be managed through whatever GPU abstraction (Metal/Vulkan) SDL uses internally.

Right! But what do I do about this? How do I build a Texture in a way that won't lock the renderer (or texture creator) for evermore? I'm struggling to believe the SDL bindings are limited to a single texture per window, that would seem to be rather useless. There must be something I'm missing here.

Practically, there's no actual problem with ownership, as the textures and the renderer will, in practice, have the same lifetime, which is until the end of main(). I just don't know how to make the borrow checker understand this. Can I pass some kind of non-locking reference to the renderer? As I say, I'm fairly new to Rust, so I just don't know how this is achieved in Rust.

That part of my post was in reference to my avoiding self-referential structs. To be clear, this isn't the major issue at the moment - the major issue is the locked renderer.

All help is very much appreciated. :slight_smile:

The README mentions a feature flag to disable the lifetime.

I remember running into this issue when I was playing around with sdl2, but I don't remember what I did about it.

1 Like

One option is to use Box::leak(Box::new(texture_creator)) to create an &'static TextureRenderer. Then, you can get a 'static lifetime on your Texture objects.

I don't think that's the case. The TextureCreator methods take &self, so you can create multiple textures using it.

I've switched to the unsafe-textures feature for now, just to move on with my project, but I'm still curious how to do this in a safe Rusty way.

This is exactly where I'm stuck - I don't seem to be able to. I'm sure I'm just making a silly Rust mistake here. Let's simplify and say I do the following:

resources.rs:

use crate::renderer;
use sdl2::image::LoadTexture;

pub struct Resources<'a> {
	texture: sdl2::render::Texture<'a>,
}

impl<'a> Resources<'a> {
	pub fn new(renderer: &renderer::Renderer) -> Resources {
		let texture = renderer.sdl_texture_creator.load_texture("res/resource.png").expect("Could not read texture file.");
		Resources { texture, }
	}
}

main.rs (inter alia):

let mut renderer = renderer::Renderer::new();
let resources = resources::Resources::new(&renderer);
renderer.set_fullscreen_on();

I get the following cargo error:

error[E0502]: cannot borrow `renderer` as mutable because it is also borrowed as immutable
   --> src/main.rs:57:2
    |
56  |     let resources = resources::Resources::new(&renderer);
    |                                               --------- immutable borrow occurs here
57  |     renderer.set_fullscreen_on();
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
...
119 | }

The second line fails because the entire renderer struct seems to now be borrowed and held by the resources variable. Is there a way to avoid this?

Ah, well, if set_fullscreen_on takes a &mut self then you can't call it while a texture exists.

Honestly, it sounds like the library you're using is just poorly designed.

3 Likes

OK, thank you! That's a very helpful note. I did think it was strange, but being new to Rust I just naturally assumed I must be missing something obvious here.