This question is about a specific pattern of ownership that may arise when implementing a state machine for a video game in Rust, where states can hold a reference to "global" borrowed context and where state machines own their states. I've tried to cut out as many details as I can while still motivating the problem, but it's a fairly large and tangled issue.
Here is the state trait:
pub trait AppState<'a> {
fn update(&mut self, Duration) -> Option<Box<AppState<'a> + 'a>>;
fn enter(&mut self, Box<AppState<'a> + 'a>);
//a number of other methods
}
I'm implementing states with a boxed trait object instead of an enum because I expect to have quite a lot of them. States return a Some(State)
in their update method in order to cause their owning state machine to switch to a new state. I added a lifetime parameter because without it, the compiler was generating boxes with type: Box<AppState + 'static>
, making the boxes useless because states contain mutable state.
Speaking of state machines, here it is:
pub struct StateMachine<'s> {
current_state: Box<AppState<'s> + 's>,
}
impl<'s> StateMachine<'s> {
pub fn switch_state(&'s mut self, new_state: Box<AppState<'s> + 's>) -> Box<AppState<'s> + 's> {
mem::replace(&mut self.current_state, new_state);
}
}
A state machine always has a valid state. By default, it starts with a Box<NullState>
, which is a state that does nothing. I have omitted NullState
for brevity. By itself, this seems to compile fine.
The InGame
state is designed to implement a basic gameplay scenario:
type TexCreator = TextureCreator<WindowContext>;
pub struct InGame<'tc> {
app: AppControl,
tex_creator: &'tc TexCreator,
tileset: Tileset<'tc>,
}
impl<'tc> InGame<'tc> {
pub fn new(app: AppControl, tex_creator: &'tc TexCreator) -> InGame<'tc> {
// ... load tileset ...
InGame {
app,
tex_creator,
tileset,
}
}
}
This game depends on Rust SDL2. This particular set of bindings requires that textures be created by a TextureCreator
, and that the textures not outlive their creator. Texture requires a lifetime parameter to ensure this. Tileset
holds a texture and therefore exports this requirement. This means that I cannot store a TextureCreator
within the state itself (though I'd like to), since a mutably-borrowed InGame
could have texture creator moved out. Therefore, the texture creator is owned in main
, where a reference to it is passed to when we create our main state:
fn main() {
let app_control = // ...
let tex_creator = // ...
let in_game = Box::new(states::InGame::new(app_control, &tex_creator));
let state_machine = states::StateMachine::new();
state_machine.switch_state(in_game);
}
I feel this program should be valid, because I have ensured that tex_creator
outlives any possible state, and that state machine is the least long-lived variable. However, I get the following error:
error[E0597]: `state_machine` does not live long enough
--> src\main.rs:46:1
|
39 | state_machine.switch_state( in_game );
| ------------- borrow occurs here
...
46 | }
| ^ `state_machine` dropped here while still borrowed
|
= note: values in a scope are dropped in the opposite order they are created
This doesn't make sense to me, because state_machine
is only borrowed by the method invocation, but the compiler is saying that it's still borrowed when the method is over. I wish it let me trace who the borrower in the error message--I don't understand why the borrow isn't returned when the method returns.
Essentially, I want the following:
Are these constraints possible to satisfy, and if so, how?
I apologize for the long-winded question and the likelihood that I've missed something obvious, as there are a number of decisions made in the implementation above where I'm not confident I understand the semantics of the lifetimes. I've tried to search for examples of this pattern online, but it seems a lot more complicated and constrained than the toy examples I've seen.
In StateMachine::switch_state
, you don't want to use the 's
lifetime on &mut self
; 's
represents the lifetime of resources borrowed by a state, not the lifetime of the state machine. Notice that by doing that, the type of self
ends up with 's
twice: the full type is &'s mut StateMachine<'s>
; you only need to use 's
on StateMachine
, not on the reference.
In a mutable reference (&'a mut T
), T
is invariant, hence 's
is invariant too. This means that the compiler considers that the state machine has the same lifetime as whatever it borrows. Therefore, after calling switch_state
, the compiler considers that the state machine ends up borrowing itself.
In short, change &'s mut self
to &mut self
:
impl<'s> StateMachine<'s> {
pub fn switch_state(&mut self, new_state: Box<AppState<'s> + 's>) -> Box<AppState<'s> + 's> {
mem::replace(&mut self.current_state, new_state)
}
}
You also need to declare state_machine
in main
as mutable:
let mut state_machine = states::StateMachine::new();