OO architecture and the borrow checker

Hello everybody,

So I decided to start learning Rust, and because I like games, I thought it would be nice to try coding a text-based one. However, everytime I try something new, I always end fighting the borrow checker. My guess is that I'm structuring my code around an object-oriented architecture (because that's what I'm used to), and Rust has not been designed specifically for that.

I created a simplified version of my project that you can find here: Rust Playground.

I want the player to interact with the game by typing commands, like "go north", "eat apple", etc...

To accomplish this task, my game has a list of commands, that can receive extra arguments, (handled by what I called selectors.

These selectors return Selectable, which group everything that can be targetted by a command, such as a creature, an item, a direction, or even another command.

The main problem is that my commands and selectors need to have access to the game to be able to modify the world, or gather commands for instance. And then I start having troubles with references and lifetimes.

So, my question is somewhat general, but wat is wrong with my design, and what are your thoughts to organize such a project? I hope that the code I provided will help you understand my problem, because most of the questions I've read show very small code that doesn't do anything, so the answers are not really applicable. Are there any patterns I'm not aware of? Should I use Rc, clone or something else to get all the pieces communicate?

Thanks in advance for those of you who will read through my code and give me insights.

Kind regards,

Arnold

The problem is, that you are thinking in OOP, but rust is not OOPish. If you want to stick with your OO approach, you may emulate it by Cell, but I would suggest you to redesign it, so commands are executed and mutating state in very specific place, and calling command typed from console, just adds them to some kind of queue. This way, there is only one place which actually mutates game state. Obviously you need some kind of queue to store called but not-yet-executed commands, and the easiest solution would be just Vector wrapped with Mutex, and when you will feel, that the mutex is too much overhead, then switch to some non-blocking implementation like mpsc, or other.

Anyway I also recommend to try ECS solution for game - Rust has very good ECS implementation called Specs, and it would let you avoid problems like that in the future.

4 Likes

A few additional thoughts. Approaches that could also lead to a design mistake.

If game holds the items and the items have a reference to game, you create a cyclic data structure. Usually, for such a task a garbage collector is used. Without a garbage collector, a first approach could be Rc<RefCell<T>>. But then you need to reason yourself, whether the cycles will be broken in order to prevent memory leaks. If the items hold no ownership, instead of Rc a pointer of type Weak<RefCell<T>> can be used. Note that only some parts of Game should be behind a RefCell<T>. Otherwise borrow() or borrow_mut() will refuse with a runtime panic because of dynamic borrow aliasing (better stop the world than reaching undefined behavior).

Another approach is an argument game: &mut Game for every function, that's right. But if you want references of substructures of Game, you will quickly run into compile time borrow aliasing issues. If you think your're smarter than the borrow checker, use raw pointers and some unsafe blocks. But I believe, using unsafe so early will result in an utterly complex non-solution.

Another idea would be to use a HashMap<K,V> with unique id's of type K instead of pointers, maybe in combination with runtime polymorphic objects (V=Box<dyn Trait>, V=Box<dyn Any>). But such a solution might be too slow.

1 Like

+1 to using specs or some other ECS with rust. It makes architecture a lot easier if you have lots of game objects your siming.

You might also be able to get by with carving off a piece of the game struct and passing that to selectables or RefCells on the game objects. I was able to do that for a bit with my first project but switched to specs because it ended up being too painful.

Thank you all for pointing me available options. Initially I decided not to use an Entity Component System to learn Rust the way I could use it outside of game development. But because it seems to be the perfect tool, I will give Specs a try, and see what I can come up with.
Maybe we are lacking tutorials for people coming from OOP background. I've read the official book two times or so, but I feel that I still don't think the Rust Way.
I've read a bit about non-lexical lifetimes, and tried using it (which resulted in some errors going away). Does it mean that these types of scenarios will be easier to handle when this feature will be released?

Not for most of the things which you are most likely trying to do from a OOP mindset. It fixes some edge cases in the language that feel particularly dumb (e.g. where they can be "fixed" just by adding some braces... or other annoyances like the fact that you can't use option in the else branch of if let Some(x) = option.as_mut()), but these are just the things that many of us still run into after learning "the Rust way."

I'm not sure how best to bridge the gap. For me, I think the transition began to happen back when I was using Python where arrays of objects can be really slow compared to objects of arrays (which can use numpy). This made me start programming in a more... I guess you might call it "data-oriented" manner, where things are grouped according to when they are needed. I later also experimented with pure functional languages like Haskell. So when I found rust, I was already thinking in largely the right mindset, and only needed a small kick to drop my last remaining few OOP tendencies (like keeping around "parent object" references).

I don't think there's anything really wrong with that approach, I've found that you can often reuse a lot of existing OO patterns or ideas in Rust by giving them a bit of a functional twist.

It sounds like you're having issues with lifetimes because a lot of objects are referencing other objects without a clear ownership hierarchy. That sort of thing is fairly common in GC'd languages like C# and Java, but starts to fall apart once you remove the garbage collector and need to manage memory youself.

I'm not sure of the scale of your game, but it almost sounds like things are too abstract and OO-ish. For example, a Command could just be an enum where each possible action is a variant and it contains all the information necessary to execute that Command (removing the need for the Selector/Selectable pair). Then there's a parse() function to turn a line of text into a Command using World as context, with the Game executing each Command using a simple match statement and delegating to the appropriate method/function for that action.

There are times when Rc and RefCell are useful, but normally when I reach for Rc<Refcell<T>> it's an indicator that my system is getting coupled or the ownership hierarchy is getting a little fuzzy.


Another way of approaching this sort of problem that I've been playing around with recently is an Actor system. The actix library is quite amazing and would be great for this sort of application. I made an IRC bot using actix and really enjoyed it, plus there's a talk on YouTube by a guy who made an actor-based Sim City game in Rust if you want to hear how one would implement a game in this way.

2 Likes