Today I’ve carried on working on the timer logic in my Ultima Online server implementation project. Previously the logic only displayed progress bars that incremented as each repetition of a timer was run. Next I needed to implement functionality to allow some arbitrary code to be run when a timer is executed. This is the whole point of having timers - execute some logic at a certain time or at certain intervals.

This journal post documents my exploration of storing a callback on the Timer struct to be called by the timer execution thread. The logic that runs on each execution will need to modify some game state e.g. a character’s hitpoints, so this post also explores how state can be passed to a callback property on a struct.

My first thought for how to implement the callback on the Timer struct was to set it as a callback property with the value being a closure. I wasn’t sure what type to give the property, so explored that first:

pub struct Timer {
    // snip
    callback: ????, // some sort of closure type?
}

Closures do not have concrete types that can be used in a struct definition, but there are three traits that closures can implement: Fn, FnOnce and FnMut.

To use one of these traits to specify the type of callback, a trait object would be necessary e.g.:

struct Timer {
    callback: Box<dyn Fn() -> ()>,
}

Using a FnOnce trait object for the callback

FnOnce is implemented by closures which can only be called once. Timer structs need to be able to have the callback called multiple times, once for each repetition. So using a FnOnce closure would therefore not work and we’ll get the error demonstrated in the example below:

struct Timer {
    callback: Box<dyn FnOnce() -> ()>,
}

fn main() {
    let timer = Timer {
        callback: Box::new(|| println!("Hello from callback")),
    };

    (timer.callback)();

    let timer_repetition = Timer {
        callback: timer.callback, // error[E0382]: use of moved value: `t.callback`
    };
}

Using a Fn trait object for the callback

Fn is implemented by closures which can be called multiple times, so the following will compile:

struct Timer {
    callback: Box<dyn Fn() -> ()>,
}

fn main() {
    let timer = Timer {
        callback: Box::new(|| println!("Hello from callback")),
    };

    (timer.callback)();

    let timer_repetition = Timer {
        callback: timer.callback,
    };

    (timer_repetition.callback)();
}

But to be able to operate on some state, the closure would need to either capture values from it’s environment or be supplied the state as arguments when executed.

Fn implementing closures can either do one of two things:

  • don’t move captured values out AND don’t mutate them, or
  • capture nothing from their environment.

We therefore can’t use state captured from the environment with a Fn closure, we’ll get an error like that in this example:

struct Timer {
    callback: Box<dyn Fn() -> ()>,
}

fn main() {
    let mut character = Character {
        hitpoints: 10,
    };

    let timer = Timer {
        callback: Box::new(move || character.hitpoints += 10),
        // error[E0594]: cannot assign to `character.hitpoints`, as it is
        // a captured variable in a `Fn` closure
    };
}

This leaves passing state as arguments to the callback closure, which is possible when using a Fn trait object:

struct Timer {
    state: Character,
    callback: Box<dyn Fn(&mut Character) -> ()>,
}

fn main() {
    let character = Character {
        hitpoints: 10,
        stamina: 100,
    };

    let timer = Timer {
        state: character,
        callback: Box::new(|character| character.hitpoints += 10),
    };

    let mut state = timer.state;
    (timer.callback)(&mut state);

    let timer_repetition = Timer {
        state,
        callback: timer.callback,
    };

    let mut state = timer_repetition.state;

    (timer_repetition.callback)(&mut state);
}

Each execution of the timer pulls out the state property from it and passes it to the callback.

The problem here is that Timer structs can now only operate on Character types. I could have multiple Timer struct definitions, one for each type of state they will operate on e.g. CharacterTimer but then specifying the types in the logic that creates the timers and executes them would then be very complicated, if not impossible.

I explored an alternative of setting the state type to be a trait object too:

struct Timer {
    state: Box<dyn State>,
    callback: Box<dyn Fn(&mut Box<dyn State>) -> ()>,
}

The State trait would categorise a struct that can have some state updated by a callback, for example through an update method:

trait State {
    fn update(&mut self) -> ();
}

Character would then needed to implement the trait:

impl State for Character {
    fn update(&mut self) {
      // update self
    }
}

