Search code examples
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:

  • That states be implemented by trait.
  • That states be owned by the state machine.
  • That states be able to contain references to arbitrary non-static data with lifetime greater than that of the state machine.
  • That when a state is swapped out, the old box be still valid so that it can be moved into the constructor of the new state. This will allow the new state to switch back to the preceding state without requiring it to be re-constructed.
  • That a state can signal a state change by returning a new state from 'update'. The old state must be able to construct this new state within itself.

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