Cannot add sdl2::render::Texture to struct due to lifetime mismatch

Hello I've been trying to implement a small game using an EntityComponentSystem in rust. But am getting stuck with lifetimes quite a bit. In this particular case I'm trying to store an sdl2::render::Texture
EntitySystem struct

The EntitySystem came from this blog post: Tutorial: Writing a tiny Entity Component System in Rust

The "working" code is available here: GitHub - yaspoon/bomberus: My failing attempt at Rust ECS it has some current hacks such as requiring the system_drawable to be a closure so that it can capture the texture variable for later use
I'm trying to make this better/more useable by instead having singleton components stored on the EntitySystem struct but getting stuck at the same problem I did before which is the lifetime of the game_texture which is a sdl2::render::Texture<'_>

Essentially the working code looks approximately like this:

pub trait ComponentHashMap {
	fn as_any(&self) -> & dyn Any;
	
	fn as_any_mut(&mut self) -> &mut dyn Any;
}

impl<T: 'static> ComponentHashMap for RefCell<HashMap<u64, T>> {
	fn as_any(&self) -> &dyn Any {
		return self as &dyn Any;
	}

	fn as_any_mut(&mut self) -> &mut dyn Any {
		return self as &mut dyn Any;
	}
}

#[derive(Copy, Clone, PartialEq, Debug)]
pub struct Entity {
	id: u64,
}

pub struct EntitySystem<'a> {
	next_id: u64,
    entities: HashMap<u64, String>,
	entity_names: HashMap<String, u64>,
	components: HashMap<TypeId, Box<dyn ComponentHashMap>>,
}

impl EntitySystem {
	pub fn new() -> EntitySystem<'static> {
		return EntitySystem {next_id: 0, entities: HashMap::new(), entity_names: HashMap::new(), components: HashMap::new()};
	}
--snip--
--endsnip--
}

let mut es = EntitySystem::new();

--snip--
Setup the sdl2 context, windows etc and get a canvas for the window
--endsnip--

let texture_creator = canvas.texture_creator();

let game_texture = match texture_creator.load_texture(Path::new("assets/bomb_party_v4.png")) {
	Ok(gt) => gt,
	Err(e) => {
	    println!("Unable to load game texture:{}", e);
	    return;
	},
};


let mut system_drawable = move |es: &mut EntitySystem, _dt: f64| -> Result<(), String> {
--snip--
	"capture" the game_texture variable for later use by this closure in the game loop to draw things to the screen
--endsnip--
}

//use the system_drawable closure

The not compiling code is available here in the texture_component_broken branch. (Sorry new users can only have two links :frowning: ) pretty much the same as above but with a few changes to store the game_texture as a member of the EntitySystem struct, I followed the compiler recommendations of adding lifetimes but eventually it gets really upset

pub struct EntitySystem<'a> {
	next_id: u64,
    entities: HashMap<u64, String>,
	entity_names: HashMap<String, u64>,
	components: HashMap<TypeId, Box<dyn ComponentHashMap>>,
	texture: Option<Texture<'a>>
}

impl EntitySystem {
	pub fn new() -> EntitySystem<'static> { //Compiler recommends 'static? Probably because of the static lifetime for the hashmaps?
		return EntitySystem {next_id: 0, entities: HashMap::new(), entity_names: HashMap::new(), components: HashMap::new(), texture: None};
	}

	pub fn set_texture_component(&mut self, texture: Option<Texture<'a>) {
		self.texture = texture;
	}
--snip--
--endsnip--
}

es.set_texture_component(Some(game_texture))

//system_drawable is commented out for now

Following the compilers recommendation results lots of:

error[E0308]: mismatched types
  --> src/main.rs:55:24
   |
