Mutably borrowed variable with lifetime in for loop

Hey I cannot wrap my head around lifetimes in Rust

Can someone explain to me why this does not work and how to build something like this? I'm trying to build a "cache" for textures (I'm using SDL2) that loads the texture into memory if it was not previously accessed.

My TextureManager


pub struct TextureManager<'a> {
    texs: HashMap<String, Texture<'a>>,
    creator: TextureCreator<WindowContext>,
}

impl <'a: 'b, 'b> TextureManager<'a> {
    pub fn new(creator: TextureCreator<WindowContext>) -> Self {
        TextureManager { texs: HashMap::new(),  creator }
    }

    pub fn get(&'a mut self, name: String) -> &'b Texture<'a> {
        let m = &mut self.texs;
        if m.contains_key(&name) {
            return m.get(&name).unwrap()
        } else {
            let tex = self.creator.load_texture(format!("assets/{}", name)).unwrap();
            m.insert(name.clone(), tex).unwrap();
            return m.get(&name).unwrap()
        }
    }

}

How I try to use it in the render loop

    let mut tex_manager = TextureManager::new(canvas.texture_creator());

    'running: loop {
        ...
        for rp in hotel.rooms.iter() {
            ...
            
            let texture = tex_manager.get(room.sprite());
            canvas.copy(&texture, sprite, screen_rect)?;
        }
        ...
        
    }

The error message:

error[E0499]: cannot borrow `tex_manager` as mutable more than once at a time
   --> src/main.rs:151:27
    |
151 |             let texture = tex_manager.get(room.sprite());
    |                           ^^^^^^^^^^^ `tex_manager` was mutably borrowed here in the previous iteration of the loop
...
163 | }
    | - first borrow might be used here, when `tex_manager` is dropped and runs the destructor for type `TextureManager<'_>`

Before writing your code (or question) to completion, already an important observation:

impl<…> TextureManager<'a> {
pub fn get(&'a mut self, …

this self parameter has type &'a mut TextureManager<'a>, which is essentially never the correct type signature of anything in Rust. It’s a useful guideline when "fighting" the borrow-checker, in the context of having structs with lifetime arguments, that you always avoid this pattern of "mutable reference to struct with same lifetime as the mutable reference".

Secondly; structs with lifetimes are an advanced topic in Rust – it’s always a good idea to think about the question of what it takes to avoid designing such a struct entirely?

Okay, now that I’ve also glanced at the sdl2 crates; at first glance, it looks like they don’t offer any satisfying way to avoid the lifetime when using Texture :-/

It looks like Texture is borrowing from TextureCreator. This means that your struct is what we call a "self-referential" datatype, because it tries to encapsulate both an "owner" of sorts (the TextureCreator; and also another thing that borrows from it (the Texture).

Rust has no built-in support for these (there should be many existing other threads on the general topic). While some macro crates exist for self-referential datatypes, they are often quite inconvenient to use.

If your TextureManager’s use case permits, I would instead suggest you let the user of TextureManager handle the ownership of the TextureCreator altogether. Basically, start with:

pub struct TextureManager<'a> {
    texs: HashMap<String, Texture<'a>>,
    creator: &'a TextureCreator<WindowContext>,
}
1 Like

Thanks for the hints.

I've now modified it has such and it compiles (and runs :smiley: )

pub struct TextureManager<'a> {
    texs: HashMap<String, Texture<'a>>,
    creator: &'a TextureCreator<WindowContext>,
}

impl <'a> TextureManager<'a> {
    pub fn new(creator: &'a TextureCreator<WindowContext>) -> Self {
        TextureManager { texs: HashMap::new(),  creator }
    }

    pub fn get(&mut self, name: String) -> &Texture<'a> {
        let m = &mut self.texs;
        if m.contains_key(&name) {
            let tex = m.get(&name).unwrap();
            tex
        } else {
            let tex = self.creator.load_texture(format!("assets/{}", name)).unwrap();
            m.insert(name.clone(), tex);
            return m.get(&name).unwrap()
        }
    }
}
...


