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:
- Firefox's new rendering engine (https://servo.org/)
- Used in iotedge and other Microsoft components (https://msrc-blog.microsoft.com/2019/11/07/using-rust-in-windows/)
- Cloudflare's HTTP 3 implementation (https://blog.cloudflare.com/enjoy-a-slice-of-quic-and-rust/)
- AWS's Firecracker virtual machine monitor (https://firecracker-microvm.github.io/)
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
Stringwill 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
&Tis a non-null reference to an immutable object, similar toconst T&in C++.&mut Tis 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
- Package management (https://crates.io/)
- Documentation (https://docs.rs/)
- Build + test (https://github.com/rust-lang/crater)
C Interop
Rust offers easy interoperability with C, or any language that can provide a C interface (Fortran, C++, managed C++, etc).