Who's this guy?

Sylvain Wallez - @bluxte

Principal engineer - Elastic

Previously tech lead, CTO, architect, trainer, developer...
...at OVH, Actoboard, Sigfox, Scoop.it, Joost, Anyware

Member of the Apache Software Foundation since 2003


We're hiring!

On the menu

  • Where does Rust come from?
  • Basics: functions, structures, methods
  • Controlled mutability
  • Ad hoc polymorphism with traits
  • Memory management
  • Controlled concurrency

Rust

"Empowering everyone to build reliable and efficient software"

  • Performance: blazingly fast and memory efficient.
  • Reliability: rich type system & ownership model guarantee memory safety & thread safety.
  • Productivity: great documentation, friendly compiler, awesome tooling.

Rust

"Empowering everyone to build reliable and efficient software"

  • Performance: blazingly fast and memory efficient.
  • Reliability: rich type system & ownership model guarantee memory safety & thread safety.
  • Productivity: great documentation, friendly compiler, awesome tooling.

Learning Rust

Online at https://www.rust-lang.org/

Rust

  • Started in 2006 at Mozilla, first announced in 2010

    • Primary goals: a fast and secure language
    • Parts of Firefox are written in Rust
  • First stable release in 2015

    • New releases every 6 weeks, “edition 2021” released in Oct '21
  • Who uses it?

    • AWS: Firecracker powers Lambda and Fargate
    • Google: parts of the Fuschia operating system
    • Linux: 2nd official language for the Kernel!
    • CloudFlare: quic / http 3 implementation
    • Dropbox: file storage
    • Clever Cloud: reverse proxy
    • Atlassian, Canonical, Coursera, Chef, Deliveroo, NPM, Sentry…
    • Growing ecosystem for embedded development

The Rust ecosystem

crates.io – there’s a crate for that!

Twitter: @rustlang, @ThisWeekInRust
https://users.rust-lang.org
https://exercism.io/

http://www.arewewebyet.org/
http://arewegameyet.com/
https://areweideyet.com/
http://www.arewelearningyet.com/ https://docs.rust-embedded.org/

Getting started: rustup & cargo

Rustup: the Rust toolchain manager

Manage versions, target OS and architectures

curl https://sh.rustup.rs -sSf | sh or download from https://rustup.rs/

rustup doc --std -- browse the docs locally!

Cargo: the Rust build system

cargo new --bin rust_intro
cargo run
.
├── Cargo.toml
└── src
    └── main.rs

Hello, Rust!

Cargo.toml

[package]
name = "rust_intro"
version = "0.1.0"
authors = ["Sylvain Wallez <sylvain@bluxte.net>"]
edition = "2018"

[dependencies]

main.rs

fn main() {
    println!("Hello, world!");
}

Variables & type inference

 

fn main() {
    let answer = 42;
    
    println!("Hello {}", answer);
    
    assert_eq!(answer,42);
}

Control structures

 

fn main() {
    for i in 0..5 {
        if i % 2 == 0 {
            println!("{} is even", i);
        } else {
            println!("{} is odd", i);
        }
    }
}

If as an expression

 

fn main() {
    for i in 0..5 {
        let even_odd = if i % 2 == 0 {"even"} else {"odd"};
        println!("{} is {}", i, even_odd);
    }
}

Function declaration

Parameters and return types must be explicit

fn is_even(i: i32) -> bool {
    i % 2 == 0
}

fn main() {
    for i in 0..5 {
        let even_odd = if is_even(i) {"even"} else {"odd"};
        println!("{} is {}", i, even_odd);
    }
}

Immutability by default

 

fn main() {
    let mut sum = 0;
    for i in 0..5 {
        sum += i;
    }
    println!("sum is {}", sum);
}

Functional iteration

 

fn is_even(i: i32) -> bool {
    i % 2 == 0
}

fn main() {
    let sum: i32 =
        (0..5)                   // this is an iterator
        .filter(|i| is_even(*i)) // filter with a closure
        .sum();                  // consume the iterator
        
    println!("sum of even numbers is {}", sum);
}

Passing values by reference

 

fn is_even(i: &i32) -> bool {
    i % 2 == 0
}

fn main() {
    let sum: i32 =
        (0..5)                   // this is an iterator  
        .filter(|i| is_even(i))  // filter with a closure
        .sum();                  // consume the iterator
            
    println!("sum of even numbers is {}", sum);
}

Mutable function parameters

 

fn modifies(x: &mut f64) {
    *x = 1.0;
}

fn main() {
    let mut result = 0.0;
    modifies(&mut result);
    println!("result is {}", result);
}

Vectors

 

