Intro to Rust

Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.

- rust-lang.org

Follow along

The Rust Playground is a quick and easy way to experiment with Rust code

https://play.rust-lang.org

I've included permalinks with code samples so you don't have to type to follow

Why you should

try to learn Rust

really

seriously

earnestly

honestly

genuinely

truly dearly

 

Rust's Value Proposition

Save Developers Time

Minimize time spent debugging

Prevent memory errors

Eliminate entire classes of security vulnerabilities

Provide world-class tooling and support

Empower everyone to write systems-level code

Without sacrificing performance or efficiency

Save Developers Time

through advancements in Language Design

  • Automatic memory management without garbage collection (ownership/borrowing)
  • Separation of data mutation and data sharing
  • Encapsulate unsafe code within safe abstractions

Save Developers Time

through advancements in Tooling

  • Built-in:
    • package manager (cargo build)
    • code formatter (cargo fmt)
    • linter (cargo clippy)
  • Standardized documentation generation and publishing using markdown (docs.rs)
    • cargo doc
  • Out-of-the-box testing and benchmarking
    • cargo test, cargo bench

Save Developers Time

with a helpful and welcoming Community

Language Intro

Functions and Structs

fn main() {
    let person = new_person("Nick".to_owned(), 21);
    println!("Our person's name is {} and they are {} years old",
             person.name,
             person.age);
}

/// Define a struct type called "Person"
struct Person {
    name: String,
    age: u8,
}

/// Defines a function "new_person" that returns a Person struct
fn new_person(name: String, age: u8) -> Person {
    return Person {
        name: name,
        age: age,
    };
}

Functions and Structs

fn main() {
    let person = Person::new("Nick".to_owned(), 21);
    println!("Our person's name is {} and they are {} years old",
             person.name,
             person.age);
}

/// Define a struct type called "Person"
struct Person {
    name: String,
    age: u8,
}

impl Person {
    /// Defines a function "Person::new" that returns a Person struct
    pub fn new(name: String, age: u8) -> Person {
        Person { name, age }
    }
}

Functions and Structs

fn main() {
    let person = Person::new("Nick".to_owned(), 21);
    person.greet();
}

/// Define a struct type called "Person"
struct Person {
    name: String,
    age: u8,
}

impl Person {
    /// Defines a function "Person::new" that returns a Person struct
    pub fn new(name: String, age: u8) -> Person {
        Person { name, age }
    }

    pub fn greet(&self) {
        println!("Our person's name is {} and they are {} years old",
                 self.name,
                 self.age);
    }
}

Structs

Contain all of the defined fields

Enums

Contains one variant and potentially some data

struct Person {
    name: String,
    age: u8,
    address: String,
    email: String,
}
enum PrimaryContactInfo {
    Email(String),
    Phone(u32),
    DoNotContactMe,
}

Rust enums are sum types  (aka tagged unions)

Rust structs are product types

Enums and Pattern Matching

fn main() {
    let favorite_color = Color::Blue;
    let color_string = stringify_color(&favorite_color);
    println!("My favorite color is {}!", color_string);
}

enum Color {
    Red,
    Green,
    Blue,
}

fn stringify_color(color: &Color) -> String {
    match color {
        Color::Red => format!("Red"),
        Color::Green => format!("Green"),
        Color::Blue => format!("Blue"),
    }
}

Enums and Pattern Matching

fn main() {
    let favorite_color = Color::Blue;
    let color_string = stringify_color(&favorite_color);
    println!("My favorite color is {}!", color_string);
}

enum Color {
    Red,
    Green,
    Blue,
    Purple,
}

fn stringify_color(color: &Color) -> String {
    match color {
        Color::Red => format!("Red"),
        Color::Green => format!("Green"),
        Color::Blue => format!("Blue"),
        // COMPILE ERROR: pattern `&Purple` not covered
    }
}

Enums and Pattern Matching

fn main() {
    let favorite_color = Color::Blue;
    let color_string = stringify_color(&favorite_color);
    println!("My favorite color is {}!", color_string);
}

enum Color {
    Red,
    Green,
    Blue,
    Purple,
}

fn stringify_color(color: &Color) -> String {
    match color {
        Color::Red => format!("Red"),
        Color::Green => format!("Green"),
        Color::Blue => format!("Blue"),
        _ => format!("a non-RGB color"),
    }
}

