Some Highlights About Rust
(for C/C++ developers)
betterCode() Rust 2023
Rainer Stropek | time cockpit
Ownership and Borrowing
The "Highlander" Princple 😉:
There can be
only one
struct Ring { inscription: &'static str, }
fn forge() -> Box<Ring> {
let one_ring = Box::new(Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...", });
one_ring
}
Owner
struct Ring { inscription: &'static str, }
fn main() {
let mut ring = forge();
println!("My powerful 💍! {}", ring.inscription);
}
fn forge() -> Box<Ring> {
let one_ring = Box::new(Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...", });
one_ring
}
Gives ownership to caller
Caller is now owner
struct Ring { inscription: &'static str, }
fn main() {
let mut ring = forge();
println!("My powerful 💍! {}", ring.inscription);
}
fn forge() -> Box<Ring> {
let one_ring = Box::new(Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...", });
one_ring
}
fn keep(ring: Box<Ring>) {
println!("Now the 💍 is mine! {}", ring.inscription);
}
ring goes out of scope ➡️ is freed
struct Ring { inscription: &'static str, }
fn main() {
let mut ring = forge();
println!("My powerful 💍! {}", ring.inscription);
keep(ring);
println!("My powerful 💍! {}", ring.inscription);
}
fn forge() -> Box<Ring> {
let one_ring = Box::new(Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...", });
one_ring
}
fn keep(ring: Box<Ring>) {
println!("Now the 💍 is mine! {}", ring.inscription);
}
Ownership
moved to
callee
ring cannot be used anymore
13 | keep(ring);
| ---- value moved here
14 | println!("My powerful 💍! {}", ring.inscription);
| ^^^^^^^^^^^^^^^^ value borrowed here after move
Owner is responsible for cleaning up
struct Ring { inscription: &'static str, }
fn main() {
let mut ring = forge();
println!("My powerful 💍! {}", ring.inscription);
borrow(&ring);
println!("My powerful 💍! {}", ring.inscription);
keep(ring);
//println!("My powerful 💍! {}", ring.inscription);
}
fn forge() -> Box<Ring> {
let one_ring = Box::new(Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...", });
one_ring
}
fn keep(ring: Box<Ring>) {
println!("Now the 💍 is mine! {}", ring.inscription);
}
fn borrow(ring: &Box<Ring>) {
println!("I can read the 💍's inscription: {}", ring.inscription);
}
ring is borrowed (immutable) ➡️
no moving of ownership
ring can still be used
struct Ring { inscription: &'static str, }
fn main() {
let mut ring = forge();
println!("My powerful 💍! {}", ring.inscription);
borrow(&ring);
println!("My powerful 💍! {}", ring.inscription);
to_english(&mut ring);
println!("My powerful 💍! {}", ring.inscription);
keep(ring);
//println!("My powerful 💍! {}", ring.inscription);
}
fn forge() -> Box<Ring> {
let one_ring = Box::new(Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...", });
one_ring
}
fn keep(ring: Box<Ring>) {
println!("Now the 💍 is mine! {}", ring.inscription);
}
fn borrow(ring: &Box<Ring>) {
println!("I can read the 💍's inscription: {}", ring.inscription);
}
fn to_english(ring: &mut Box<Ring>) {
ring.inscription = "One Ring to rule them all, One Ring to find them, ...";
}
Mutable borrow ➡️ method can change ring
Caller agrees to mutable borrow
struct Ring { inscription: &'static str, }
fn main() {
let mut ring = forge();
let first_borrow = ˚
let second_borrow = ˚
println!("{} and {}", first_borrow.inscription, second_borrow.inscription);
}
fn forge() -> Box<Ring> {
let one_ring = Box::new(Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...", });
one_ring
}
Multiple immutable borrows are ✅
struct Ring { inscription: &'static str, }
fn main() {
let mut ring = forge();
let first_borrow = ˚
let second_borrow = ˚
println!("{} and {}", first_borrow.inscription, second_borrow.inscription);
let mut first_borrow = &mut ring;
let mut second_borrow = &mut ring;
first_borrow.inscription = "One ring to rule them all, one ring to find them, ...";
}
fn forge() -> Box<Ring> {
let one_ring = Box::new(Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...", });
one_ring
}
12 | let mut first_borrow = &mut ring;
| --------- first mutable borrow occurs here
13 | let mut second_borrow = &mut ring;
| ^^^^^^^^^ second mutable borrow occurs here
Only one mutable borrow at a time
It is ok to have multiple immutable borrows
or multiple immutable references
Either one mutable reference
References must always be valid
What if you need multiple references?
- It is fine to have multiple immutable borrows
- Use reference-counted smart pointers
- Use RefCell<T> 🔗 for runtime borrow checker
- Sometimes, the compiler simply isn't smart enough
or to conservative
- Sometimes, the compiler simply isn't smart enough
- Unsafe code
- Last resort
- Bypasses borrow checker completely
Lifetimes
struct Ring {
inscription: &'static str,
}
fn get_the_one (ring1: & Ring, ring2: & Ring) -> Option<& Ring> {
[ring1, ring2]
.into_iter()
.find(|ring| ring.inscription.starts_with("Ash nazg durbatulûk"))
}
68 | fn get_the_one (ring1: & Ring, ring2: & Ring) -> Option<& Ring> {
| -------- -------- ^ expected named lifetime parameter
Photo of "The One Ring": https://en.wikipedia.org/wiki/One_Ring#/media/File:One_Ring_Blender_Render.png (CC BY-SA 4.0)
fn main() {
let some_ring = Ring { inscription: "I whish you luck 🍀", };
let result: Option<&Ring>;
{
let the_one_ring = forge();
result = get_the_one(&the_one_ring, &some_ring);
}
println!("The one ring is: {}", match result {
Some(ring) => ring.inscription,
None => "No ring found",
});
}
some_ring
💍
the_one_ring
result
⚡
the_one_ring goes out-of-scope
Potentially accessing invalid reference
What's the Problem?
struct Ring {
inscription: &'static str,
}
fn get_the_one (ring1: & Ring, ring2: & Ring) -> Option<& Ring> {
[ring1, ring2]
.into_iter()
.find(|ring| ring.inscription.starts_with("Ash nazg durbatulûk"))
}
<'a>
'a
'a
'a
Adding lifetime
specifiers
Result must not outlive the inputs!
fn main() {
let some_ring = Ring { inscription: "I whish you luck 🍀", };
let result: Option<&Ring>;
{
let the_one_ring = forge();
result = get_the_one(&the_one_ring, &some_ring);
}
println!("The one ring is: {}", match result {
Some(ring) => ring.inscription,
None => "No ring found",
});
}
31 | let the_one_ring = forge();
| ------------ binding `the_one_ring` declared here
32 | result = get_the_one(&the_one_ring, &some_ring);
| ^^^^^^^^^^^^^ borrowed value does not live long enough
33 | }
| - `the_one_ring` dropped here while still borrowed
struct Ring {
inscription: &'static str,
}
fn get_the_one (ring1: & Ring, ring2: & Ring) -> Option<& Ring> {
[ring1, ring2]
.into_iter()
.find(|ring| ring.inscription.starts_with("Ash nazg durbatulûk"))
}
struct Ring { inscription: &'static str, }
fn main() {
}
fn forge() -> Ring {
let one_ring = Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...", };
one_ring
}
static represents the entire duration
of the program
struct Ring { inscription: &'static str, }
struct Hobbit<'a> {
ring: &'a Ring,
}
fn main() {
}
fn forge() -> Ring {
let one_ring = Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...", };
one_ring
}
Hobbit must not outlive the borrowed ring
➡️ needs a lifetime
struct Ring { inscription: &'static str, }
struct Hobbit<'a> {
ring: &'a Ring,
}
struct RingCollection<'a> {
rings: Vec<&'a Ring>,
}
fn main() {
}
fn forge() -> Ring {
let one_ring = Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...", };
one_ring
}
Collection must not outlive the borrowed rings
➡️ needs a lifetime
struct Ring { inscription: &'static str, }
struct Hobbit<'a> {
ring: &'a Ring,
}
struct RingCollection<'a> {
rings: Vec<&'a Ring>,
}
fn main() {
let frodo: Hobbit;
let saurons_rings: RingCollection;
{
let the_one_ring = forge();
frodo = Hobbit { ring: &the_one_ring, };
saurons_rings = RingCollection { rings: vec![&the_one_ring], };
}
println!("Sauron has {} rings", saurons_rings.rings.len());
println!("Frodo has the ring {}", frodo.ring.inscription);
}
fn forge() -> Ring {
let one_ring = Ring { inscription: "...", };
one_ring
}
16 | let the_one_ring = forge();
| ------------ binding `the_one_ring` declared here
17 |
18 | frodo = Hobbit { ring: &the_one_ring, };
| ^^^^^^^^^^^^^ borrowed value does not live long enough
19 | saurons_rings = RingCollection { rings: vec![&the_one_ring], };
20 | }
| - `the_one_ring` dropped here while still borrowed
...
23 | println!("Frodo has the ring {}", frodo.ring.inscription);
| ---------------------- borrow later used here
the_one_ring goes out of scope
the_one_ring
frodo
saurons_rings
⚡
Parallel Programming
and Ownership
struct Ring {
inscription: String,
}
fn forge(rings: &mut Vec<Ring>) {
rings.push(Ring { inscription: "Narya".to_string() });
rings.push(Ring { inscription: "Nenya".to_string() });
rings.push(Ring { inscription: "Vilya".to_string() });
rings.push(Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...".to_string() });
}
Producer
Vector owns rings
struct Ring {
inscription: String,
}
fn forge(rings: &mut Vec<Ring>) {
rings.push(Ring { inscription: "Narya".to_string() });
rings.push(Ring { inscription: "Nenya".to_string() });
rings.push(Ring { inscription: "Vilya".to_string() });
rings.push(Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...".to_string() });
}
fn distribute(rings: &[Ring]) -> Vec<(&Ring, String)> {
let mut result = Vec::new();
for ring in rings {
result.push((ring, match ring.inscription.as_str() {
"Narya" => "Gandalf".to_string(),
"Nenya" => "Galadriel".to_string(),
"Vilya" => "Elrond".to_string(),
_ => "Frodo".to_string(),
}));
}
result
}
Consumes rings and produces annotated ones
struct Ring {
inscription: String,
}
fn forge(rings: &mut Vec<Ring>) {
rings.push(Ring { inscription: "Narya".to_string() });
rings.push(Ring { inscription: "Nenya".to_string() });
rings.push(Ring { inscription: "Vilya".to_string() });
rings.push(Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...".to_string() });
}
fn distribute(rings: &[Ring]) -> Vec<(&Ring, String)> {
let mut result = Vec::new();
for ring in rings {
result.push((ring, match ring.inscription.as_str() {
"Narya" => "Gandalf".to_string(),
"Nenya" => "Galadriel".to_string(),
"Vilya" => "Elrond".to_string(),
_ => "Frodo".to_string(),
}));
}
result
}
fn count_rings(rings: &[(&Ring, String)], receiver: &'static str) -> usize {
rings.iter().filter(|(_, name)| name == receiver).count()
}
Consumes annotated rings
fn main() {
let mut rings = Vec::new();
forge(&mut rings);
let distributed_rings = distribute(&rings);
const RECEIVER: &'static str = "Gandalf";
let number_of_rings = count_rings(&distributed_rings, RECEIVER);
println!("{RECEIVER} has {number_of_rings} ring(s).");
}
Producer
Producer/consumer
Consumer
use std::sync::mpsc;
use std::thread;
struct Ring { inscription: String, }
fn forge(tx: mpsc::Sender<Ring>) {
tx.send(Ring { inscription: "Narya".to_string() }).unwrap();
tx.send(Ring { inscription: "Nenya".to_string() }).unwrap();
tx.send(Ring { inscription: "Vilya".to_string() }).unwrap();
tx.send(Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...".to_string() }).unwrap();
}
Send takes ownership of ring
use std::sync::mpsc;
use std::thread;
struct Ring { inscription: String, }
fn forge(tx: mpsc::Sender<Ring>) {
tx.send(Ring { inscription: "Narya".to_string() }).unwrap();
tx.send(Ring { inscription: "Nenya".to_string() }).unwrap();
tx.send(Ring { inscription: "Vilya".to_string() }).unwrap();
tx.send(Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...".to_string() }).unwrap();
}
fn distribute(rx: mpsc::Receiver<Ring>, tx: mpsc::Sender<(Ring, String)>) {
for received_ring in rx {
let receiver = match received_ring.inscription.as_str() {
"Narya" => "Gandalf".to_string(),
"Nenya" => "Galadriel".to_string(),
"Vilya" => "Elrond".to_string(),
_ => "Frodo".to_string(),
};
tx.send((received_ring, receiver)).unwrap();
}
}
Ring has been moved in memory,
but not string content
use std::sync::mpsc;
use std::thread;
struct Ring { inscription: String, }
fn forge(tx: mpsc::Sender<Ring>) {
// Note that send takes ownership of Ring.
tx.send(Ring { inscription: "Narya".to_string() }).unwrap();
tx.send(Ring { inscription: "Nenya".to_string() }).unwrap();
tx.send(Ring { inscription: "Vilya".to_string() }).unwrap();
tx.send(Ring { inscription: "Ash nazg durbatulûk, ash nazg gimbatul, ...".to_string() }).unwrap();
}
fn distribute(rx: mpsc::Receiver<Ring>, tx: mpsc::Sender<(Ring, String)>) {
for received_ring in rx {
let receiver = match received_ring.inscription.as_str() {
"Narya" => "Gandalf".to_string(),
"Nenya" => "Galadriel".to_string(),
"Vilya" => "Elrond".to_string(),
_ => "Frodo".to_string(),
};
tx.send((received_ring, receiver)).unwrap();
}
}
fn count_rings(rx: mpsc::Receiver<(Ring, String)>, receiver: &'static str) -> usize {
rx.iter().filter(|(_, name)| name == receiver).count()
}
Consumer
fn main() {
let (tx_forged, rx_forged) = mpsc::channel();
let (tx_distributed, rx_distributed) = mpsc::channel();
thread::spawn(move || forge(tx_forged));
thread::spawn(move || distribute(rx_forged, tx_distributed));
const RECEIVER: &'static str = "Gandalf";
let number_of_rings = count_rings(rx_distributed, RECEIVER);
println!("{RECEIVER} has {number_of_rings} ring(s).");
}
Ownership of sender tx_foged is moved to the thread
Producer:
tx_forged
Consumer/
Producer:
rx_forged
tx_distributed
Consumer:
rx_distributed
Error Handling
Error Handling in Rust
- Rust does not have exceptions
- Rust does not have null pointers
- At least that's true for safe Rust
- Rust does not have HRESULTs
Rust has a superpower 🦸♀️
Enums
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Ring {
#[allow(dead_code)]
inscription: String,
#[allow(dead_code)]
owner: String,
}
const RING_URL: &str = "https://cddataexchange.blob.core.windows.net/data-exchange/ring.json";
async fn get_rings_rookie() -> Vec<Ring> {
let rings: Vec<Ring> =
reqwest::get(RING_URL)
.await
.unwrap()
.json()
.await
.unwrap();
rings
}
#[tokio::main]
async fn main() {
println!("{:?}", get_rings_rookie().await);
}
async fn get_rings_with_panic() -> Vec<Ring> {
let response = reqwest::get(RING_URL).await;
if response.is_err() {
panic!();
}
let response = response.unwrap();
let rings = response.json::<Vec<Ring>>().await;
if rings.is_err() {
panic!();
}
rings.unwrap()
}
#[tokio::main]
async fn main() {
println!("{:?}", get_rings_with_panic().await);
}
In case of a bug, we can panic
- Anticipated runtime error: Handle Result gracefully
- Unanticipated bug: panic
#[derive(Debug)]
struct RingError;
async fn get_rings_with_error() -> Result<Vec<Ring>, RingError> {
let response = reqwest::get(RING_URL).await;
if response.is_err() {
return Err(RingError);
}
let response = response.unwrap();
let rings = response.json::<Vec<Ring>>().await;
if rings.is_err() {
return Err(RingError);
}
Ok(rings.unwrap())
}
#[tokio::main]
async fn main() {
match get_rings_with_error().await {
Ok(rings) => println!("{:?}", rings),
Err(_) => println!("Error while retrieving rings"),
};
}
Error handling with if or match
#[derive(Debug, Deserialize)]
struct Ring {
#[allow(dead_code)]
inscription: String,
#[allow(dead_code)]
owner: String,
}
#[derive(Debug)]
struct RingError;
impl From<reqwest::Error> for RingError {
fn from(_: reqwest::Error) -> Self {
RingError
}
}
Conversion into error type
#[derive(Debug, Deserialize)]
struct Ring {
#[allow(dead_code)]
inscription: String,
#[allow(dead_code)]
owner: String,
}
#[derive(Debug)]
struct RingError;
impl From<reqwest::Error> for RingError {
fn from(_: reqwest::Error) -> Self {
RingError
}
}
async fn get_rings_with_better_error() -> Result<Vec<Ring>, RingError> {
Ok(reqwest::get(RING_URL)
.await?
.json()
.await?)
}
#[tokio::main]
async fn main() {
match get_rings_with_better_error().await {
Ok(rings) => println!("{:?}", rings),
Err(_) => println!("Error while retrieving rings"),
};
}
Error propagation
⚠️ Two Very Useful Crates ⚠️
Structs and Traits
struct Ring(String);
impl Ring {
fn new(inscription: &str) -> Self {
Ring(inscription.to_string())
}
fn is_the_one(&self) -> bool {
self.0.starts_with("Ash nazg durbatulûk, ash nazg gimbatul")
}
}
Tuple struct
Methods
Instance member
struct Ring(String);
impl Ring {
fn new(inscription: &str) -> Self {
Ring(inscription.to_string())
}
fn is_the_one(&self) -> bool {
self.0.starts_with("Ash nazg durbatulûk, ash nazg gimbatul")
}
}
fn main() {
let ring = Ring::new("Ash nazg durbatulûk, ash nazg gimbatul, ...");
if ring.is_the_one() {
println!("One ring to rule them all");
} else {
println!("Not the one");
}
let ring = Ring::new("Three Rings for the Elven-kings under the sky, ...");
if ring.is_the_one() {
println!("One ring to rule them all");
} else {
println!("Not the one");
}
}
struct Ring(String);
impl Ring { ... }
trait IsTheOne {
fn is_the_one(&self) -> bool;
}
impl IsTheOne for Ring {
fn is_the_one(&self) -> bool {
self.0.starts_with("Ash nazg durbatulûk, ash nazg gimbatul")
}
}
Trait, somewhat like an interface, but so much more...
Trait implementation
struct Ring(String);
impl Ring { ... }
trait IsTheOne {
fn is_the_one(&self) -> bool;
}
impl IsTheOne for Ring {
fn is_the_one(&self) -> bool {
self.0.starts_with("Ash nazg durbatulûk, ash nazg gimbatul")
}
}
fn get_a_ring() -> impl IsTheOne {
let mut rng = rand::thread_rng();
if rng.gen() {
Ring::new("Ash nazg durbatulûk, ash nazg gimbatul, ...")
} else {
Ring::new("Three Rings for the Elven-kings under the sky, ...")
}
}
fn main() {
let ring = get_a_ring();
if ring.is_the_one() {
println!("One ring to rule them all");
} else {
println!("Not the one");
}
}
All return values must have same type
use rand::Rng;
struct Ring(String);
trait IsTheOne {
fn is_the_one(&self) -> bool;
}
struct SignificantOther(String);
impl SignificantOther {
fn new(name: &str) -> Self {
SignificantOther(name.to_string())
}
}
impl IsTheOne for SignificantOther {
fn is_the_one(&self) -> bool {
self.0 == "Karin"
}
}
Another struct that implements IsTheOne
use rand::Rng;
struct Ring(String);
trait IsTheOne {
fn is_the_one(&self) -> bool;
}
struct SignificantOther(String);
impl SignificantOther {
fn new(name: &str) -> Self {
SignificantOther(name.to_string())
}
}
impl IsTheOne for SignificantOther {
fn is_the_one(&self) -> bool {
self.0 == "Karin"
}
}
fn get_something_precious() -> Box<dyn IsTheOne> {
let mut rng = rand::thread_rng();
if rng.gen() {
Box::new(Ring::new("Ash nazg durbatulûk, ash nazg gimbatul, ..."))
} else {
Box::new(SignificantOther::new("Karin"))
}
}
Trait object
Return values do not need to have same type
fn main() {
let precious = get_something_precious();
if precious.is_the_one() {
println!("I found someone or something really precious");
} else {
println!("Not the one");
}
let precious_things = vec![
Box::new(Ring::new("Ash nazg durbatulûk, ash nazg gimbatul, ...")) as Box<dyn IsTheOne>,
Box::new(SignificantOther::new("Karin")),
];
for precious in precious_things {
if precious.is_the_one() {
println!("I found someone or something really precious");
} else {
println!("Not the one");
}
}
}
Dynamic method dispatching
There is so much more to say and learn... 🔗
Q&A
Thank you for your attention!
some_highlights_about_rust
By Rainer Stropek
some_highlights_about_rust
- 394