fn main() {
    let mut v = Vec::new();
    v.push(10);
    v.push(20);
    v.push(30);

    let first = v[0];           // will panic if out-of-range
    let maybe_first = v.get(0); // returns an Option

    println!("v is {:?}", v);
    println!("first is {}", first);
    println!("maybe_first is {:?}", maybe_first);
}

Some, None? The Option enum

 


#![allow(unused)]
fn main() {
pub enum Option<T> {
    None,
    Some(T),
}
}

Pattern matching

 

fn main() {
    let v = vec![10, 20, 30]; // initialization macro    
    let idx = 0;
    
    match v.get(idx) {
        Some(value) => println!("Value is {}", value),
        None => println!("No value..."),
    }
}

Destructuring assigment

 

fn main() {
    let v = vec![10, 20, 30];    
    let idx = 0;
    
    if let Some(value) = v.get(idx) {
        println!("Value is {}", value);
    }
}

More pattern matching

 

fn main() {
    let n = 0;
    let text = match n {
        0 => "zero",
        1 => "one",
        2 => "two",
        _ => "many",
    };

    println!("{} is {}", n, text);
}

Tuples

 

fn add_mul(x: f64, y: f64) -> (f64, f64) {
    (x + y, x * y)
}

fn main() {
    let t = add_mul(2.0, 10.0);

    println!("tuple is {:?}", t);

    println!("add {} mul {}", t.0, t.1);

    let (add, mul) = t;
    println!("add {} mul {}", add, mul);
}

Structs

 

struct Person {
    first_name: String,
    last_name: String
}

fn main() {
    let p = Person {
        first_name: "John".to_string(),
        last_name: "Smith".to_string()
    };
    println!("This is {} {}", p.first_name, p.last_name);
}

Struct implementation

struct Person {
    first_name: String,
    last_name: String
}

impl Person {
    fn new(first: &str, name: &str) -> Person {
        Person {
            first_name: first.to_string(),
            last_name: name.to_string()
        }
    }
}

fn main() {
    let p = Person::new("John","Smith");
    println!("This is {} {}", p.first_name,p.last_name);
}

(String are objects, &str are references to char arrays)

Struct methods

struct Person {
    first_name: String,
    last_name: String
}

impl Person {
    fn new(first: &str, name: &str) -> Person {
        Person {
            first_name: first.to_string(),
            last_name: name.to_string()
        }
    }
    
    fn full_name(&self) -> String {
        format!("{} {}", self.first_name, self.last_name)
    }

}

fn main() {
    let p = Person::new("John","Smith");
    println!("This is {}", p.full_name());
}

Variations on self

#[derive(Debug)]
struct Person {
    first_name: String,
    last_name: String
}

impl Person {
    fn new(first: &str, name: &str) -> Person {
        Person {
            first_name: first.to_string(),
            last_name: name.to_string()
        }
    }

    fn full_name(&self) -> String {
        format!("{} {}",self.first_name, self.last_name)
    }

    fn set_first_name(&mut self, name: &str) {
        self.first_name = name.to_string();
    }

    fn to_tuple(self) -> (String, String) {
        (self.first_name, self.last_name)
    }
}

fn main() {
    let mut p = Person::new("John","Smith");
    println!("{:?}", p);

    p.set_first_name("Jane");
    println!("{:?}", p);

    println!("{:?}", p.to_tuple());
    
    // p has now moved, below will fail to compile
    // println!("{:?}", p);
}

Struct implementations: wrapping up

  • no self argument: associated functions, like the new "constructor"

  • &self argument: can use the values of the struct, but not change them

  • &mut self argument: can modify the values

  • self argument: will consume the value, which will move

There can be only one owner

#[derive(Debug)]
struct Person { name: String }

impl Person {
    fn new(name: &str) -> Person {
        Person { name: name.to_string() }
    }
}

fn take_ownership(p: Person) {
    println!("{} is mine", p.name);
}

fn borrow_it(p: &Person) {
    println!("I'm giving {} back to you!", p.name);
}

fn main() {
    let p = Person::new("John");
    println!("{:?}", p);
    
    // let x = p;  // moving p will break the code below
    // println!("{:?}", x);
    
    borrow_it(&p);
    println!("{:?}", p);
    
    take_ownership(p);    
    // println!("{:?}", p); // will fail
}

Extending existing types

trait Show {
    fn show(&self) -> String;
}

impl Show for i32 {
    fn show(&self) -> String {
        format!("a four-byte signed {}", self)
    }
}

impl Show for f64 {
    fn show(&self) -> String {
        format!("an eight-byte float {}", self)
    }
}

fn main() {
    let answer = 42;
    let pi = 3.14;
    println!("Here is {}", answer.show());
    println!("Here is {}", pi.show());
}