You can’t update state without some extra arguments to update though. These arguments can’t be specific to the type that implements State, because then all types implementing State would need an update function that takes those arguments:

trait State {
    fn update(&mut self, hitpoints, stamina) -> ();
}

impl State for Character {
    fn update(&mut self, hitpoints, stamina) {
      self.hitpoints += hitpoints;
      self.stamina += stamina;
    }
}

impl State for TreasureChest { // A TreasureChest doesn't have "hitpoints" or "stamina"
    fn update(&mut self, gold) { // <-- type signature is incompatible with that of State
      self.gold += gold;
    }
}

Instead, update could take a more generic argument which provides information on the state property that needs updating and the delta to change it by:

struct StateDelta {
    property: String,
    delta: u8,
}

update could then take a vec of these structs:

trait State {
    fn update(&mut self, state_deltas: Vec<StateDelta>) -> ();
}

Implementing State for a Character would then look like this:

impl State for Character {
    fn update(&mut self, state_deltas: Vec<StateDelta>) {
        for state_delta in state_deltas {
            match state_delta.property.as_str() {
                "+hitpoints" => self.hitpoints += state_delta.delta,
                "-hitpoints" => self.hitpoints -= state_delta.delta,
                "+stamina" => self.stamina += state_delta.delta,
                "-stamina" => self.stamina -= state_delta.delta,
                _ => (),
            }
        }
    }
}

And then setting up the Timer that modifies the state of a Character:

fn main() {
    let character = Character {
        hitpoints: 10,
        stamina: 100,
    };

    let timer = Timer {
        state: Box::new(character),
        callback: Box::new(|character| {
            let state_deltas = vec![
                StateDelta {
                    property: String::from("+hitpoints"),
                    delta: 10,
                },
            ];
            character.update(state_deltas);
        }),
    };

    let mut state = timer.state;
    (timer.callback)(&mut state);

    // snip
}

This works, but storing the state separately on the Timer just to be able to pass it as an argument to callback and so satisfy the Fn trait seems unnecessary, when we could instead have the closure capture the state from its environment.

Using a FnMut trait object for the callback

To allow capturing state from the environment and mutating it, a FnMut trait object is needed instead:

struct Timer {
    callback: Box<dyn FnMut() -> ()>,
}

The state (a Character struct in this case) can then be captured by the closure and mutated. But just attempting to mutate the state in the callback after making it a FnMut trait object won’t work. An error relating to lifetimes will result as in this example:

fn main() {
    let mut character = Character {
        hitpoints: 10,
    };

    let mut timer = Timer {
        callback: Box::new(|| {
            character.hitpoints += 10;
            // error[E0597]: `character.hitpoints` does not live long enough

        }),
    };
} // - `character.hitpoints` dropped here while still borrowed

The problem is that the compiler is giving the trait object (dyn FnMut() -> ()) on the Timer struct a lifetime of 'static, meaning it treats the lifetime of the callback closure as potentially as long as the program. But the closure captures the hitpoints property from Timer using a mutable reference, and that is only valid for the lifetime of the Timer struct it belongs to. The Timer struct is dropped at the end of the main() function and so has a shorter lifetime than 'static. The solution is to provide the compiler more information by specifying a lifetime parameter on Timer - to tell it that the closure should only have a lifetime equal to that of the Timer struct it belongs to:

struct Timer<'a> {
    callback: Box<dyn FnMut() -> () + 'a>,
}

Now the code compiles and we can run repetitions of the timer too:

fn main() {
    let mut character = Character {
        hitpoints: 10,
    };

    let mut timer = Timer {
        callback: Box::new(|| {
            character.hitpoints += 10;
        }),
    };

    (timer.callback)();

    let mut timer_repetition = Timer {
        callback: timer.callback,
    };

    (timer_repetition.callback)();
}

Using a function pointer for the callback

I explored the alternative of using a function pointer for the callback. A function pointer to a function on a struct can be set as a property on another struct e.g.:

struct Timer {
    callback: fn(),
}

struct Character {}

impl Character {
    fn update() {}
}

fn main() {
    let timer = Timer {
        callback: Character::update,
    };
}

