ruststate-machinelifetimeownership-semantics

Getting around Rust ownership problems when using state machine pattern


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.


Solution

  • 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();