Thanks to my sponsors: Jake Demarest-Mays, nyangogo, Foxie Solutions, Berkus Decker, Victor Song, Mikkel Rasmussen, Aalekh Patel, Diego Roig, SeniorMars, Jelle Besseling, Andy F, hgranthorner, Olivia Crain, Andy Gocke, Mark Old, Tabitha, Joshua Roesslein, Christian Bourjau, Steven McGuire, Herman J. Radtke III and 235 more
I am a Java, C#, C or C++ developer, time to do some Rust
👋 This page was last updated ~4 years ago. Just so you know.
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.
Cool bear's hot tip
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.
Cool bear's hot tip
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.
Cool bear's hot tip
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.
Cool bear's hot tip
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.
Cool bear's hot tip
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.
Cool bear's hot tip
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
Cool bear's hot tip
Ah, see - there are other things that are immutable by default.
Yeah. There's "this" - the app
, uh, variable.
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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
.
Cool bear's hot tip
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?
Cool bear's hot tip
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.
Cool bear's hot tip
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?
Cool bear's hot tip
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.
Cool bear's hot tip
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!
Cool bear's hot tip
...
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.
Cool bear's hot tip
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's hot tip
Cool bear promise.
Alright then.
Enter school bear
Cool bear's hot tip
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.
Cool bear's hot tip
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.
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
Well, Rust cares a lot about "memory safety".
Yes, yes, so I keep reading.
Cool bear's hot tip
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.
Cool bear's hot tip
Sure! It runs fine now.
But what if you change something?
Why would I change something.
Cool bear's hot tip
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.
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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.
Cool bear's hot tip
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.
Cool bear's hot tip
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.
Cool bear's hot tip
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!
Cool bear's hot tip
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?
Cool bear's hot tip
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 _
?
Cool bear's hot tip
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.
Cool bear's hot tip
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.
Cool bear's hot tip
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-
Cool bear's hot tip
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
Cool bear's hot tip
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
Cool bear's hot tip
Okay okay just Ctrl+C it.
AH. OKAY. IT STOPPED. WHAT THE HECK HAPPENED.
Cool bear's hot tip
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
Cool bear's hot tip
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
Cool bear's hot tip
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
Cool bear's hot tip
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?
Cool bear's hot tip
It does. It very much does.
Okay so how does Rust help with that?
Cool bear's hot tip
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.
Cool bear's hot tip
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.
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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
.
Cool bear's hot tip
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.
Cool bear's hot tip
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.
Cool bear's hot tip
But because one
and two
have different lifetimes, they have different types.
Okay?
Cool bear's hot tip
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 <>
?
Cool bear's hot tip
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?
Cool bear's hot tip
Yes!
And the function is generic over the lifetime of reference?
Cool bear's hot tip
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);
}
Cool bear's hot tip
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);
}
Cool bear's hot tip
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.
Cool bear's hot tip
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();
}
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
Mutable static variables are definitely dangerous.
Okay, next slide please?
Cool bear's hot tip
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?
Cool bear's hot tip
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
?
Cool bear's hot tip
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.
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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
.
Cool bear's hot tip
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 '_
?
Cool bear's hot tip
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?
Cool bear's hot tip
That's right.
But a thought occurs - what if I wanted to return a Journal
from some function?
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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.
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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>
?
Cool bear's hot tip
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?
Cool bear's hot tip
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
.
Cool bear's hot tip
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?
Cool bear's hot tip
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>
?
Cool bear's hot tip
You absolutely 100% can!
But do you need to?
What do you mean, do I need to?
Cool bear's hot tip
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
?
Cool bear's hot tip
In that case, you want a Box<T>
.
What's that? Another smart pointer?
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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.
Cool bear's hot tip
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?
Cool bear's hot tip
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.
Cool bear's hot tip
Ah, that's when the "learn to think the Rust way" comes in.
Yes but WHAT DOES IT MEAN, ahhhhhhh.
Cool bear's hot tip
Okay, remain calm - currently App
holds everything right?
The title
, the running
boolean flag, and the client
.
Yes, that's correct.
Cool bear's hot tip
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.
Cool bear's hot tip
And then Client::update
also wants a &mut App
, so we need
to borrow ourselves mutably a second time.
Yeah.
Cool bear's hot tip
And that's forbidden.
Yeah seems like a big no-no for some reason.
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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;
}
}
}
Cool bear's hot tip
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.
Cool bear's hot tip
Which is now where?
Oh, it's in AppState
.
Cool bear's hot tip
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
}
}
Cool bear's hot tip
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?
Cool bear's hot tip
No, you did it.
Whoa. I feel smarter already. Okay but, uh, aren't we still
mutably borrowing self
twice?
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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?
Cool bear's hot tip
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.
Cool bear's hot tip
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.
Cool bear's hot tip
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.
Cool bear's hot tip
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.
Cool bear's hot tip
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.
Cool bear's hot tip
What if I told you it didn't have to be like this?
It doesn't?
Cool bear's hot tip
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.
Cool bear's hot tip
No worries!
Here's another article just for you:
The HTTP crash course nobody asked for
HTTP does a pretty good job staying out of everyone's way.
If you're reading this article, there's a solid chance it was delivered to you over HTTP. Even if you're reading this from an RSS reader or something. And you didn't even have to think about it!
"Not having to think about it" is certainly a measure of success for a given technology. By contrast, . I wish I didn't.