A small dive into Rust
The thing that I hated the most, back at engineer school1 studying C and C++ was pointer logic. No matter how hard I tried, I could never remember the correct syntax. Plus, there was the burden of manual memory managment. I hated it so much that I never got back to C and the very few times I had to do C++, I ended up allocating everything on the stack. Yeah. Don’t do that kids. That’s bad for your health.
I recently came across a Microsoft article from last summer, which revealed that 70% to 80% of all the vulnerabilities discovered in Windows were related to memory management problems. Now, you can say anything you like about how shitty the UX is on Windows, about Microsoft’s predatory behavior toward the whole industry and, in particular, the free/libre software ecosystem, but there’s one thing about which I have absolutely no doubt: there are really brilliant people working at Microsoft.
Which means: if Microsoft is facing the problem. The whole industry is facing the problem.
I say this in response to some criticisms I’ve seen during my (short) carreer against languages that manage memory for you. Those stating that, not having the habbit of allocating/deallocating your memory yourself makes you a messy programer. Those stating that programers from pre-Java era knew how to make good programs and correctly manage the computer’s resources.
The Ariane V’s first flight fiasco as well as the whole Angry Video Game Nerd channel should prove them wrong and make them realize that programs in the 70’s were as shitty as today’s.
Anyway. Manually handle your memory is a bad idea in all cases. No exceptions. So what can we do about it? Well, adopt languages that manage memory for us!
Always keep learning
You know, there’s a reason why I called this blog Always keep learning. I love to learn new things. I love that “oh, wow…” moment. The one when you understands something.
So, following this motto, I’ve gotten used to learning 2 or 3 different new languages or technologies every year. Last year was Kotlin and Android’s Jetpack. The years before was Vue, Python and Django.
This year, it was Docker (well, it’s about time!) and… Rust.
I’ve been wanting to learn Rust for a few years now. Atually, since Mozilla announced that it made available its new HTML rendering engine, around 2017, whiwh is written in Rust. And I must confess that it is the most difficult language I’ve learnt so far2.
Rust is a tricky language to learn. The syntax is the least difficulty of all. Rust has been influenced by a lot of diffrent languages including C and C++ (obvioulsy), but also Swift, Ruby, Scala, Haskell and OCamL from what I’ve seen so far.
A small glimpse of Rust
Weirdly enough, Rust isn’t that much inspired by neither C nor C++. Here is Rust’s hello world:
fn main() {
println!("Hello World!");
}
Hello world is probably the most common and least interesting program in the world but it
already shows a few influences of Rust. The first one is the println!
function3 which
has spread from Scala to virtually every language created for the JVM (Groovy, Kotlin, Clojure…).
The function declaration in Rust looks like this:
fn <function-name> (<arg-name>: <type>, ...) [-> <return-type>] {
// Function body and this is a comment
}
I don’t really know where that syntax comes from. It resembles Swift’s, though Swift keyword for
declaring functions is func
. OCamL, which has the fun
keyword, could be a common inspiration.
Just like for Swift, the return type is declared using a right arrow (->
) and optionnal when
the function returns nothing4. The following are equivalent:
fn print_int(a: i32) -> () {
// snip
}
fn print_int(a: i32) {
// snip
}
You can declare a value using let
. Values a immutable. If you want a variable, you must annotate it
with mut
:
fn main() {
// This asks its name to the user on
// on the command prompt
let hello = "Hello";
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("{} {}!", hello, name);
}
You’ve probably noticed here the formatting syntax of println!
is not borrowed from C but from Python.
As you can see, Rust also has pointers and the syntax for declaring a pointer on a variable is
inspired from C: &
. Also, the syntaxe is a bit different : &
is used both for declaring
a pointer on a variable and get its memory reference. C declares a pointer type with *int
.
But pointers in Rust are not pointers. They are references like the ones you can find in the
Java world. You never manipulate the pointer itself. Always the underlying data. &
only
serves to tell the compiler that the object is a reference. The use is transparent (like in Java).
You also probably noticed the usage of ::
. This probably comes directly from C++ and has
the same meaning usage: modules referencing. As for comments, they come from C#: //
, /* */
denotes respectvely a single-line and multi-line comment. ///
and /** */
are respectvely
a single-line and multi-line documentation (I’m not sure ///
is valid in Java?).
Control flow is also synctactically a 1:1 copy of Swift’s:
if n < 0 {
print!("{} is negative", n);
} else if n > 0 {
print!("{} is positive", n);
} else {
print!("{} is zero", n);
}
while n < 101 {
// snip
}
for n in 1..101 {
// snip
}
Edit: According to a colleague that knows Rust far better than I do, it’s actually the other
way around. Swift is more recent than Rust and copied the control flow syntax of Rust. The two
languages now mutually influence each other. Graydon Hoare, the creator of Rust, is now working
on Swift, which borrowed its if let
syntax from Rust.
The absence of parenthesis around the boolean condition and the enforcements of curly braces felt weird to me at first. Espacially since, while developing in Kotlin, I’ve taken the habit of using a lot a one-liner conditionnals omitting the curly braces. But you get used to it quickly. I know the possibility to omit the curly braces is an open debate in C and C++.
Like most recent languages, Rust dropped the bool ? val1 : val2
ternary from C in favour of
the if ... else ...
construct:
let b = true
let val = if bool { "Iz tru" } else { "iz falss" };
We need to go deeper
If Rust isn’t an object oriented language it isn’t really a pure imperative one either. It is
something in between. Like in C, complex types can be created with struct
s:
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
Rust also have enums but they’re are nothing like C’s. They OCamL’s records or Kotlin and Scala’s data classes as you can do pattern matching and destructuring on them:
// Create an `enum` to classify a web event. Note how both
// names and type information together specify the variant:
// `PageLoad != PageUnload` and `KeyPress(char) != Paste(String)`.
// Each is different and independent.
enum WebEvent {
// An `enum` may either be `unit-like`,
PageLoad,
PageUnload,
// like tuple structs,
KeyPress(char),
Paste(String),
// or c-like structures.
Click { x: i64, y: i64 },
}
// A function which takes a `WebEvent` enum as an argument and
// returns nothing.
fn inspect(event: WebEvent) {
match event {
WebEvent::PageLoad => println!("page loaded"),
WebEvent::PageUnload => println!("page unloaded"),
// Destructure `c` from inside the `enum`.
WebEvent::KeyPress(c) => println!("pressed '{}'.", c),
WebEvent::Paste(s) => println!("pasted \"{}\".", s),
// Destructure `Click` into `x` and `y`.
WebEvent::Click { x, y } => {
println!("clicked at x={}, y={}.", x, y);
},
}
}
Side note: this kind of pattern-matching has also been proposed to Python 3.10 with PEP 0622 and that is awesome.
Anyway, Rust’s pattern matching is definitely coming from the ML language family and
OCamL in particular, even though the syntax looks more like a mix of Java’s switch
and
JS’ arrow functions (wait… what!?). I’m not really sure where it comes from.
So Rust has structs and enums. But it doesn’t have methods… Or does it…?
You can write functions that are placed in a “module” with the same name than an enum
or a struct using the impl
keyword:
struct User {
username: String,
first_name: String,
last_name: String,
email: String,
sign_in_count: u64,
active: bool,
}
impl User {
// Don't do that.
// This is an internationalization anti-pattern
fn full_name(self) -> String {
format!("{} {}", self.first_name, self.last_name):
}
}
If the first argument is self
, which is Rust’s this
, you have access to an object-like
syntactic sugar:
struct User {
username: String,
first_name: String,
last_name: String,
email: String,
sign_in_count: u64,
active: bool,
}
impl User {
fn full_name(self) -> String {
format!("{} {}", self.first_name, self.last_name);
}
}
let user = User {
username: "Neo".to_string(),
first_name: "Thomas A.".to_string(),
last_name: "Anderson".to_string(),
email: "neo@the.matrix".to_string(),
sign_in_count: 42,
active: true
};
// Both calls are equivalent
user.full_name();
User::full_name(user);
This notation is probably inspired by Python’s. Rust also has the concept of traits which are probably coming from Scala’s and are somehow equivalent to Java 8’s interfaces (methods signatures + optionnal implementation). There is no concept of inheritence, though. Traits and implems are enough for most needs, though.
And, of course, like any language of the 21st century, Rust features anonymous functions (or closures, or lambda, call them whatever you like):
vec![1, 2, 3, 4, 5, 6, 7, 8, 9]
.iter()
.for_each(|item| {
// do something here
println!("{}", item)
});
The syntax here seems to come from Ruby.
Rust’s memory managment
The most distinctive feature that makes Rust a good candidate to finally get rid of C is its particular memory managment system. In order to efficiently keep track of what memory to preserve and what memory to free without implmenting a garbage collector, Rust has introduced two concepts: ownership5 and lifetime.
Now these two concepts are a bit complex to be extensively explained here but let me try to give you an overview.
The first one, ownership, is defined by 3 rules:
- each value in Rust has a variable that’s called its owner,
- there can only be one owner at a time,
- when the owner goes out of scope, the memory of the value is collected.
Example:
fn some_function() {
// Some things are done here
// s is not valid here, it’s not been declared yet
{
// s is valid from this point forward
let s = "hello";
// do stuff with s
}
// this scope is now over, and s is no longer valid
}
Now, in this example, ownership sounds straightforward. But, what happens when you pass the variable in a function ? It takes its owernship and you can’t use it afterwards unles the function returns it:
fn some_function() {
let s = String::from("hello");
takes_ownership(s);
// Oopsie. This code won't compile. s is invalid here
// since you moved its ownership to the function
// takes_ownership, it has been freed right after the call
// was finished.
println!(s);
}
fn takes_ownership(some_string: String) {
// some_string comes into scope
println!("{}", some_string);
}
// Here, some_string goes out of scope and `drop` is called.
// The backing memory is freed.
To prevent a function call to take ownership, you must either make it return the value or pass it by reference:
fn some_function() {
let s = String::from("hello");
// Pass s by reference and prevent
// do_something to take ownership.
do_something(&s);
// Ok.
println!(s);
}
fn do_something(some_string: &String) {
// some_string is used exactly the same
// way than if it was not passed by reference.
println!("{}", some_string);
}
You can find the complete reference on Rust’s ownership system here.
As for lifetimes, it is related to ownership and reference passing. Let’s take the following example:
{
// r comes into scope
let r;
{
// x comes into scope
let x = 5;
// r references x
r = &x;
// x is freed after that
}
// Something bad would happen here as r references x which
// has been freed. Rust says that x does not live long
// enough and has gone out of scope.
println!("r: {}", r);
}
Lifetimes are a safety to prevent usage of a memory section that could have been freed in the meantime.
Conclusion
Rust is an awesome language. The perfect one for developing critical projects in which performance is not a luxury. I started learning it thinking my knowledge in a variety of other languages would help me understanding it quickly. That was a mistake. Even though the inspirations from other languages are clear and I was never really lost in the syntax, Rust introduces a few concepts that are a bit tough to catch at first.
I nerver expected the learning curve to be flat, but I didn’t expect it to be that steep. Still, Rust is a fun language to learn and to program with. And I would definitely recommand it to anyone adventurous enough to learn it.
-
Sorry, this is so french. Engineer school is a 5 years diploma post-high school. ↩
-
Let aside OCamL that I had to learn for my studies and was definitely not ready for. ↩
-
Actually a macro in Rust, but I’m not going to explain this concept because I haven’t even touched on it myself yet. ↩
-
Actually, it returns a special type called
unit
and written()
which comes from OCamL. Kotlin and Scala also have this concept ofunit
. ↩ -
The borrowing and ownership concepts take their roots into the work of the french mathematician Jean-Yves Girard called linear logic and the teams that translated that mathematic theory into a type system. The research paper discribing this work is available here. ↩