I am a Java, C#, C or C++ developer, time to do some Rust
As I've said before, I'm working on a book about lifetimes. Or maybe it's just a long series - I haven't decided the specifics yet. Like every one of my series/book things, it's long, and it starts you off way in the periphery of the subject, and takes a lot of detours to get there.
In other words - it's great if you want an adventure (which truly understanding Rust definitely is), but it's not the best if you are currently on the puzzled end of a conversation with your neighborhood lifetime enforcer, the Rust compiler.
So, let's try to tackle that the crux of the issue another way - hopefully a more direct one.
I want to build an app
Let's say I want to build an app. Let's also say maybe I have some experience with another language - maybe it's Java, maybe it's C#, maybe it's C or C++, or maybe something else entirely.
$ cargo new an-app Created binary (application) `some-app` package
Let's say it's a graphical app, which has a window, and the window should have a size. I could just do something like this:
// in `src/main.rs` const WIDTH: usize = 1280; const HEIGHT: usize = 720; fn main() { println!("Should make a {}x{} window.", WIDTH, HEIGHT); }
That would do the job:
$ cargo run --quiet Should make a 1280x720 window.
But if I've learned one thing being a Java/C#/C/C++/etc. developer, it's that globals are bad, and I shouldn't use them.
So instead - since I'm making an app - I can make an App
struct:
struct App { width: usize, height: usize, } fn main() { let app = App { width: 1280, height: 720, }; println!("Should make a {}x{} window.", app.width, app.height); }
This instantly feels a lot better. It's nicer to look at - and it groups together related values. It's not just "two globals" floating around the source code, it's part of the app - the app has a window size.
Let's also say the app's window will need its own title, because I feel strongly that people should use my app windowed, and not fullscreen - so they'll definitely see the titlebar.
So, I look up rust string
and I find out that there is a type named
String
- this feels familiar. Java also has a type named String
.
So does C# - it even has the string
keyword as an alias to it.
C has, uh, unresolved issues, and C++ has a whole bunch of string types.
struct App { width: usize, height: usize, title: String, } fn main() { let app = App { width: 1280, height: 720, title: "My app", }; println!( "Should make a {}x{} window with title {}", app.width, app.height, app.title ); }
Unfortunately, it doesn't work:
cargo check --quiet error[E0308]: mismatched types --> src/main.rs:11:16 | 11 | title: "My app", | ^^^^^^^^ | | | expected struct `std::string::String`, found `&str` | help: try using a conversion method: `"My app".to_string()` error: aborting due to previous error
But I'm not deterred, because while I was putting off learning Rust, I've heard lots of people say: "you'll see, it's hard, but the compiler has got your back, so just go with the flow".
So, I just trust the compiler, and I do what it wants me to do:
let app = App { width: 1280, height: 720, // new: title: "My app".to_string(), }; }
$ cargo run --quiet Should make a 1280x720 window with title My app
So far, so good.
At this point in my process of building the app, I realize that now we have three fields, and two of them really seem like they belong together: the app I'm making is going to have lots of graphical elements, some of which are definitely going to have a width and a height.
Since making my first struct went okay, I just make another one:
struct App { dimensions: Dimensions, title: String, } struct Dimensions { width: usize, height: usize, } fn main() { let app = App { dimensions: Dimensions { width: 1280, height: 720, }, title: "My app".to_string(), }; println!( "Should make a {}x{} window with title {}", app.dimensions.width, app.dimensions.height, app.title ); }
Now, keep in mind this is my first Rust project. A graphical application.
Because if I'm starting to learn Rust, it might as well involve a project I'm actually interested in. And I know it sounds ambitious, but I've definitely made a bunch of graphical applications before, in Java, C#, C or C++, so it's "just" a matter of figuring out the little differences Rust has, which should be no trouble at all.
If I was more advanced in Rust, I might be tempted to implement the Display
or the Debug
trait on Dimensions
, because I expect a lot of print
debugging, and the println!
line is getting a bit long.
But right now, I'm blissfully unaware what a trait even is (can't they just call it a class? or an interface?), so I'm sticking with that code, which has the merit of being very explicit about what it does.
Now for the app itself
Although I'm happy that I got over my first compiler error, there is a lot to do. I'm able to actually create a window pretty quickly (omitted here for brevity) by looking up a "crate" (can't they just call it a library?) that does that, and adapting the code from its README.
I have to say, cargo
is nice. I don't know how I feel about rustc
yet,
but cargo
is nice. I sure wish Java, C#, C, or C++ had something like that.
Don't they all have something like that?
They have some things, yes, but not like that.
But now that I have a window up and running, I'm starting to think about the logic of my application. How will it work?
If this was Java, C#, C or C++, I know exactly what I would do. I would have
App
take care of window creation, maybe keyboard, mouse and gamepad input,
just all your generic, run-of-the-mill setup and bookkeeping operations, and
then I would have all the project-specific logic somewhere else, in another
class (or struct).
I've done that tons of times before. In fact, I already have a framework for doing that, that I've written myself - in Java, C#, C or C++, that lets me skip the boring setup and bookkeeping part, and concentrate on the logic.
In my Java, C# or C++ framework, I have a base class, Client
, which has
methods like update
and render
- they do nothing by default. But when I
subclass it, for each of my projects, I just need to override those update
and render
methods to do what the project actually needs to do.
And my App
class, well - I re-use that one everywhere. It contains a
reference (Java/C#) or a pointer (C++), or a pointer to the data + a pointer
to a struct full of function pointers (C), and whenever the App
decides
it's time to update
, it calls this.client.update(...)
, and it ends
up using Project47::update
- which has the logic for project 47, rather
than Client::update
, which just does nothing.
Aren't you going to include code examples in Java, C#, C or C++?
No, I'm not - because either the reader knows exactly what I'm talking about, from years of having to write Java, C#, C, or C++, or they don't, in which case their mind is fresh, and they should be reading some other article, that approaches that problem from another angle.
So, what, should they just stop reading that article? They're already 6 minutes in.
Not necessarily - I will show Rust code that "simulates" how things would work if they were writing Java, C#, C or C++.
So.
With my prior experience in mind, I do a little bit of research to see how I could achieve the same thing, but in Rust. I'm not particularly pleased to discover that Rust does not have classes.
That means I now have two things to learn: lifetimes (which Rust advocates have been very vocal about), and traits.
Since this all seems intimidating, before I move on to building a framework
so that I can make graphical apps without all the boilerplate, I try to
do it the simple way - by just stuffing logic directly into the App
.
I also decide that my first graphical app will actually not be graphical, it will simply output lines to the terminal, so that for the time being, I don't have to worry about whether I should use OpenGL, or Vulkan, or wgpu, or maybe I should make my own abstraction top of Metal for the macOS/iOS builds, I really like DirectX 12 though, maybe there's a translation layer from that to something else?
I'll worry about that later - for now I comment out the windowing code, and just focus on making a simple app that works in the terminal.
It'll be a "jack in the box" game - where you turn and turn and turn the crank, and then Jack just POPS OUT.
Using my prior knowledge, I write what seems like it should work, given my prior knowledge of Java, C# or C++ (it would be a bit more involved in C):
// THE FOLLOWING CODE DOES NOT COMPILE // (and is overall quite different from valid Rust code) use std::{time::Duration, thread::sleep}; fn main() { let app = App { title: "Jack in the box".to_string(), ticks_left: 4, running: true, }; println!("=== You are now playing {} ===", app.title); loop { app.update(); app.render(); if !app.running { break; } sleep(Duration::from_secs(1)); } } struct App { title: String, ticks_left: usize, running: bool, fn update() { this.ticks_left -= 1; if this.ticks_left == 0 { this.running = false; } } fn render() { if this.ticks_left > 0 { println!("You turn the crank..."); } else { println!("Jack POPS OUT OF THE BOX"); } } }
This doesn't work at all. The Rust compiler is angry at me for a bunch of reasons. Or maybe it's just disappointed.
$ cargo check --quiet error: expected identifier, found keyword `fn` --> src/main.rs:28:5 | 28 | fn update() { | ^^ expected identifier, found keyword error: expected `:`, found `update` --> src/main.rs:28:8 | 28 | fn update() { | ^^^^^^ expected `:` error[E0599]: no method named `update` found for struct `App` in the current scope --> src/main.rs:13:13 | 13 | app.update(); | ^^^^^^ method not found in `App` ... 23 | struct App { | ---------- method `update` not found for this error[E0599]: no method named `render` found for struct `App` in the current scope --> src/main.rs:14:13 | 14 | app.render(); | ^^^^^^ method not found in `App` ... 23 | struct App { | ---------- method `render` not found for this error: aborting due to 4 previous errors
Forced to go Back Online to research some of this, I discover that unlike in
Java, C#, or C++, I can't just implement methods in the same block as the
struct
class
block that contains the fields.
Weird choice, but okay - apparently what I need is an impl
block:
struct App { title: String, ticks_left: usize, running: bool, } impl App { fn update() { this.ticks_left -= 1; if this.ticks_left == 0 { this.running = false; } } fn render() { if this.ticks_left > 0 { println!("You turn the crank..."); } else { println!("Jack POPS OUT OF THE BOX"); } } }
This is apparently not enough to make my example work (it looks good to me, though) - now it's complaining about this
:
cargo check --quiet error[E0425]: cannot find value `this` in this scope --> src/main.rs:31:9 | 31 | this.ticks_left -= 1; | ^^^^ not found in this scope (cut)
A few google searches later, I learn that, unlike in Java, C# or C++, there
is no concept of "static" and "non-static" methods. (The static
keyword
does exist, but it only has one of the meanings it has in C++).
However, update
and render
as I've written them are actually the closest
thing to a "static method". I learn that, in Rust, there is no implicit
this
pointer.
Every fn item
(function) of an impl
block has to declare all its inputs,
and if we want something like this
, which Rust calls the "receiver", we also
need to spell it out.
Also, it's spelled self
, not this
.
If you don't have a receiver as the first parmeter, then it's not a "method", it's an "associated function", which you can call like that:
let app = App { /* ... */ }; loop { // over here: App::update(); App::render(); if !app.running { break; } sleep(Duration::from_secs(1)); }
...but then it can't access any of the fields of app
, the App
instance we
initialized just above.
So, if we want a thing like this
, we need to add self
explicitly:
struct App { title: String, ticks_left: usize, running: bool, } impl App { fn update(self) { self.ticks_left -= 1; if self.ticks_left == 0 { self.running = false; } } fn render(self) { if self.ticks_left > 0 { println!("You turn the crank..."); } else { println!("Jack POPS OUT OF THE BOX"); } } }
There are now no errors left in that part of the code.
Since I've changed app.update()
to App::update()
to show off "associated
functions", it is now complaining about my loop:
$ cargo check --quiet error[E0061]: this function takes 1 argument but 0 arguments were supplied --> src/main.rs:13:9 | 13 | App::update(); | ^^^^^^^^^^^-- supplied 0 arguments | | | expected 1 argument ... 30 | fn update(self) { | --------------- defined here error[E0061]: this function takes 1 argument but 0 arguments were supplied --> src/main.rs:14:9 | 14 | App::render(); | ^^^^^^^^^^^-- supplied 0 arguments | | | expected 1 argument ... 34 | fn render(self) { | --------------- defined here
And as I see those diagnostics, it cements in my mind that the receiver - in that
case, self
- really is just a regular argument.
Which I can pass myself, if I want to:
loop { App::update(app); App::render(app); if !app.running { break; } sleep(Duration::from_secs(1)); }
Or, I can just use the method call syntax, which is what I wanted to do in the first place:
loop { app.update(); app.render(); if !app.running { break; } sleep(Duration::from_secs(1)); }
Now, according to my Java or C# experience, I might be thinking that this would be enough - that it would just work. But it doesn't.
$ cargo check --quiet error[E0382]: use of moved value: `app` --> src/main.rs:14:9 | 4 | let app = App { | --- move occurs because `app` has type `App`, which does not implement the `Copy` trait ... 13 | app.update(); | --- value moved here 14 | app.render(); | ^^^ value used here after move (cut)
According to my C++ experience however, I'm feeling uneasy about the whole thing. Java and C# have "reference semantics" for classes (and we're really trying our best to make something that feels like a class), so similar code in those languages definitely ought to work.
But in C++, that's not the case. In C++, we know that if we just try to pass an instance of a class, it can either be "moved", or it can be "copied", depending on choices we make.
By looking up some more documentation on methods and receivers, I finally find out that this:
impl App { fn update(self) { // ... } }
Is really a shorthand for this:
impl App { fn update(self: App) { // ... } }
This once again reinforces the notion that self
, although special in that
it allows using "method call syntax", is really just a regular parameter.
And looking at the compiler's error again, I can see that having a parameter
of type self: App
will definitely do... the same as in C++: either it will
move (as it does here), or it will be copied - which it would be, if our type
"implemented the Copy
trait", whatever that means.
Anyway, regardless of whether I'm using my C# or Java experience and doing
a bit of catching up on reference semantics vs copy semantics, or if I use
my C++ instinct to come to the same conclusion, I end up realizing that
I don't want to get an App
, I want to get a reference to an App
.
Something like that:
impl App { fn update(self: &App) { // ... } }
Except, as I'm just now learning, there is also a shorthand for that, and it's:
impl App { fn update(&self) { // ... } }
So I use it, for both fn update
and fn render
.
$ cargo check --quiet error[E0594]: cannot assign to `self.ticks_left` which is behind a `&` reference --> src/main.rs:31:9 | 30 | fn update(&self) { | ----- help: consider changing this to be a mutable reference: `&mut self` 31 | self.ticks_left -= 1; | ^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be written (cut)
The error count has steeply decreased - and we're back in "the Rust compiler is giving me good advice" territory, so I'm finally starting to get some confidence back, and just follow the directions:
impl App { fn update(&mut self) { self.ticks_left -= 1; if self.ticks_left == 0 { self.running = false; } } fn render(&self) { if self.ticks_left > 0 { println!("You turn the crank..."); } else { println!("Jack POPS OUT OF THE BOX"); } } }
This makes sense to me, given my prior experience. Rust also has a concept of
constness
, it's just opt-out instead of opt-in. If this was C++, render
would
take a const reference, and update
would take a regular reference.
It's just that, in Rust, "regular" means immutable.
Immutable? Not const
?
Immutable. "constness" is a complicated and error-prone concept (where does the keyword go? does it allow interior mutability?). Immutability is not.
Are there other things that are immutable by default?
I don't know, I just started learning Rust, why are you asking me questions? Let's just focus on getting this thing compiled and running:
$ cargo check --quiet error[E0596]: cannot borrow `app` as mutable, as it is not declared as mutable --> src/main.rs:13:9 | 4 | let app = App { | --- help: consider changing this to be mutable: `mut app` ... 13 | app.update(); | ^^^ cannot borrow as mutable
Ah, see - there are other things that are immutable by default.
Yeah. There's "this" - the app
, uh, variable.
Okay let me look it up. The thing that is immutable there is... ah, I see.
Okay so it's not a variable.
It's not?
No - it's a "binding".
I guess the name "variable" sort of implies that it can change (it can "vary"), whereas bindings are, well, immutable by default.
And what is a binding?
From what I'm gathering, it's just a name you give to a value.
It says here that... it says here that a classic mistake is to consider the "variable" as a single thing, like "x is an integer", but that it's actually two things: there is an "integer value" somewhere (on the stack, on the heap, or wherever constants live), and there's the name you give to it - the binding.
And the binding is just the name, right?
Well, that - and also, it can be mutable or immutable.
Okay, I think I get it. So when I do this:
let app = App { /* ... */ };
There's a value of type App
somewhere in memory. And since all memory is
read-write, it can definitely be mutated (altered, modified, written to,
changed, updated).
But then I bind it to a name, app
- and because I don't explicitly say that
it's a mutable binding, then that means I can never mutate it through app
.
I wanna say that sounds right, but also I have a dozen tabs open right now so, ask again later. Turns out there's a lot of things to be read about Rust.
Yeah, yeah, but... my app?
Oh your app yeah, I don't know, didn't the compiler suggest a fix?
Oh right.
fn main() { // new: `mut` keyword let mut app = App { title: "Jack in the box".to_string(), ticks_left: 4, running: true, }; // etc. }
I think... I think it should work now?
Wonderful!
And now... the framework
Now that I've figured out all of this, I don't want to have to repeat it every time I make another graphical app. I want to have some re-usable code.
And I don't mean "just copy paste it from my trusty USB key", I mean - have
an actual framework, a "crate", as they say, maybe I'll even publish it so
I can get started on a new project by just adding a line to my Cargo.toml
.
So, because of my prior experience in C#, Java, C or C++, I follow along with my plans. We don't have classes, but we have traits - which, at first glance, feel a bit like Java interfaces.
So, what should the interface for "this project's logic" look like?
Let's look at App
again:
impl App { fn update(&mut self) { // ... } fn render(&self) { // ... } }
Okay, so we need an update
method that needs to be able to mutate self
,
and a render
method that does not need to mutate self
, but still needs to
read from it - because how is it going to render the app's state, if it can't
read it?
We can definitely make a religion trait out of that:
trait Client { fn update(&mut self); fn render(&self); }
And then we can separate the generic boilerplate (in App
) from the
project-specific logic (in MyClient
):
struct App { title: String, running: bool, } struct MyClient { ticks_left: usize, }
And then... then we have to implement the Client
trait for the MyClient
struct:
impl Client for MyClient { fn update(&mut self) { self.ticks_left -= 1; if self.ticks_left == 0 { self.running = false; } } fn render(&self) { if self.ticks_left > 0 { println!("You turn the crank..."); } else { println!("Jack POPS OUT OF THE BOX"); } } }
But, hm, running
is not a field of MyClient
- it's a field of App
.
So I guess Client
also needs to be able to access the App
.
Well, no biggie, I can just have it take a mutable reference to App
- just
like it takes a mutable reference to MyClient
.
That change needs to be done both on the trait
and on the impl
block:
trait Client { // new: `app: &mut App` fn update(&mut self, app: &mut App); fn render(&self); } impl Client for MyClient { // changed here, too: fn update(&mut self, app: &mut App) { self.ticks_left -= 1; if self.ticks_left == 0 { // and now we can change `running` app.running = false; } } fn render(&self) { // etc. } }
Finally, I can just remove update
and render
from the impl App
block -
in fact I think I'll just have a run
method that has the loop and everything in it.
impl App { fn run(&mut self) { println!("=== You are now playing {} ===", self.title); loop { self.client.update(self); self.client.render(); if !self.running { break; } sleep(Duration::from_secs(1)); } } }
Of course uhhh App
doesn't have a client
field yet. And in my new main
method, nowhere do I get to specify that I want MyClient
to be running:
fn main() { let mut app = App { title: "Jack in the box".to_string(), running: true, }; app.run(); }
Let's see... the Client::update
method looks like this:
trait Client { fn update(&mut self, app: &mut App); }
Which is a shorthand for this:
trait Client { fn update(self: &mut Self, app: &mut App); }
And Self
in this context means Client
(since we're in a trait Client
block):
trait Client { fn update(self: &mut Client, app: &mut App); }
So clearly we need a &mut Client
.
Alright then.
struct App { title: String, running: bool, client: &mut Client, }
And then in my main
function, I can simply do this:
fn main() { let mut client = MyClient { ticks_left: 4 }; let mut app = App { title: "Jack in the box".to_string(), running: true, client: &mut client, }; app.run(); }
This does not compile.
Oh no
The first thing the Rust compiler tells me is that I can't just say &mut Client
.
Client
is a trait - its concrete type could be anything! And at some point
in the language's evolution, it was decided that to make that very clear, one
should refer to it as dyn Client
.
So:
struct App { title: String, running: bool, // new: `dyn` client: &mut dyn Client, }
The second diagnostic is a lot more involved:
$ cargo check --quiet error[E0106]: missing lifetime specifier --> src/main.rs:18:13 | 18 | client: &mut dyn Client, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 15 | struct App<'a> { 16 | title: String, 17 | running: bool, 18 | client: &'a mut dyn Client, | error[E0228]: the lifetime bound for this object type cannot be deduced from context; please supply an explicit bound --> src/main.rs:18:18 | 18 | client: &mut dyn Client, | ^^^^^^^^^^
Now, since I've just started learning Rust, I have no idea what any of this means.
The lifetime... bound? Deduced?
Look pal, there's all the context you need:
fn main() { let mut client = MyClient { ticks_left: 4 }; let mut app = App { title: "Jack in the box".to_string(), running: true, client: &mut client, }; app.run(); }
See? The client
binding refers to a value that lives up until the end of
main
.
It definitely outlives app
. Isn't that all the context you need to deduce
the lifetime of App::client
?
Also - App
contains other fields, right? It has a bool
, it even has a
String
, which is not just a primitive type (integers, etc.). And I didn't
hear any complaints about those? Why should &mut dyn Client
be any different?
Sure, sure, the compiler tells me how to solve it. It says if I just do this:
struct App<'a> { title: String, running: bool, client: &'a mut dyn Client, }
...then everything is fine.
Well, almost:
$ cargo check --quiet error[E0726]: implicit elided lifetime not allowed here --> src/main.rs:47:6 | 47 | impl App { | ^^^- help: indicate the anonymous lifetime: `<'_>`
impl App<'_> { fn run(&mut self) { // etc. } }
Now everything is fine.
Wait, no:
$ cargo check --quiet error[E0499]: cannot borrow `*self.client` as mutable more than once at a time --> src/main.rs:52:13 | 52 | self.client.update(self); | ^^^^^^^^^^^^------^----^ | | | | | | | first mutable borrow occurs here | | first borrow later used by call | second mutable borrow occurs here error[E0499]: cannot borrow `*self` as mutable more than once at a time --> src/main.rs:52:32 | 52 | self.client.update(self); | ----------- ------ ^^^^ second mutable borrow occurs here | | | | | first borrow later used by call | first mutable borrow occurs here
Everything is not fine.
Okay. OKAY. There's a lot going on there. Maybe I can't just take my prior experience in Java, C#, C or C++ and just.. just wing it.
Maybe Rust references are just really complicated.
How about pointers? C and C++ have pointers. C#... eh, it's complicated. Java
doesn't have pointers, but it still has NullPointerException
for some reason.
Can't we just use pointers? Can't we just make the borrow checker shut it for once?
Let's give it a try:
// no lifetime parameter struct App { title: String, running: bool, // raw pointer client: *mut dyn Client, } struct MyClient { ticks_left: usize, } trait Client { // now takes a raw pointer to app fn update(&mut self, app: *mut App); fn render(&self); } impl Client for MyClient { fn update(&mut self, app: *mut App) { self.ticks_left -= 1; if self.ticks_left == 0 { // this is fine, probably unsafe { (*app).running = false; } } } fn render(&self) { if self.ticks_left > 0 { println!("You turn the crank..."); } else { println!("Jack POPS OUT OF THE BOX"); } } } impl App { fn run(&mut self) { println!("=== You are now playing {} ===", self.title); loop { // this converts a reference to a raw pointer let app = self as *mut _; // this converts a raw pointer to a reference let client = unsafe { self.client.as_mut().unwrap() }; // ..which we need because the receiver is a reference client.update(app); client.render(); if !self.running { break; } sleep(Duration::from_secs(1)); } } }
Theeeeeeere. Now it compiles. And it runs. It runs great! I can't wait to make other graphical applications using that framework. It has everything: command-line windows, an event loop... well, a loop.
Yeah. But didn't you just throw all of Rust's safety guarantees out the window?
What.. just because I used a few unsafe
blocks?
Precisely yeah.
Well, if they wanted me to benefit from Rust's safety guarantees, maybe they shouldn't have made them so confusing. Maybe I shouldn't have to take a freaking college course just to understand why the compiler is making a certain suggestion.
Okay, but-
No, no, I know, I'm being dramatic. But I'm still pretty frustrated that the compiler would tell me to "add a lifetime parameter" or something, when clearly my code can work perfectly fine without it!
Who needs references? Down! With! References! Down! With! Borrowck!
...
Alright alright I'll cool off. I'm not saying Rust is a bad language though - I like some of the syntax. And I don't have to worry about header files... and there's a neat little package manager slash build system tool, it's neat. I might just keep using it. Just.. with pointers, I guess.
Before you do that: I've just finished reading my dozen of browser tabs, and I think I can tell you a bit more about what's happening.
Would you be willing to try and let me explain? It won't be long.
It won't be long?
Cool bear promise.
Alright then.
Enter school bear
First, it's important to acknowledge that Rust is not like C#, Java, C or C++.
I don't know, it has pointers and references and everything. It even has curly braces.
Yes. But no. It's different. It's fundamentally different. It allows you to write software that does the same thing, but you have to think about it differently - because Rust has different priorities.
And you have to learn how to think differently.
Okay, okay, fine, I'll learn. But nothing I can find online teaches me, you know, how to think differently.
Well - it's very hard to teach. Because there's a lot of habits to unlearn.
There's entirely new concepts. trait
is not just a funny way to spell
class.
Lifetimes are not just an "opt-in safety feature", it's not an afterthought, it sits right there, at the base of the whole language.
So you're saying I should not have been making a graphical app as my first project?
Well... time will tell. It's good to pick projects that interest you, but even if you've done that kind of project before - lots of times - with another language, that doesn't mean it'll be necessarily easy for you to do in Rust.
Ugh, fine.
So you said Rust had "different priorities", what does that mean?
Well, Rust cares a lot about "memory safety".
Yes, yes, so I keep reading.
And for example, your program with pointers is not memory safe.
Not even a little.
But it runs fine. And I've looked at the code - several times, even - and I can't think of a single thing wrong with it.
Sure! It runs fine now.
But what if you change something?
Why would I change something.
Okay, what if someone else starts using your framework, and they reach that point:
fn main() { let mut app = App { title: "Jack, now outside the box".to_string(), running: true, client: /* ??? */, }; app.run(); }
And they go: "client? what's a client?", and since they have prior experience with C#, Java, C, or C++, they go "ehh it probably doesn't matter, I can just pass null".
But there's no null
keyword in Rust.
Indeed, but there's std::ptr::null_mut()
.
fn main() { let mut app = App { title: "Jack, now outside the box".to_string(), running: true, client: std::ptr::null_mut() as *mut MyClient, }; app.run(); }
Wait, where did they get MyClient
from?
Eh.. in this hypothetical situation, maybe your crate has an example client, and they've asked help from a friend to resolve the "type annotation needed" error and that's what they came up with.
Okay, fine, let's say they do that. Then what?
Then it compiles fine!
And when they run it, they see this:
$ cargo run --quiet === You are now playing Jack, now outside the box === thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:65:35 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Yeah, obviously it doesn't work - they're using it wrong.
And that's not a big deal - they can just, uh, run it with RUST_BACKTRACE=1
and see where the problem is.
$ RUST_BACKTRACE=1 cargo run --quiet === You are now playing Jack, now outside the box === thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:65:35 stack backtrace: (cut) 13: core::panicking::panic at src/libcore/panicking.rs:56 14: core::option::Option<T>::unwrap at /home/amos/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/macros/mod.rs:10 15: some_app::App::run at src/main.rs:65 16: some_app::main at src/main.rs:10 (cut) note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
And then they realize they have a problem, and they fix it, and everything is fine.
Yes. That's actually not the worst scenario.
The worst scenario is if you hadn't used a helper such as as_mut()
, and a null
pointer was actually dereferenced in your code:
impl App { fn run(&mut self) { println!("=== You are now playing {} ===", self.title); loop { let app = self as *mut _; // before: // let client = unsafe { self.client.as_mut().unwrap() }; // after: let client: &mut dyn Client = unsafe { std::mem::transmute(self.client) }; client.update(app); client.render(); if !self.running { break; } sleep(Duration::from_secs(1)); } } }
Eh, I don't really see the difference, let me try it:
$ RUST_BACKTRACE=1 cargo run --quiet === You are now playing Jack, now outside the box === [1] 1082353 segmentation fault (core dumped) RUST_BACKTRACE=1 cargo run --quiet
Ah.
Ah indeed. Now you have a segmentation fault.
Okay, yes, segmentation faults - I know these, thanks to my prior experience in C or C++. Those are fine too - you can just use a debugger to get a stacktrace, then make your way back to where that pointer actually became null, and in two shakes of a lamb's tail, you're on your way to shipping your app.
Yes. Two or more shakes yes.
But that's not even the worst case scenario.
Bear, come on - always with the worst case scenarios. You worry too much!
I do, but that's besides the point.
The worst case scenario is not a null pointer - it's a dangling pointer.
fn main() { let client_ptr = { let mut client = MyClient { ticks_left: 4 }; &mut client as _ }; let mut app = App { title: "Jack, now outside the box".to_string(), running: true, client: client_ptr, }; app.run(); }
Hang on, what does this even do? Can you walk me through it?
Wait no, I think I see it - okay so you're declaring a new binding, client_ptr
,
and the right-hand-side expression is... a block?
Yes, blocks are expressions in Rust.
Now that I like. Okay and the block makes a new MyClient
, which it binds to
client
- a mutable binding. And then as
, I think I've seen as
before, is
the casting operator, but you're casting it to _
?
Yes! I'm casting it to "figure it out, rustc".
Which, since I also use it in the App { ... }
initializer below, and the
left-hand-side is a &mut MyClient
, rustc can figure out that it needs
to be *mut MyClient
.
Okay, that all makes sense.
And then... ohhhh I see it. I see it. Sneaky bear. Since the MyClient
value
is built in a block... which has its own scope... it'll be freed before App
even has a chance to warm up, and the pointer will be dangling.
Now you're getting it!
Okay but - sorry, I don't really see where the problem is still. This will also just result in a segmentation fault, which as I pointed out, anyone with C or C++ experience is more than familiar with.
I don't think your argument is as compelling as you think it is.
Oh yes?
Why don't you try running it, then?
Sure.
$ RUST_BACKTRACE=1 cargo run --quiet === You are now playing Jack, now outside the box === You turn the crank... You turn the crank... You turn the crank... Jack POPS OUT OF THE BOX
Wait. Why does it-
Bwahahahahahaha. Yes, yes, it works.
Now run it in release mode.
$ RUST_BACKTRACE=1 cargo run --quiet --release === You are now playing Jack, now outside the box === You turn the crank... You turn the crank... You turn the crank... You turn the crank...
Wait what
You turn the crank... You turn the crank... You turn the crank... You turn the crank... You turn the crank...
Bear, help, why won't it stop
You turn the crank... You turn the crank... You turn the crank... You turn the crank... You turn the crank...
WHY WON'T IT STOP TURNING THE CRANK
Bwahahahahahahhahahahahahahah.
You turn the crank... You turn the crank... You turn the crank... You turn the crank... You turn the crank...
THE CRANK TURNING MUST BE STOPPED, BEAR HELP
Okay okay just Ctrl+C it.
AH. OKAY. IT STOPPED. WHAT THE HECK HAPPENED.
Oh, nothing much. Just the worst case scenario: silent corruption.
YES IT WOULD APPEAR SO. I'M SORRY FOR SHOUTING I'M STILL SORT OF UNDER THE SHOCK OF THE ENDLESS CRANK TURNING
That's okay. See the problem is... not all failures are noisy. Some failures are silent, and they're the worst kind.
Even worse, the program behaved differently in its debug build and its release build. Even if you had written tests, you wouldn't have caught it, because tests are built in debug by default.
WHEW YEAH THAT'S NOT GREAT
It isn't. And you're just making a game involving Jack, and a box, and his unclear location relative to the box.
Imagine if you were instead making an operating system, or a web browser, or a device directly responsible for keeping someone alive.
YEAH THAT WOULD BE SUPER SCARY
It would.
And those are not theoretical errors btw. Microsoft said in 2019 that 70% of its security bugs were memory safety issues.
And they're not the only ones saying it.
OKAY THEN - sorry - okay then, I guess it is a pretty important issue, and Rust helps with that?
It does. It very much does.
Okay so how does Rust help with that?
Well, let's start simple.
Here's a function that prints an i64
(signed 64-bit integer):
fn show(x: i64) { println!("x = {}", x); }
It's a great little function! But it only works with i64
.
Right. If we want to show something else, we either need to make
other functions, or we need to make show
generic.
Exactly - like that:
fn show<T>(x: T) { println!("x = {}", x); }
Yeah, generics. I know that because of my prior experience with C++, C#, or Java.
Yeah! Except it doesn't work because we haven't said that T
must be something that can be shown.
Something that.. can be shown? How do we say that?
We add a constraint:
use std::fmt::Display; fn show<T: Display>(x: T) { println!("x = {}", x); }
Or, the long-winded way:
use std::fmt::Display; fn show<T>(x: T) where T: Display, { println!("x = {}", x); }
Ahhh and Display
is... from what I can see in the standard library
documentation, Display
is a trait?
That is correct. Display
is a trait that represents a certain property
of a type: the ability to be... displayed.
Okay, but what does that have to do with lifetimes?
Well, what do you think is the difference between those two values:
fn main() { { let one = MyClient { ticks_left: 4 }; } let two = MyClient { ticks_left: 4 }; // some other code println!("Thanks for playing!"); }
Well... one
is in its own scope, so it'll get freed immediately after it's
initialized. Whereas two
will remain valid for the entire duration of main
.
That's exactly right! They have different lifetimes.
And can you tell me where the lifetimes are in that code?
Well they're... uh... no, I don't see them.
Precisely.
You don't see them, because the compiler has deduced them. They exist, and
they matter, and they determine what you can do with one
and what you can
do with two
, but you cannot see them in the code, and you cannot name them.
I see.
But because one
and two
have different lifetimes, they have different types.
Okay?
So if you were to write a function like that:
fn show_ticks(mc: &MyClient) { println!("{} ticks left", mc.ticks_left); }
...then you actually would've written a generic function.
What? That function isn't generic - where are the angle brackets? The chevrons?
The <>
?
Oh, you can add them if you want:
fn show_ticks<'wee>(mc: &'wee MyClient) { println!("{} ticks left", mc.ticks_left); }
Ah, so the other version was just a shorthand?
Yes!
And the function is generic over the lifetime of reference?
Technically, over the lifetime of the value the reference points to, but yes.
You could also make the function not generic, and ask for a specific
lifetime, like 'static
, which means the input should live forever.
fn show_ticks(mc: &'static MyClient) { println!("{} ticks left", mc.ticks_left); }
Now show_ticks
is not generic anymore.
And can we still use it? Like that?
fn main() { let one = MyClient { ticks_left: 4 }; show_ticks(&one); }
You can! But not like that:
cargo check --quiet error[E0597]: `one` does not live long enough --> src/main.rs:5:16 | 5 | show_ticks(&one); | -----------^^^^- | | | | | borrowed value does not live long enough | argument requires that `one` is borrowed for `'static` 6 | } | - `one` dropped here while still borrowed
You can only use it if you have a value of type MyClient
that lasts
forever. And one
dies when we reach the end of main
, so that particular
example doesn't work. And you don't know how to get a value of type
MyClient
that lasts forever.
Sure I do!
static FOREVER_CLIENT: MyClient = MyClient { ticks_left: 4 }; fn main() { show_ticks(&FOREVER_CLIENT); }
Oh, yeah, that works - that's actually why that lifetime is named 'static
,
static variables (in the data segment of an executable) live forever.
I was thinking more about something like this:
fn main() { let client = MyClient { ticks_left: 4 }; let client_ref = Box::leak(Box::new(client)); show_ticks(client_ref); }
Oh, this, yeah, I don't know about this.
Okay so - as soon as some of the arguments to a function are references, that function is generic.
Because the lifetime of "the value a reference points to" is part of the type of that reference. We just omit it a lot of the time, because mostly the compiler can figure it out.
Yeah I was about to ask - if we go back to the version the compiler suggested:
struct App<'a> { title: String, running: bool, client: &'a mut dyn Client, } impl App<'_> { fn run(&mut self) { println!("=== You are now playing {} ===", self.title); loop { let app = self as *mut _; self.client.update(app); self.client.render(); if !self.running { break; } sleep(Duration::from_secs(1)); } } }
...then it requires explicit lifetime annotations. Even though - and I want
to emphasize this - even though it knows exactly what the lifetime of app
and client
are here:
fn main() { let mut client = MyClient { ticks_left: 4 }; let mut app = App { title: "Jack in the box".to_string(), running: true, client: &mut client, }; app.run(); }
Yes! But consider this: lifetimes must be enforced throughout the entire program.
Not just in the scope of main
.
Yeah. Still. The compiler knows how I use it. The compiler knows how I use it everywhere. Can't it just figure it out?
Yes and no!
You use lifetime annotations (and bounds/constraints) for the same reason
you use types. If add(x, y)
only knows how to add i64
values, then that's
the type of the argument it takes.
If a function needs to hold on to a value forever, then it takes a &'static T
.
Okay, can you walk me through a bunch of examples?
Sure!
$ cargo new some-examples Created binary (application) `some-examples` package
struct Logger {} static mut GLOBAL_LOGGER: Option<&'static Logger> = None; fn set_logger(logger: &'static Logger) { unsafe { GLOBAL_LOGGER = Some(logger); } } fn main() { let logger = Logger {}; set_logger(Box::leak(Box::new(logger))); }
Right, a logger. I'd see why you'd want to hold onto that forever.
Why do you use unsafe
there?
Mutable static variables are definitely dangerous.
Okay, next slide please?
use std::time::SystemTime; fn log_message(timestamp: SystemTime, message: &str) { println!("[{:?}] {}", timestamp, message); } fn main() { log_message(SystemTime::now(), "starting up..."); log_message(SystemTime::now(), "shutting down..."); }
Okay, no lifetime annotations this time - I'm guessing message
is only
valid for the entire duration of the log_message
call.
And you don't need to hold on to it because... you just read from it, and then immediately stop using it. Okay.
What's a good example of needing to hold onto something for more than the duration of a function call, but less than the duration of a program?
That's exactly where things get interesting!
#[derive(Default)] struct Journal<'a> { messages: Vec<&'a str>, } impl Journal<'_> { fn log(&mut self, message: &str) { self.messages.push(message); } } fn main() { let mut journal: Journal = Default::default(); journal.log("Tis a bright morning"); journal.log("The wind is howling"); }
Wait, Default
? #derive
?
sigh okay fine:
struct Journal<'a> { messages: Vec<&'a str>, } impl Journal<'_> { fn new() -> Self { Journal { messages: Vec::new(), } } fn log(&mut self, message: &str) { self.messages.push(message); } } fn main() { let mut journal = Journal::new(); journal.log("Tis a bright morning"); journal.log("The wind is howling"); }
I see. The messages should live for as long as the journal itself.
Let me just try it out for myself... wait, it doesn't compile.
No, it doesn't!
$ cargo check -q error[E0312]: lifetime of reference outlives lifetime of borrowed content... --> src/main.rs:13:28 | 13 | self.messages.push(message); | ^^^^^^^ | note: ...the reference is valid for the lifetime `'_` as defined on the impl at 5:14... --> src/main.rs:5:14 | 5 | impl Journal<'_> { | ^^ note: ...but the borrowed content is only valid for the anonymous lifetime #2 defined on the method body at 12:5 --> src/main.rs:12:5 | 12 | / fn log(&mut self, message: &str) { 13 | | self.messages.push(message); 14 | | } | |_____^
So we haven't expressed the constraints correctly?
No!
And it's easier to see if we don't use any shorthands:
impl<'journal> Journal<'journal> { fn new() -> Self { Journal { messages: Vec::new(), } } fn log<'call>(&'call mut self, message: &'call str) { self.messages.push(message); } }
Wait you can do that?? Why do I keep seeing 'a
everywhere?
idk, it's short. When you only have one lifetime.. it's like x
in maths.
Okay so, with that code, the error becomes:
$ cargo check -q error[E0312]: lifetime of reference outlives lifetime of borrowed content... --> src/main.rs:13:28 | 13 | self.messages.push(message); | ^^^^^^^ | note: ...the reference is valid for the lifetime `'journal` as defined on the impl at 5:6... --> src/main.rs:5:6 | 5 | impl<'journal> Journal<'journal> { | ^^^^^^^^ note: ...but the borrowed content is only valid for the lifetime `'call` as defined on the method body at 12:12 --> src/main.rs:12:12 | 12 | fn log<'call>(&'call mut self, message: &'call str) { | ^^^^^
That is much better. So by using shorthands we accidentally borrowed it just
for the duration of the call, when really we wanted to borrow it for the
whole lifetime of our Journal
.
Yes, because we're literally storing it in the Journal
.
So what we want is this:
impl<'journal> Journal<'journal> { // omitted: `fn new` fn log<'call>(&'call mut self, message: &'journal str) { self.messages.push(message); } }
And if I wanted to bring back some shorthands... that:
impl<'journal> Journal<'journal> { // cut fn log(&mut self, message: &'journal str) { self.messages.push(message); } }
And.. can I shorten it even more using '_
?
You cannot. Just like x as _
is casting to the "figure it out" type,
'_
is the "idk, something" lifetime, and if you use it for both
impl Journal<'_>
and for &'_ str
, it'll default to &'call
, not
to &'journal
.
I see.
Okay so - you said Rust was looking over my shoulder, checking for lifetimes in the whole program, correct?
That's right.
But a thought occurs - what if I wanted to return a Journal
from some function?
Well, it all depends.
If you try something like this:
fn main() { let journal = get_journal(); } fn get_journal() -> Journal { let mut journal = Journal::new(); journal.log("Tis a bright morning"); journal.log("The wind is howling"); journal }
...you'll get a compile error.
Let me try it..
$ cargo check --quiet error[E0106]: missing lifetime specifier --> src/main.rs:21:21 | 21 | fn get_journal() -> Journal { | ^^^^^^^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from help: consider using the `'static` lifetime | 21 | fn get_journal() -> Journal<'static> { | ^^^^^^^^^^^^^^^^
Ah, yeah. So now I can't just write Journal
, I have to write Journal<something>
.
Okay, what if I write this:
fn get_journal<'a>() -> Journal<'a> { let mut journal = Journal::new(); journal.log("Tis a bright morning"); journal.log("The wind is howling"); journal }
That seems to work! What's going on there?
What's going on is that all your string constants: "Tis a bright morning",
"The wind is howling", etc., all have the 'static
lifetime.
So the resulting Journal
has the 'static
lifetime too?
Can I verify that somehow?
Sure you can:
fn main() { let journal: Journal<'static> = get_journal(); }
Oh right. It's just a type. I can just spell it out.
So what, how, uh... when would it prevent me from doing bad things?
Here's one:
fn get_journal<'a>() -> Journal<'a> { let s = String::from("Tis a dark night. It's also stormy."); let mut journal = Journal::new(); journal.log(&s); journal }
Let me check:
$ cargo check --quiet error[E0515]: cannot return value referencing local variable `s` --> src/main.rs:26:5 | 25 | journal.log(&s); | -- `s` is borrowed here 26 | journal | ^^^^^^^ returns a value referencing data owned by the current function error: aborting due to previous error
Oh yeah. Oh that's good. I like that. I've seen warnings about that from my prior experience with C or C++, but not good like that.
Yeah, and you know from your prior experience with Java or C# that in those languages it wouldn't even be a problem because of garbage collection.
Yeah, oh and, about that, is there a way for me to get roughly the same behavior as in C# or Java?
There's always reference counting!
use std::sync::Arc; #[derive(Default)] struct Journal { messages: Vec<Arc<String>>, } impl Journal { fn log(&mut self, message: Arc<String>) { self.messages.push(message); } } fn main() { let _journal: Journal = get_journal(); } fn get_journal() -> Journal { let s = Arc::new(String::from("Tis a dark night. It's also stormy.")); let mut journal: Journal = Default::default(); journal.log(s); journal }
Wait, wait, hold on - where did all the lifetimes go?
We don't need them anymore! Because with smart pointers like Arc
and Rc
,
as long as we hold at least one reference (one Arc<T>
) to a value, the
value lives on.
But wait, &T
and Arc<T>
seem like completely different syntaxes... but
they're both actually pointers?
They're both pointers.
What if I have an Arc<T>
and I need a &T
? Am I stuck writing methods
that take an Arc<T>
?
No, you're not stuck at all.
You can definitely borrow (get a reference to) the contents of an Arc<T>
-
it'll be valid for as long as the Arc<T>
lives.
Any examples?
Sure:
use std::sync::Arc; struct Event { message: String, } #[derive(Default)] struct Journal { events: Vec<Arc<Event>>, } impl Journal { fn log(&mut self, message: String) { self.events.push(Arc::new(Event { message })); } fn last_event(&self) -> Option<&Event> { self.events.last().map(|ev| ev.as_ref()) } } fn main() { // muffin }
Ah, you're talking about last_event
, right? Even though we're holding
onto values of type Arc<Event>
, we can turn those into an &Event
.
Yes, and more often than not, the coercion is automatic.
So, for example, this works:
impl Event { fn print(&self) { println!("Event(message={})", self.message); } } fn main() { let ev = Arc::new(Event { message: String::from("well well well."), }); ev.print(); }
Convenient! I guess last_event
could just as well return an Arc<Event>
, right?
Since it's actually a pointer, shouldn't we be able to just... increase the reference
count by one and return that?
Yes, by cloning it! And by "it" I mean the smart pointer, not the Event
:
impl Journal { fn last_event(&self) -> Option<Arc<Event>> { self.events.last().map(|x| Arc::clone(x)) } }
Note that we're using Option
here, because if the journal is empty,
there will be no last event.
Also, Option
has a nice shorthand for what we're doing:
impl Journal { fn last_event(&self) -> Option<Arc<Event>> { self.events.last().cloned() } }
But wait! Wait a minute. Does that mean I can get rid of the lifetime problem
in my application by just using Arc<T>
?
You absolutely 100% can!
But do you need to?
What do you mean, do I need to?
What I mean is: do you need shared ownership? Will you have other
references to the Client
?
Uhh I don't know yet. Maybe I don't, maybe it's okay if App
has sole
ownership of Client
?
In that case, you want a Box<T>
.
What's that? Another smart pointer?
Yes! It's exactly the same size as a reference.
struct Foobar {} fn main() { let f = Foobar {}; let f_ref = &f; let f_box = Box::new(Foobar {}); println!("size of &T = {}", std::mem::size_of_val(&f_ref)); println!("size of Box<T> = {}", std::mem::size_of_val(&f_box)); }
$ cargo run --quiet size of &T = 8 size of Box<T> = 8
Right, eight bytes, because we're on 64-bit.
Okay so I can just use a Box
then? Let me try that...
// back in `some-app/src/main.rs` fn main() { let client = MyClient { ticks_left: 4 }; let mut app = App { title: "Jack in the box".to_string(), running: true, client: Box::new(client), }; app.run(); } struct App { title: String, running: bool, client: Box<dyn Client>, } impl App { // unchanged }
Okay, this does work... what's the difference with the version with the lifetime annotations and such?
As far as generated code goes - there is no difference. It should be the exact same binary (in release mode at least).
But in terms of ownership: in the version with lifetime annotations,
App
borrowed Client
from fn main
.
Now, App
owns Client
- and so Client
lives for as long as the App
.
Okay. Well, this is a lot of new information but okay. But it works the same right?
The exact same, yes!
And I didn't have to change any of the other code, like that over here:
impl App { fn run(&mut self) { println!("=== You are now playing {} ===", self.title); loop { let app = self as *mut _; self.client.update(app); self.client.render(); if !self.running { break; } sleep(Duration::from_secs(1)); } } }
...even though Client::update
takes a &mut self
and self.client
is now a Box<dyn Client>
, because it does that same magic that coerces
a smart pointer into a &T
or &mut T
as needed?
Yes, autoderef!
It's not actual magic, by the way. It's just two traits: Deref and DerefMut.
Ohh. It's traits all the way down.
And then some.
Okay, things are clearer now. But... we still have a *mut App
in our code,
right here:
let app = self as *mut _;
...which we pass to Client::update
:
trait Client { // hhhhhhhhhhhhhhhhhhhhhhhere. fn update(&mut self, app: *mut App); fn render(&self); }
Can we get rid of that now?
I don't know, try it, see how the compiler likes it.
Oh okay.
trait Client { fn update(&mut self, app: &mut App); fn render(&self); } impl Client for MyClient { fn update(&mut self, app: &mut App) { self.ticks_left -= 1; if self.ticks_left == 0 { app.running = false; } } fn render(&self) { // omitted } } impl App { fn run(&mut self) { println!("=== You are now playing {} ===", self.title); loop { // remember `&mut self` is just `self: &mut Self`, // so `self` is just a binding of type `&mut App`, // which is the exact type that `Client::update` takes. self.client.update(self); self.client.render(); if !self.running { break; } sleep(Duration::from_secs(1)); } } }
Now we just compile it, and...
$ cargo check --quiet error[E0499]: cannot borrow `*self.client` as mutable more than once at a time --> src/main.rs:52:13 | 52 | self.client.update(self); | ^^^^^^^^^^^^------^----^ | | | | | | | first mutable borrow occurs here | | first borrow later used by call | second mutable borrow occurs here error[E0499]: cannot borrow `*self` as mutable more than once at a time --> src/main.rs:52:32 | 52 | self.client.update(self); | ----------- ------ ^^^^ second mutable borrow occurs here | | | | | first borrow later used by call | first mutable borrow occurs here
Arghh. No, see? Same error.
Ah, that's when the "learn to think the Rust way" comes in.
Yes but WHAT DOES IT MEAN, ahhhhhhh.
Okay, remain calm - currently App
holds everything right?
The title
, the running
boolean flag, and the client
.
Yes, that's correct.
And the problem is that, since Client::update
takes a &mut Client
,
just by doing self.client.update(...)
, we're borrowing self
mutably once.
Uh-huh.
And then Client::update
also wants a &mut App
, so we need
to borrow ourselves mutably a second time.
Yeah.
And that's forbidden.
Yeah seems like a big no-no for some reason.
So here's an idea - how about we separate the state a bit.
On one side, we'll have.. well, the client. And on the other side, we'll have, well, the state of the app.
What do you mean, "separate" - like, make a new struct?
Exactly! Name it AppState
or something.
Okay, let's try it.
struct App { client: Box<dyn Client>, state: AppState, } struct AppState { title: String, running: bool, }
Okay bear, I'm done, what now?
Well now we carefully review the MyClient::update
method.
Let me pull it up from the mainframe.. here it is:
impl Client for MyClient { fn update(&mut self, app: &mut App) { self.ticks_left -= 1; if self.ticks_left == 0 { app.running = false; } } }
Okay - think carefully.
Does MyClient::update
need to have access to the whole App
?
Well... the only thing it's doing is... toggling the running
flag.
Which is now where?
Oh, it's in AppState
.
So MyClient
really only needs..
..a mutable reference to AppState
! Say no more.
trait Client { fn update(&mut self, state: &mut AppState); fn render(&self); } impl Client for MyClient { fn update(&mut self, state: &mut AppState) { self.ticks_left -= 1; if self.ticks_left == 0 { state.running = false; } } fn render(&self) { // omitted } }
There!
And then you need to update a few other things in the application...
I'm getting to it! First the main
function:
fn main() { let client = MyClient { ticks_left: 4 }; let mut app = App { state: AppState { title: "Jack in the box".to_string(), running: true, }, client: Box::new(client), }; app.run(); }
..and then App::run
:
impl App { fn run(&mut self) { println!("=== You are now playing {} ===", self.state.title); loop { self.client.update(&mut self.state); self.client.render(); if !self.state.running { break; } sleep(Duration::from_secs(1)); } } }
...and then:
$ cargo check Finished dev [unoptimized + debuginfo] target(s) in 0.00s
...then it works! Did we do it?
No, you did it.
Whoa. I feel smarter already. Okay but, uh, aren't we still
mutably borrowing self
twice?
No, you're, uh... no, you're borrowing different parts
of self
mutably. That's perfectly okay.
So splitting state is good? Because it makes it easier to track the lifetimes of various things?
Very good. Much recommend.
In fact, it's good even outside of Rust. If you ever go back to C, or C++, after a long period of doing Rust, you may find yourself writing things a little differently.
Phew, okay.
Any last words? I mean, do you have some more advice for my graphical app?
As a matter of fact I do!
One pattern that is especially useful in a language like Rust, that doesn't let you write code that "works right now" but would definitely break if you changed it in a way that silently breaks the invariants only you, the code designer, have in your mind, is the following.
Instead of directly performing mutation, consider returning values that describe the mutation that should be performed.
Oh yeah, I g... I'm sorry bear I don't, I don't actually get it, can you show me an example?
Sure! In your graphical app - you're passing the whole of &mut AppState
...
just to be able to quit the application, right?
Ah yeah, pretty much yep.
And it's the oooonly thing you'll ever do with that AppState
. You'll
never, for example, set running
to true
. You'll never mess with any
other part of the AppState
.
No yeah that's fair.
So you could just as well have it return a boolean: true
if it should
keep running, and false
if it should quit.
Ah yeah, let me try that:
trait Client { // returns false if the app should exit fn update(&mut self) -> bool; fn render(&self); } impl Client for MyClient { fn update(&mut self) -> bool { self.ticks_left -= 1; self.ticks_left > 0 } fn render(&self) { if self.ticks_left > 0 { println!("You turn the crank..."); } else { println!("Jack POPS OUT OF THE BOX"); } } } struct App { client: Box<dyn Client>, state: AppState, } struct AppState { title: String, } impl App { fn run(&mut self) { println!("=== You are now playing {} ===", self.state.title); loop { let running = self.client.update(); self.client.render(); if !running { break; } sleep(Duration::from_secs(1)); } } }
Ohhhh yeah. I like that. Fewer references! Fewer "opportunities" to fight with the borrow checker. I could get used to that.
Right? Although we've lost in clarity a little bit - it's not immediately
obvious why update()
returns a bool
. I can tell you know that this
is the case, because you added a comment to indicate what it actually
does.
Yeah, guilty. I did add a comment because of that.
But you didn't add a comment everywhere did you? It's not in the
impl Client for MyClient
block, and it's not in App::run
where
update
is called.
That's true, I suppose you would have to do some digging to find what it does.
What if I told you it didn't have to be like this?
It doesn't?
No!
Rust has enums, and you should use them.
You could have an enum like that:
enum UpdateResult { None, QuitApplication, }
Oh.. and that's what update
would return?
Let's try it:
trait Client { fn update(&mut self) -> UpdateResult; fn render(&self); } impl Client for MyClient { fn update(&mut self) -> UpdateResult { self.ticks_left -= 1; if self.ticks_left == 0 { UpdateResult::QuitApplication } else { UpdateResult::None } } } impl App { fn run(&mut self) { println!("=== You are now playing {} ===", self.state.title); loop { let res = self.client.update(); self.client.render(); match res { UpdateResult::None => {} UpdateResult::QuitApplication => { return; } } sleep(Duration::from_secs(1)); } } }
Oh that's neat. Very neat. You're so wise bear.
Thanks for all the help.
No worries!
If you liked what you saw, please support my work!