2

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.

jaco0646
  • 15,303
  • 7
  • 59
  • 83
Guy
  • 23
  • 3
  • *`Box`, making the boxes useless because states contain mutable state.* — this is not a valid conclusion. Having mutable state has nothing to do with the lifetimes contained within the trait object. – Shepmaster Nov 04 '17 at 13:44
  • Re: "I'm implementing states with a boxed trait object instead of an enum because I expect to have quite a lot of them" -- this is a non sequitur. The tradeoff between trait objects and sum types doesn't really change with the number of variants; it's more about what guarantees you want. (That's not to say I think you're wrong -- just nitpicking the phrasing a bit.) – trent Nov 04 '17 at 14:32
  • @Shepmaster you're right. I think what I should have said is that the state in the box holds borrowed data which has a lifetime less than `'static`. If the states contained owned simple immutable state I could define them statically. I'm still trying to understand lifetimes so this correction might be wrong too. – Guy Nov 04 '17 at 20:11
  • @trentcl The main tradeoff is that with enums I would have to match on all the possible kinds of states when I call the individual methods defined on AppState. So if there are, say, 20 states enumerated in AppState, and the public interface of AppState has 10 methods, I would need 10 matches on each of 20 states to static-dispatch to the appropriate state-specific method implementations. I may be missing something, but that was my understanding. edit: Although to be honest now that I think about it the simplicity of defining app states up front and statically is kind of appealing. – Guy Nov 04 '17 at 20:13
  • Sure, but you'd have to write all that behavior anyway, right? With trait objects it's just split among 20 different `impl`s with 10 methods apiece, instead of a single `impl` that contains 10 methods each with a 20-armed `match` expression. Not to mention that trait objects have a runtime cost and can't have methods that take `Self` or are generic over types. Trait objects are good when you can't or don't need to know which variant you're dealing with, but if you might need to [downcast](https://stackoverflow.com/q/40024093/3650362) to a concrete type, you'd be better served by an enum. – trent Nov 04 '17 at 20:53
  • @trentcl That's true. I was mainly hoping to avoid all the arms of the match expression. I'm not worried about the speed overhead of dynamic dispatch in my current project. However, my design has a critical flaw I just found: most states hold ownership of important mutable API objects like the video canvas, etc. However, switching states by returning a new state requires creating the new state inside of `update` and moving the app data into it. I actually can't do this, since i'd be moving out of the active state. So enums are looking more appealing. – Guy Nov 04 '17 at 21:33
  • @trentcl I mean, I guess I could use an Rc> to hold the mutable app data, but the overhead of that is not better than a big match expression (compared to all of the `borrow()` and `borrow_mut()`s that will be spread throughout AppState impls when using Rc>). With enums I could define explicit transitions. – Guy Nov 04 '17 at 21:36

1 Answers1

2

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();
trent
  • 25,033
  • 7
  • 51
  • 90
Francis Gagné
  • 60,274
  • 7
  • 180
  • 155
  • Excellent, thanks a ton! Your change fixed it and your explanation is quite good. It looks like I need to read up more on variance. I've seen that page before and I was struggling with it earlier today, but I didn't realize it was related to this issue. – Guy Nov 04 '17 at 05:55