The update() function on Character in the example above does not take any arguments. In reality it would need to take a self argument so that it could update the Character instance. Adding a self parameter to update() means that the type for callback on Timer then needs to reflect that. The state also needs to be added as a property on Timer so it can be passed to the callback:

struct Timer {
    callback: fn(&Character),
    state: Character,
}

struct Character {}

impl Character {
    fn update(&self) {}
}

fn main() {

    let character = Character {};

    let timer = Timer {
        callback: Character::update,
        state: character,
    };

    (timer.callback)(&timer.state);
}

This compiles but now we have the same issue we saw before when passing state as arguments to a callback closure that implemented the Fn trait - that Timer would only work with Character structs and no other types of state.

To allow Timer to take callbacks which can operate on different types of state, a trait bound can be specified on Timer:

trait State {}

struct Character {}
struct Monster {}

impl State for Character {}
impl State for Monster {}

struct Timer<T: State> {
    state: Box<T>,
    callback: fn(Box<T>),
}

impl Character {
    fn update(self: Box<Self>) {}
}

impl Monster {
    fn update(self: Box<Self>) {}
}

fn main() {
    let character = Character {};

    let character_timer = Timer {
        state: Box::new(character),
        callback: Character::update,
    };

    (character_timer.callback)(character_timer.state);

    let monster_timer = Timer {
        state: Box::new(monster),
        callback: Monster::update,
    };

    (monster_timer.callback)(monster_timer.state);
}

Using T: State specifies that both the state and callback properties on Timer need to take a type that implements the State trait. Importantly it also means that for any particular instance of a Timer, the concrete type these two properties take must be the same i.e. you can’t set up a Timer with a Monster for state but Character::update as the callback. It ensures that whatever concrete type is set for state must also be the concrete type that callback accepts as an argument:

    let character = Character { hitpoints: 100 };

    let timer = Timer {
        state: Box::new(character),
        callback: Monster::update,
        // error[E0308]: mismatched types
        // = note: expected fn pointer `fn(Box<Character>)`
        //   found fn item `fn(Box<Monster>) {Monster::update}`
    };

Now we have the ability to pass the state instance to the callback and the callback can modify it. But we need to pass additional arguments so the callback knows how to modify the state, e.g. the integer amount to increase a character’s hitpoints by. The same approach as we took before with the Fn trait closure can be taken here - introducing a StateDelta type:

struct StateDelta {
    property: String,
    delta: u8,
}

struct Timer<T: State> {
    // snip
    callback: fn(Box<T>, Vec<StateDelta>),
}

impl Character {
    fn update(self: Box<Self>, state_deltas: Vec<StateDelta>) {}
}

// snip

fn main() {
    let character = Character {};

    let timer = Timer {
        state: Box::new(character),
        callback: Character::update,
    };

    let state_deltas = vec![
        StateDelta {
            property: String::from("+hitpoints"),
            delta: 10,
        },
    ];

    (timer.callback)(timer.state, state_deltas);
}

One thing to note here - StateDelta only allows a u8 type for the delta property. This only allows passing numeric deltas, but what if the state needs a String property updating? A string could be represented as an integer which would then need to be converted back to a string by the update() method on the state type, but I’m unsure if that will be true for all the different types that need to be updated on state types, because I’m not sure what they all are yet. I’ll revisit this later, if I decide to take this approach.

Wrap up

I’m not 100% sure which of the above approaches is best at this point.

In my next journal post I’ll explore passing the Timer struct between threads, since adding the callback property as in the code above will not compile at the moment when multiple threads are involved.

I’ll then explore how the state that is mutated by the callback can be used from elsewhere in the code in between calls to the callback, which isn’t possible with the code above since ownership of the state is either taken by the closure and not given back, or needs to be transferred back and forth between the closure and the execution thread on each repetition.

Allowing the state to be used by other code in between timer executions (and after the final execution) will be necessary. As an example: given a timer that increments hitpoints every few seconds, the updated hitpoints will need to be sent back to the game client after each execution so the client can display the correct hitpoints. Some mechanism is needed to allow ownership to be transferred elsewhere in between timer executions.

Exploring different approaches to solving these two issues next should guide me towards the best approach.