What is great in Rust — by example
By: Masahiro Yoshida
Tags: Rustlang, Debugging, Game Development, Software Development

Photo by Kirill Sharkovski on Unsplash

One day, in a game development studio.

Debugger: Sometimes this app crashes.

Engineer: Oh, do you know how to make it crash?

Debugger: I don’t know exactry, but users say it crashes in some battle scenes.

Engineer: Umm.. I can’t investigate without knowing the exact way to make it crash.

Debugger: Umm..

and time spends…

When developing games by C++, I have experienced such a case so many times. This kind of problem means that some part of memory is corrupted when the game is in particular situation. But it’s very hard to investigate where the memory corruption occures. Because nobody knows when and from where a memory field is modified.

Ok then, let the compiler know the memory changing flow. That is, Rust.

Memory corruption example — in C++

for example, let’s write a program implimenting ‘Summon a new character’.

This is GameObject class. which replesents any object in the game. (Hero, Enemy, etc.)

////////////////////////////////////////// game_object.hpp #pragma once class GameObjectManager; class GameObject {

public:



GameObject();



//will be called each frame.

void Update(GameObjectManager* manager);



bool CanSummon();

}; ///////////////////////////////////////// game_object.cpp #include "game_object.hpp"

#include "game_object_manager.hpp"



GameObject::GameObject()

{

}



void GameObject::Update(GameObjectManager* manager)

{

//when i can summon. do summon.

if(CanSummon()) {

manager->SummonObject();

}

}



bool GameObject::CanSummon() {

//some logic for check if this object can summon.

return true;

}

GameObjectManager class. which manages all GameObject s in the game.

/////////////////////////////////////// game_object_manager.hpp

#pragma once



#include "game_object.hpp"

#include <vector>



class GameObjectManager {

private:

std::vector<GameObject> _game_objects;

public:



GameObjectManager();



//will be called every frame.

void Update();



void SummonObject();



}; /////////////////////////////////////// game_object_manager.cpp

#include "game_object_manager.hpp"



GameObjectManager::GameObjectManager()

{

}



void GameObjectManager::Update() {

//update all objects

for(GameObject& obj : _game_objects) {

obj.Update(this);

}

}



void GameObjectManager::SummonObject() {

//create new GameObject, and add to the vector.

_game_objects.emplace_back(GameObject());

}

This is a tipical design of Object Oriented Programming. One game character (GameObject) desides to summon, then call SummonObject method via GameObjectManager , the Manager creates a new GameObject and add to _game_objects . perfect!

However, this code includes a critical bug. When number of character increases by summoning. and some time spent, this program crashes by memory corruption.

What is happening? Let’s see the program flow carefully.

When GameObjectManager::Update() called, for each object in _game_objects , GameObject::update() is called. And in some condition the object summons, and a new GameObject is added to _game_objects .

So it's adding a new object to _game_objects in _game_objects loop! C++ vector initially reserves some length of memory, but when it requires more than reserved, vector will allocate a new enough memory, and moves all data to new allocated memory.

When _game_objects memory’s reallocation occures in SummonObject() , the iterator that is used in GameObjectManager::Update() will no longer point valid memory address. If the iterator points some memory used by other part of the program, that begins unexpected behaviour, and will crash in worst case.

The problem is, it’s hard to be aware of this kind of bug for even experienced programmer.

Let’s make it Rust!

Now, let’s write same program in Rust. With detailed comment for beginner.

//Rust has no class, only struct

struct GameObject {

//some stuff here

}



//implement GameObject

impl GameObject {



//`self` is `this` in C++

//`&` means reference.

//`mut` means mutable.

//`&mut self` means this method will mutate self data.

//We must make GameObjectManager `mut` because

// `summon_object()` will mutate GameObjectManager.

fn update(

&mut self,

manager: &mut GameObjectManager

) {

if self.can_summon(){

manager.summon_object();

}

}



//check self can summon.

//need no `mut` because this will not mutate self data.

fn can_summon(&self) -> bool {

//check self status if this can summon.

true //without `;`, means `return true;`

}

}



struct GameObjectManager {

//`Vec` is `std::vector` in C++

game_objects: Vec<GameObject>

}



impl GameObjectManager {



//requires `&mut self` to mutate self data.

fn update(&mut self) {



//call each objects `update()`.

for o in &mut self.game_objects {

//o is mutable reference for each `GameObject`.

o.update(self);

}

}



//summon

//requires `&mut self` to mutate game_objects.

fn summon_object(&mut self) {

self.game_objects.push(GameObject{});

}

}

You see mut keyword, which must be unfamiliar with beginners. mut is shortende form of mutable, means “I will change this variable”. You may think it’s like opposite of C++’s const , but It’s not true.

I will not explain the detail of mut , but important thing here is, Rust’s compiler knows variable’s mutabilities, and check always if danger mutation is occured.

Now, compiling this code. we get following error message.

error[E0499]: cannot borrow `*self` as mutable more than once at a time

--> src/main.rs:27:22

|

26 | for o in &mut self.game_objects {

| ----------------------

| |

| first mutable borrow occurs here

| first borrow used here, in later iteration of loop

27 | o.update(self);

| ^^^^ second mutable borrow occurs here



error: aborting due to previous error



For more information about this error, try `rustc --explain E0499`.

The compiler complains that GameObjectManager ’s mutable reference(borrow) occures two times, one is the start of the itaration for GameObjectManager::game_objects , another is passing reference to GameObject::update() in the iteration loop.

From the Rust document, here is the rule of borrowing.

You may have one or the other of these two kinds of borrows, but not both at the same time:

- one or more references ( &T ) to a resource,

- exactly one mutable reference ( &mut T ).

In other words, while a memory is being written by one part, no other part can read or write that memory. And the memory can be read from anywhere at the same time when no other part is writing.

Rust guarantees memory safety by this restriction.

So this program can not compile. Then what to do? The point of this problem is that we intend to mutate GameObjectManager from GameObject which is managed by GameObjectManager . Actually the above C++ program with same design causes crash.

As you see, rust can detect this kind of problem at compile time! It’s obvious that investigating compile error is easier than runtime error. Great rust!

Conclusion

This is why I love and recommend Rust. Learning rust, I think why C++ is so free to memory management and access. It’s like crossroad with no traffic signals. It’s so fearful to drive.

Even if you don’t have a chance to write rust in product, lerning rust will make you write codes considering more memory safety and efficiency.