Data Modelling in Rust Continued

Intro

So, just after my previous blog posting about data modelling in Rust I started looking at GhostCell (and corresponding crate) and how to use that for my game example. It's an extremely simple data type that works similarly to RefCell, but all borrow checks are performed at compile time and thus avoids all runtime overhead. It achieves this by separating the cells from the actual borrowing permission. When borrowing a reference to the interior of a cell you need a reference to a token connected to the cell. The cell and token are connected using a common lifetime parameter. Truly ingenious stuff!

Solution 4: Reference Counted GhostCell's

For my example it's probably most convenient to use GhostCell in combination with Rc. The reference types now looks like this:

struct Ref<'brand, T>(Rc<GhostCell<'brand, T>>);
type WRef<'brand, T> = Weak<GhostCell<'brand, T>>;

type PlayerRef<'brand> = Ref<'brand, Player<'brand>>;
type PlayerWRef<'brand> = WRef<'brand, Player<'brand>>;

type GameRef<'brand> = Ref<'brand, Game<'brand>>;
type GameWRef<'brand> = WRef<'brand, Game<'brand>>;

Note the added lifetime parameter 'brand which is the connection between a GhostCell and corresponding GhostToken. I also wrapped the Rc type to be able to add some methods that makes it easier to use.

Here's the usage example:

    GhostToken::new(|mut token| {
        let game = Ref::new(Default::default());
        let t = &mut token;

        let p1 = create_player(t, &game, "Eric", 10);
        let p2 = create_player(t, &game, "Tom", 15);
        let p3 = create_player(t, &game, "Carl", 17);

        make_friends(t, &p1, &p2);
        make_friends(t, &p1, &p3);

        p2.borrow_mut(t).health = 20;

        for x in p1.borrow(t).friends.iter() {
            println!(
                "{}: {}",
                x.upgrade().unwrap().borrow(t).name,
                x.upgrade().unwrap().borrow(t).health
            )
        }
    });

Very similar to the RefCell example except we need to create a new GhostToken that is used to borrow the contents of a GhostCell. The big advantage is that there is zero runtime overhead to plain Rc and the borrowing can never cause a runtime panic!

Solution 5: Pooled GhostCell's

It's also possible to combine GhostCell with object pooling to avoid the overhead of Rc. This requires adding a another lifetime parameter for the pooled objects (I couldn't seem to make it work with a 'static lifetime). Here are the reference types:

struct GCell<'brand, T>(GhostCell<'brand, T>);
type Ref<'t, 'brand, T> = &'t GCell<'brand, T>;
type PlayerRef<'t, 'brand> = Ref<'t, 'brand, Player<'t, 'brand>>;
type GameRef<'t, 'brand> = Ref<'t, 'brand, Game<'t, 'brand>>;

I needed to wrap the GhostCell to implement the Default trait which is required for the object pooling.

The usage example is very similar except I need to create and pass a player object pool:

let players = CellPool::new(100);
let game = GCell::new(Game { players: &players });

This solution has virtually no runtime overhead over using plain Rust references, and, as with RefCell, no additional interior mutability is required in the game data types. Sweet!

Conclusion

GhostCell is an awesome addition to the Rust toolbox. It's a bit more cumbersome to use than RefCell because of the added lifetime, but I think the benefits of static borrow checking are worth it in almost all cases.

It just continues to amaze me what a powerful and flexible Rust is for creating your own safe data structures and memory management techniques! At first I was sceptical about the practicality of modelling data like this example in safe Rust, but now I've realized that it's not only practically feasible, but you also have awesome control over performance and memory characteristics while still maintaining memory safety!

Comments

Popular posts from this blog

Rust for the Seasoned Scala Developer

Data Modelling in Rust