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);
}

Takes ownership (see also Drop trait 🔗)

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 = &ring;
    let second_borrow = &ring;

    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 = &ring;
    let second_borrow = &ring;

    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
    • Rc<T> 🔗
    • Arc<T> (shared ownership between threads, 🔗)
  • Use RefCell<T> 🔗 for runtime borrow checker
    • Sometimes, the compiler simply isn't smart enough
      or to conservative
  • 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);
}

Methods that can fail return Result 🔗.
unwrap will panic in case of an error

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

  • 265