Enums instead of null

Rust has no null type

Instead, there is an enum called Option<T>

pub enum Option<T> {
    None,
    Some(T),
}

We use Option<T> in cases where there may or may not be a T

e.g. Option<String>

Enums instead of null

pub enum Option<T> {
    None,
    Some(T),
}

Example: vector.get returns Some if an element exists or None if it doesn't

fn main() {
    let numbers: Vec<i32> = vec![ 10, 20, 30 ];
    let third: Option<i32> = numbers.get(2);
    match third {
        None => println!("There is no third element!"),
        Some(elem) => println!("The third element is {}", elem),
    }
}

Enums instead of null

pub enum Option<T> {
    None,
    Some(T),
}

Example: vector.get returns Some if an element exists or None if it doesn't

fn main() {
    let numbers: Vec<i32> = vec![ 10, 20, 30 ];
    let fourth: Option<i32> = numbers.get(3);
    match fourth {
        None => println!("There is no fourth element!"),
        Some(elem) => println!("The fourth element is {}", elem),
    }
}

Enums instead of exceptions

Rust has no exceptions

Instead, there is an enum called Result<T, E>

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

We use Ok(T) to represent a successful outcome with value T

We use Err(E) to represent a failed outcome with error E

Enums instead of exceptions

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}
fn main() {
    let root: Result<f64, String> = square_root(-4.0);
    match root {
        Ok(the_square_root) => println!("Success! Got {}", the_square_root),
        Err(problem) => println!("Problem with square root: {}", problem),
    }
}

fn square_root(num: f64) -> Result<f64, String> {
    if num < 0.0 {
        return Err("unable to take the square root of a negative number!".to_string());
    }
    
    Ok(num.sqrt())
}

When a function returns a Result, we can't access the internal data unless we pattern match

Error handling patterns

use std::io::Error as IoError;
use std::fs::read_to_string;

fn main() -> Result<(), IoError> {
    let file_result: Result<String, IoError> = read_to_string("./config.json");
    let file_contents: String = match file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };
    
    println!("{}", file_contents);
    Ok(())
}

Often, we want to just get the inner value if it's Ok, or return the error value to the caller if it's Err.

Error handling patterns

use std::io::Error as IoError;
use std::fs::read_to_string;

fn main() -> Result<(), IoError> {
    let file_contents: String = read_to_string("./config.json")?; // <- notice the '?'
    println!("{}", file_contents);
    Ok(())
}

With the ? operator, we can automatically return Err.

In this way, Errors are just data, but we can choose to

use them similarly to exceptions when it's suitable

Ownership &Borrowing

Ownership &Borrowing

The goal: Automatic memory management without garbage collection

Why?

Manual memory management leads to memory bugs and security vulnerabilities (problems in C, C++)

Garbage collection leads to unpredictable latency at runtime (the "stop the world" problem in Java, etc.)

Ownership

Every value in Rust has exactly one Owner.

After binding a value to a variable, we say that variable owns the value.

fn main() {
    let greeting = "Hello, world!".to_string();
}

Here, 'greeting' is the binding (aka variable),

and "Hello, world!" is the value

Ownership

If we re-assign a value to a new binding, we say the value has moved, and can no longer be accessed by the old binding.

fn main() {
    let greeting = "Hello, world!".to_string();
    let welcome = greeting;
    println!("{}", greeting);
}

Ownership

It has to do with how Rust knows when to clean up

Since every value is owned by exactly one variable, Rust can clean up as soon as that variable goes out of scope

Automatic memory management

Why does Rust do this?

fn say_that_greeting() {
    let greeting = "Hello, world!".to_string();
    println!("{}", greeting);
} // greeting goes out of scope and is dropped

Dropping runs the deconstructor and frees its memory

Ownership

fn main() {
    let greeting =
        "Hello, world".to_string();
    say(greeting);
}

fn say(thing: String) {
    println!("{}", thing);
}

fn main

greeting = "Hello, world";

Imagine the values living in the stack, in a stack frame

Ownership

When we pass a value to a function, we move it into that function's stack frame

fn main() {
    let greeting =
        "Hello, world".to_string();
    say(greeting);
}

fn say(thing: String) {
    println!("{}", thing);
}

fn main

thing = "Hello, world";

fn say

Ownership