Sweet, we've added new methods to i32 and f64!

Adding type constraints

trait Show {
    fn show(&self) -> String;
}

impl Show for i32 {
    fn show(&self) -> String {
        format!("a four-byte signed {}", self)
    }
}

impl<T> Show for Option<T> where T: Show {
    fn show(&self) -> String {
        match self {
            Some(v) => v.show(),
            None => format!("nothing"),
        }
    }
}

fn main() {
    let answer = Some(42);
    let void: Option<i32> = None;
    println!("Here is {}", answer.show());
    println!("Here is {}", void.show());
}

Box: dynamic allocation

#[derive(Debug)]
struct Node {
    value: String,
    left: Option<Box<Node>>,
    right: Option<Box<Node>>,
}

impl Node {
    fn new(s: &str) -> Node {
        Node{value: s.to_string(), left: None, right: None}
    }

    fn set_left(&mut self, node: Node) {
        self.left = Some(Box::new(node));
    }

    fn set_right(&mut self, node: Node) {
        self.right = Some(Box::new(node));
    }
}


fn main() {
    let mut root = Node::new("root");
    root.set_left(Node::new("left"));
    root.set_right(Node::new("right"));

    println!("{:#?}", root);
}

A generic sorted tree

#[derive(Debug)]
struct Node<T> {
    value: T,
    left: Option<Box<Node<T>>>,
    right: Option<Box<Node<T>>>,
}

impl<T: Ord> Node<T> {
    fn new(v: T) -> Node<T> {
        Node{value: v, left: None, right: None}
    }

    fn set_left(&mut self, node: Node<T>) {
        self.left = Some(Box::new(node));
    }

    fn set_right(&mut self, node: Node<T>) {
        self.right = Some(Box::new(node));
    }
    
    fn insert(&mut self, data: T) {
        if data < self.value {       // <-- Ord is used here
            match self.left {
                Some(ref mut n) => n.insert(data),
                None => self.set_left(Self::new(data)),
            }
        } else {
            match self.right {
                Some(ref mut n) => n.insert(data),
                None => self.set_right(Self::new(data)),
            }
        }
    }
}

fn main() {
    let mut root = Node::new("root".to_string());
    root.insert("one".to_string());
    root.insert("two".to_string());
    root.insert("four".to_string());

    println!("{:#?}", root);
}

Automatic memory reclamation

Box::new(node) allocates on the heap and node is moved inside the box. Ownership of the box can move, but you can only get a reference to its content.

The memory is automatically freed when the box has no more owner (it is "dropped").

struct DropTracer(i32);

impl Drop for DropTracer {
    fn drop(&mut self) {
        println!("Dropping {}", self.0);
    }
}

fn main() {
    let a = DropTracer(0);
    println!("a contains {}", a.0);

    let mut b = Box::new(DropTracer(1));
    println!("b contains {}", b.0);
    
    println!("Replacing b");
    b = Box::new(DropTracer(2));
    println!("b contains {}", b.0);
   
    println!("Exiting");
}

Automatic file closing

Ownership and lifetimes will automatically close files.

use std::fs::File;
use std::path::Path;
use std::io::Read;

fn read_file() -> String {
    let mut text = String::new();
    let path = Path::new("file.txt");
    
    let mut file = File::open(path).unwrap();
    file.read_to_string(&mut text).unwrap();
    
    return text;
}

fn main() {
    let str = read_file();
    println!("Text is {}", str);
}

The Error type


#![allow(unused)]
fn main() {
pub enum Result<T, E> {
    /// Contains the success value
    Ok(T),
    /// Contains the error value
    Err(E),
}
}

Reading a file with proper error handling:

use std::fs::File;
use std::path::Path;
use std::io::Read;

fn read_file() -> Result<String, std::io::Error> {
    let mut text = String::new();
    let path = Path::new("file.txt");
    
    let mut file = File::open(path)?;
    file.read_to_string(&mut text)?;
    
    return Ok(text);
}

fn main() -> Result<(), std::io::Error>{
    let str = read_file()?;
    println!("Text is {}", str);
    Ok(())
}

There's a lot more to talk about...

  • shared references with reference counting
  • multithreading and the Sync and Send traits
  • Mutex and RwLock from the standard library
  • async programming
  • interior mutability
  • etc...

Compared to other languages, Rust is simple but has non conventional features that are its strength.

If you want to use it, take the time to learn it. Ferris will thank you :-)

Thanks!

 

 

Presentation contents inspired by https://stevedonovan.github.io/rust-gentle-intro/
Sources available at https://github.com/swallez/introduction-to-rust/