Introduction

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

Rust is a modern language aiming to be as fast and expressive as C++ without memory-unsafety

Notable users:

This presentation is generated using a Rust tool. The editable code examples are powered by https://play.rust-lang.org/.

Safety

Rust makes strong guarantees about safety

  • No null or invalid pointers
  • No data races
  • Individual APIs may use the type system to provide their own guarantees. For example String will always be valid utf-8.

To avoid sacrificing performance, Rust provides the unsafe keyword. This enables operations the compiler cannot validate like dereferencing arbitrary pointers, mutating global state, or calling into C code. The Rust typesystem allows writing safe abstractions around these operations.

Ownership

  • 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 value will be dropped.
// this function takes ownership of `v`
fn print_vector(v: Vec<i32>) {
    println!("{:?}", v);

    // `v` is deallocated here
}

fn main() {
    let v = vec![1, 2, 3];

    print_vector(v); // `v` is moved, transferring ownership

    // trying to use `v` here is a compile error
    // v.len();
}

This style of resource management is similar to how RAII types like unique_ptr work in C++.

Unlike C++, Rust does not have move constructors. A move is always just a memcpy.

Simple types like integers can still be used after being moved. This behaviour is controlled by the Copy trait.

Borrowing

Rust has two kinds of references

  • &T is a non-null reference to an immutable object, similar to const T& in C++.
  • &mut T is a mutable (or unique) reference.

The core of Rust's safety guarantees is the "shared xor mutable" rule. An object can be borrowed by either a single &mut reference, or multiple & references.

fn main() {
    // `let mut` defines a mutable variable. By default variables are immutable.
    let mut list = vec![1, 2, 3];

    // Creating an iterator borrows `list`
    let mut iter = list.iter();
    println!("{:?}", iter.next());

    // We cannot mutate `list` here while it is borrowed
    // list.push(4);

    println!("{:?}", iter.next());
}

Threading

Rust's borrowing model allows writing thread-safe abstractions like a mutex.

This allows safely mutating a value through a shared reference.

use std::sync::{Arc, Mutex};
use std::rc::Rc;
use std::thread;
use std::time::Duration;

fn main() {
    // Create a reference-counted, mutex-protected integer
    let value = Arc::new(Mutex::new(0));

    // Create another reference to the integer and move it onto another thread
    let cloned_value = value.clone();
    thread::spawn(move || {
        // Lock the mutex
        let mut lock_guard = cloned_value.lock().unwrap();

        // Since the mutex is locked, we can safely get a `&mut` reference to
        // mutate the integer
        *lock_guard = 1;

        // The mutex is unlocked when `lock_guard` goes out of scope
    });

    println!("{}", value.lock().unwrap());
    thread::sleep(Duration::from_millis(20));
    println!("{}", value.lock().unwrap());
}

Note this is safe because the Arc type uses atomic operations for reference counting, so it can be used from multiple threads. This is described by the Send trait. If we use the non-atomic version, we get an error.

Threading

rayon is a Rust library for parallelizing work. It uses the typesystem to provide a safe API.

#![allow(unused_imports)]

extern crate rayon;

use std::sync::atomic::{AtomicI32, Ordering};
use std::iter::FromIterator;
use rayon::prelude::*;

fn main() {
    // Create a list of integers from 0 to 100
    let range = Vec::from_iter(0..100);

    let mut total = 0;

    range.iter().for_each(|&n| {
        total += n;
    });

    println!("{}", total);
}

Note the difference in the signatures of the two for_each methods

Fn and FnMut are traits describing closures. FnMut closures need mutable access to their environment, whereas Fn closures only need shared access and so are safe to run concurrently.

Enums

Rust does not have the concept of null references. All objects are always valid instances of their type. Instead Rust has enums, which can be one of several variants.

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

Rust will force you to handle all variants of an enum.

fn main() {
    let array: Vec<i32> = vec![];

    match array.last() {
        Some(value) => println!("The last value is: {}", value),
        None => println!("The array is empty"),
    }
}

Tooling

Rust has excellent tooling

C Interop

Rust offers easy interoperability with C, or any language that can provide a C interface (Fortran, C++, managed C++, etc).