When the function scope ends, values in that scope get dropped! This is where memory cleanup happens

fn main() {
    let greeting =
        "Hello, world".to_string();
    say(greeting);
    // 'greeting' has been moved away
}

fn say(thing: String) {
    println!("{}", thing);
} // 'thing' gets Dropped!

fn main

Ownership

If we try to use the value again, we get a compile-time error! Since the memory was freed, this is invalid

fn main() {
    let greeting =
        "Hello, world".to_string();
    say(greeting);
    //  -------- value moved here
    exclaim(greeting);
    //      ^^^^^^^^ value used here
    //               after move
}

fn say(thing: String) {
    println!("{}", thing);
}

fn exclaim(thing: String) {
    println!("Oh my, {}!", thing);
}

fn main

Ownership

Lets Rust know when it can clean up values

Prevents us from using values after they've been freed

Pros

Cons

Can only use values once...?

This is where Borrowing comes in

Borrowing

Allows us to access a value without moving it

fn main() {
    let greeting =
        "Hello, world".to_string();
    say(&greeting);
}

fn say(thing: &String) {
    println!("{}", thing);
}

fn main

greeting = "Hello, world";

Borrowing

We ask to borrow a type T using the ampersand: &T

fn main() {
    let greeting =
        "Hello, world".to_string();
    say(&greeting);
}

fn say(thing: &String) {
    println!("{}", thing);
}

fn main

greeting = "Hello, world";

fn say

thing = &greeting;

We call &T a reference to a T

Borrowing

However, we cannot mutate a value borrowed with &T

fn main() {
    let greeting = "Hello, world".to_string();
    dramatize(&greeting);
    
}

fn dramatize(thing: &String) {
    thing.make_ascii_uppercase();
}

Borrowing

Instead, we need to use a mutable reference: &mut T

fn main() {
    let mut greeting = "Hello, world".to_string();
    dramatize(&mut greeting);
    println!("{}", greeting);
}

fn dramatize(thing: &mut String) {
    thing.make_ascii_uppercase();
}

// Sneak peek from String:
impl String {
    pub fn make_ascii_uppercase(&mut self) { ... }
}

Borrowing

&T

&mut T

"Shared reference"

"Immutable reference"

"Unique/Exclusive reference"

"Mutable reference"

Why use multiple reference types?

To prevent data races

Borrowing

to PREVENT data races

A data race is a type of undefined behavior.

Data races require two conditions:

1. Shared memory being accessed by two threads/processes/actors at the same time

2. One of those accesses is a write (or mutation)

Borrowing

to PREVENT data races

Data races can cause security vulnerabilities and bugs

However, Rust's borrowing rules prevent all data races.

How?

By separating sharing (&T) and mutation (&mut T)

fn main() {
    let mut greeting: String = "Hello world".to_string();

    let shared: &String = &greeting;
    let mutable: &mut String = &mut greeting;

    println!("{}", shared);
    println!("{}", mutable);
}

Data structures: Arrays/Slices

(like arrays in other languages,)

arrays are fixed-length, contiguous blocks of memory for storing many of the same thing

let array: [u8; 5] = [104, 101, 108, 108, 111];

slices are references to a segment of an array.

They are made up of a pointer and a length

let slice: &[u8] = &array[1..4];
println!("{:?}", slice); // [101, 108, 108]

Data structures: Arrays/Slices

let array: [u8; 5] = [104, 101, 108, 108, 111];
let slice: &[u8] = &array[1..4];
println!("{:?}", slice); // [101, 108, 108]
index value
0 104
1 101
2 108
3 108
4 111

array =

name value
ptr
len 3

slice =

Data structures: Arrays/Slices

let array: [u8; 5] = [104, 101, 108, 108, 111];
let slice: &[u8] = &array[1..4];
println!("{:?}", slice); // [101, 108, 108]

println!("{}", slice[2]); // 108
println!("{}", slice[3]); // panic: 'index out of bounds'

Indexing into a slice is bounds-checked using the length

This makes it impossible to over-read into unclaimed memory

Data structures: Vectors

let vector: Vec<u8> = vec![104, 101, 108, 108, 111];

Vectors are heap-allocated, dynamically-growing arrays

index value
0 104
1 101
2 108
3 108
4 111
name value
ptr
len 5
capacity 5

vector =