55 |     let player = match es.new_entity_with_name("Player".to_string()) {
   |                           ^^^^^^^^^^^^^^^^^^^^ lifetime mismatch
   |
   = note: expected mutable reference `&mut EntitySystem<'static>`
              found mutable reference `&mut EntitySystem<'_>`
note: the anonymous lifetime defined here...
  --> src/main.rs:54:34
   |
54 | fn create_player_entity(es: &mut EntitySystem) -> Result<Entity, String> {
   |                                  ^^^^^^^^^^^^
   = note: ...does not necessarily outlive the static lifetime

I'm guessing this is because of the use of the static lifetime in other parts of the EntitySystem code
which I was following from the tutorial for reference I'm only upto Chapter 10 of the rust book so appologies if this is something simple....

first of all, I didn't read through the code, I simply pull it down, compile it, and fixes what the compiler says.

I'd suggest you first get yourself comfortable with simple generic types in rust, then simple functions with lifetimes, then you try to play with generics with lifetimes, as beginners tend to be easily confused when generics are combined with lifetimes. especially if you want to deal with more than one lifetime variables, it gets messy really quickly.

I would assume you come from C++ background, possibly a C++ game developer. just a reminder, I see a lot of return expr; at tail locations in your code, this is both unnecessary, and also non idiomatic in rust, a compound expression evaluates to the expression at the tail location.

anyway, back to the code, here's explanation for the compiler errors and the fixes:

since you add a lifetime generic parameter to EntitySystem, now EntitySystem without a lifetime argument is not a complete type, just like Vec is not a type, but Vec<u8> is. (although at times lifetime argument is not treated exactly the same as type argument).

so the impl block at L43 is incomplete, you can only impl for a type, but EntitySystem (withou lifetime arguments) is not a type, but if you give it a lifetime, it will be -- e.g. EntitySystem<'static> is a valid type, however, if you simply impl EntitySystem<'static> {}, the methods is only implemented for the specific type with 'static lifetime, not other lifetimes. to implement methods generically (i.e. for all lifetimes), you use lifetime variable, typically denoted 'a, but if you use it directly:

impl EntitySystem<'a> {}

the compiler will complain about undeclared lifetime, it's like "normal" variables, you must declare it before using it, and the syntax to declare a generic argument (not only lifetime argument, but also type argument) is impl<'a, 'b, T, U>. also, your constructor returns EntitySystem<'static> type, which means you can only ever create a object with EntitySystem<'static> type, not any other EntitySystem<'a> type, so you need to change it, too:

impl<'a> EntitySystem<'a> {
	pub fn new() -> EntitySystem<'a> {
		return EntitySystem {
			next_id: 0,
			entities: HashMap::new(),
			entity_names: HashMap::new(),
			components: HashMap::new(),
			texture: None,
		};
	}
	//...
}

then you will get another compiler message about the Texture type needs a lifetime argument, for the same reason Texture is not a complete type (without a lifetime argument), you can fix it easily now, since you already declared the 'a lifetime variable for the impl block:

	pub fn set_texture_component(&mut self, texture: Option<Texture<'a>>) {
		self.texture = texture;
	}

the last compiler error is at your main function:

at L137, you bind variable game_texture to the return value of texture_creator.load_texture(), game_texture now "borrows" texture_creator.

at L145, you move game_texture into es, you can think of ex now borrows texture_creator now.

the problem is, variables are dropped in the reverse order of the construction, so texture_creator declared at L135 will be dropped BEFORE es which is declared at L133.

simply reverse the order of the two should fix the compiler error.

	let texture_creator = canvas.texture_creator();
	let mut es = EntitySystem::new();

PS: because there are multiple return paths, manually drop the es variable is not practical.

another design advice: avoid two phase initialization (a.k.a. two stage construction), for example, don't construct an EntitySystem with an Option<Texture<'a>> intialized to None, then set_texture() outside the constructor. just pass the texture to the constructor and get rid of the Option wrapper. if you do that, you won't get the drop order problem, because of the data dependency, you can't declare es before texture_creator in the first place. as a bonus, you don't need to always check for None at runtime.

Same fix with @nerditation , but in diff mode.

--- a/src/entity_system.rs
+++ b/src/entity_system.rs
-pub struct EntitySystem {
+pub struct EntitySystem<'a> {
     next_id: u64,
     entities: HashMap<u64, String>,
     entity_names: HashMap<String, u64>,
     components: HashMap<TypeId, Box<dyn ComponentHashMap>>,
+    texture: Option<Texture<'a>>,
 }

-impl EntitySystem {
-    pub fn new() -> EntitySystem {
+impl<'a> EntitySystem<'a> {
+    pub fn new() -> EntitySystem<'a> {
         return EntitySystem {
             next_id: 0,
             entities: HashMap::new(),
             entity_names: HashMap::new(),
             components: HashMap::new(),
+            texture: None,
         };
     }

@@ -241,4 +245,8 @@ pub fn remove_component_from_entity<ComponentType: 'static>(

         return Err(format!("Unable to downcast ref to expected component type"));
     }
+
+    pub fn set_texture_component(&mut self, texture: Option<Texture<'a>>) {
+        self.texture = texture;
+    }
 }
--- a/src/main.rs
+++ b/src/main.rs
@@ -128,9 +128,6 @@ fn main() {
         }
     };

-    //Entity System setup
-    let mut es = EntitySystem::new();
-
     let texture_creator = canvas.texture_creator();

     let game_texture = match texture_creator.load_texture(Path::new("assets/bomb_party_v4.png")) {
@@ -141,6 +138,10 @@ fn main() {
         }
     };

+    //Entity System setup
+    let mut es = EntitySystem::new();
+    es.set_texture_component(Some(game_texture));

Advice:

  • if you have a custom fmt style config, put your rustfmt.toml in the project to let others use the same config, otherwise all code needs fmt changes and causes the mess
1 Like

WOW, thank you nerditation for the in depth explanations! You didn't just fix the code you explained how to fix it with clear explanations. I really appreciate it and learnt.

Also thanks Vague for the diff view and the rustfmt.toml tip!