let tex_creator = canvas.texture_creator();
let mut tex_manager = TextureManager::new(&tex_creator);

Previously I always got complains that the texture I've created in the get method does not live long enough. That's why I've added the &'a mut self.

Thinking further down the line, actually this might not work very well either, because, the get function then

pub fn get(&mut self, name: String) -> &Texture<'a>

can only give access to a single texture at a time...

If this turns out to be a problem, you could try replacing HashMap<String, Texture<'a>> with elsa::FrozenMap<String, Box<Texture<'a>>>, to make get work with &self instead of &mut self.

use elsa::FrozenMap;
use sdl2::{
    image::LoadTexture,
    render::{Texture, TextureCreator},
    video::WindowContext,
};

pub struct TextureManager<'a> {
    texs: FrozenMap<String, Box<Texture<'a>>>,
    creator: &'a TextureCreator<WindowContext>,
}

impl<'a> TextureManager<'a> {
    pub fn new(creator: &'a TextureCreator<WindowContext>) -> Self {
        TextureManager {
            texs: FrozenMap::new(),
            creator,
        }
    }

    pub fn get(&self, name: &str) -> &Texture<'a> {
        if let Some(tex) = self.texs.get(name) {
            tex
        } else {
            let tex = self
                .creator
                .load_texture(format!("assets/{name}"))
                .unwrap();
            self.texs.insert(name.to_owned(), Box::new(tex))
        }
    }
}

You could even chose Rc instead, which allows getting an Rc<Texture<'a>> that doesn't contain a lifetime anymore that borrows to the TextureManager itself (only the lifetime borrowing the original TextureCreator, still, of course..)

click to see a code example for that
use std::rc::Rc;

use elsa::FrozenMap;
use sdl2::{
    image::LoadTexture,
    render::{Texture, TextureCreator},
    video::WindowContext,
};

pub struct TextureManager<'a> {
    texs: FrozenMap<String, Rc<Texture<'a>>>,
    creator: &'a TextureCreator<WindowContext>,
}

impl<'a> TextureManager<'a> {
    pub fn new(creator: &'a TextureCreator<WindowContext>) -> Self {
        TextureManager {
            texs: FrozenMap::new(),
            creator,
        }
    }

    pub fn get(&self, name: &str) -> &Texture<'a> {
        if let Some(tex) = self.texs.get(name) {
            tex
        } else {
            let tex = self
                .creator
                .load_texture(format!("assets/{name}"))
                .unwrap();
            self.texs.insert(name.to_owned(), Rc::new(tex))
        }
    }

    pub fn get_rc(&self, name: &str) -> Rc<Texture<'a>> {
        if let Some(tex) = self.texs.map_get(name, Rc::clone) {
            tex
        } else {
            let tex = self
                .creator
                .load_texture(format!("assets/{name}"))
                .unwrap();
            self.texs.insert(name.to_owned(), Rc::new(tex));
            self.texs.map_get(name, Rc::clone).unwrap()
        }
    }
}

Edit: Just noticing… if you only do the Rc-based approach, you actually don't need elsa anymore:

use std::{cell::RefCell, collections::HashMap, rc::Rc};

use sdl2::{
    image::LoadTexture,
    render::{Texture, TextureCreator},
    video::WindowContext,
};

pub struct TextureManager<'a> {
    texs: RefCell<HashMap<String, Rc<Texture<'a>>>>,
    creator: &'a TextureCreator<WindowContext>,
}

impl<'a> TextureManager<'a> {
    pub fn new(creator: &'a TextureCreator<WindowContext>) -> Self {
        TextureManager {
            texs: RefCell::new(HashMap::new()),
            creator,
        }
    }

    pub fn get(&self, name: String) -> Rc<Texture<'a>> {
        self.texs
            .borrow_mut()
            .entry(name)
            .or_insert_with_key(|name| {
                self.creator
                    .load_texture(format!("assets/{name}"))
                    .unwrap()
                    .into()
            })
            .clone()
    }
}

(and… you could even go back to &mut self and remove the RefCell, I guess)

1 Like