Data structures: Vectors

let mut vector: Vec<u8> = vec![104, 101, 108, 108, 111];
vector.push(33);

Vectors are heap-allocated, dynamically-growing arrays

index value
0 104
1 101
2 108
3 108
4 111
name value
ptr
len 6
capacity 10

vector =

index value
0 104
1 101
2 108
3 108
4 111
5 33

free'd

Data structures: Vectors

let mut vector: Vec<u8> = vec![104, 101, 108, 108, 111];
vector.push(33);
let slice = &vector[2..5]; // [108, 108, 111]

We can use slices to view into a vector

name value
ptr
len 6
capacity 10

vector =

index value
0 104
1 101
2 108
3 108
4 111
5 33
name value
ptr
len 3

slice =

Data structures: Vectors

let mut vector: Vec<u8> = vec![104, 101, 108, 108, 111];
let slice = &vector[2..5]; // [108, 108, 111]
vector.push(33);
println!("{:?}", slice); // Compile-time error!

Rust protects us from memory unsafety

name value
ptr
len 6
capacity 10

vector =

index value
0 104
1 101
2 108
3 108
4 111
5 33
name value
ptr
len 3

slice =

index value
0 104
1 101
2 108
3 108
4 111

free'd

Data structures: Vectors

let mut vector: Vec<u8> = vec![104, 101, 108, 108, 111];
let slice = &vector[2..5]; // [108, 108, 111]
vector.push(33);
println!("{:?}", slice); // Compile-time error!

// error[E0502]: cannot borrow `vector` as mutable because it
//                              is also borrowed as immutable
//  --> src/main.rs:5:5
//   |
// 4 |     let slice = &vector[2..5]; // [108, 108, 111]
//   |                  ------ immutable borrow occurs here
// 5 |     vector.push(33);
//   |     ^^^^^^^^^^^^^^^ mutable borrow occurs here
// 6 |     println!("{:?}", slice);
//   |                      ----- immutable borrow later used here

Rust protects us from memory unsafety

Data structures: String/&str

let string: String = "hello".to_string();

Strings are just vectors whose data must be UTF-8

index value
0 h
1 e
2 l
3 l
4 o
name value
ptr
len 5
capacity 5

string =

Data structures: String/&str

let mut string: String = "hello".to_string();
string.push('!');

Strings are just vectors whose data must be UTF-8

index value
0 h
1 e
2 l
3 l
4 o
name value
ptr
len 6
capacity 10

string =

free'd

index value
0 h
1 e
2 l
3 l
4 o
5 !
6
7
8
9

Data structures: String/&str

let mut string: String = "hello".to_string();
string.push('!');
let ello: &str = &string[1..5];

String slices (spelled &str) are views into Strings

name value
ptr
len 6
capacity 10

string =

index value
0 h
1 e
2 l
3 l
4 o
5 !
6
7
8
9
name value
ptr
len 4

ello =

Data structures: String/&str

let string_literal: &'static str = "hello world";
let heap_string: String = string_literal.to_string();
let string_slice: &str = &heap_string[0..6];

String literals are string slices with the 'static lifetime

&'static references are valid for the full program lifetime

&'static str strings are compiled into the binary's .text section

Traits: shared behavior

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

traits are Rust's way to share behavior

Types may implement traits, then may be used wherever that trait is expected

Traits: shared behavior

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

struct Fibonacci {
    prev: i32,
    current: i32,
}

impl Iterator for Fibonacci {
    type Item = i32;
    
    fn next(&mut self) -> Option<Self::Item> {
        let next = self.prev + self.current;
        self.prev = self.current;
        self.current = next;
        Some(next)
    }
}

traits are Rust's way to share behavior

fn main() {
    let mut fib = Fibonacci {
        prev: 1,
        current: 1,
    };
    
    for _ in 0..10 {
        let next: Option<i32> = fib.next();
        println!("{:?}", next);
    }
}

Some familiar traits

/// Describes how to pretty-print some type
trait Display { ... }

/// Describes how to print the internals of some type
trait Debug { ... }

println!("{}", 1337);              // Uses 'Display' to print
println!("{:?}", vec![1, 2, 3]);   // Uses 'Debug' to print

Display and Debug are used for printing types

Rust examples

Intro to Rust

By Nick Mosher

Intro to Rust

  • 358