Rust

Presentation for the Frostbite Rendering Team (+ Friends!) by Daniel Collin / @daniel_collin

Thanks!

To Alex Crichton (of Mozilla Research) for allowing me to use some of his slides in this presentation.

Agenda

  • Me and programming languages
  • What is Rust?
  • Ownership and Borrwing
  • Misc
  • Unsafe
  • Thoughts

Me and programming languages.

Aiming to try out 3 new programming languages every year.

See beyond the usual box.

Ideas, concepts can usually be applied in your "daily" language as well.

Spend more time with the ones I find most interesting.

What is Rust?


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

Rust grew out of a personal project by Mozilla employee Graydon Hoare.

Mozilla sponsored starting in 2009. Rust itself is community driven under MIT licence.

Why Rust?

C/C++ Java/.NET Python
More control,
less safety
Less control,
more safety
Rust

More control, more safety

Systems Programming Language

  • Fine grained control over allocations.
  • No required garbage collector.
  • Minimal runtime.
  • Statically typed.

Runs fast

  • Compiles to an executable binary.
  • LLVM backend (Same backend as Clang for Mac/iOS/PS4).
  • LLVM's suite of optimizations.
  • Competitive with C/C++.

Prevents almost all crashes

  • Safe by default.
  • Sophisticated type system and analysis.
  • No segfaults.
  • No null pointers.
  • No dangling pointers.

Eliminates data races

  • Ownership guarantees.
  • Borrowing prevents dangling pointers.
  • Strong, safe abstractions.

Ownership and borrowing

Type system enforces ownership and borrowing

  1. All resources have a clear owner.
  2. Others can borrow from the owner.
  3. Owner cannot free or mutate the resource while it is borrowed.

Ownership


int main() {
    int* data = malloc(sizeof(int));
    *data = 3;
    foo(slot);
    foo(slot); // use after free!
}

void foo(int* data) {
    printf("The number was: %d\n", *data);
    free(data);
}
							

a.out(62940,0x7fff7b9ea310) malloc: *** error for object 0x7fecb0c03b10:
pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
zsh: abort      ./a.out
					

Ownership


fn main() {
    let data = Box::new(3u32);
    foo(data); // moves the value!
    foo(data); // error: use of moved value
}

fn foo(slot: Box<u32>) {
    println!("The number was: {}", slot);
}
        

test.rs:6:9: 6:13 error: use of moved value: `data` [E0382]
test.rs:6     foo(data); // error: use of moved value
                 ^~~~
test.rs:5:9: 5:13 note: `data` moved here because it has type 'Box<u32>', 
                         which is non-copyable
test.rs:5     foo(data); // moves the value!
                 ^~~~
error: aborting due to previous error

Ownership


fn foo() -> Box<u32> {
    let three = Box::new(3u32);
    return three; // transfer ownership
}

fn main() {
    // acquire ownership of return value
    let my_three = foo();
}
						

Borrowing

Owned values can be borrowed in Rust to allow usage for a certain period of time.


// The `&` sigil means "borrowed reference"
fn foo(slot: &Vec<u32>) { /* ... */ }

fn main() {
    let data = Vec::new();

    // doesn't move!
    foo(&data);
    foo(&data);
}
        

Borrowing

Borrowed values are only valid for a particular lifetime


let a: &u32;
{
    let b = 3;
    a = &b; // error! `b` does not live long enough
}
        

let b = 3;
let a: &u32;
a = &b; // ok, `b` has the same lifetime as `a`
        

Borrowing

Borrowing prevents moving


let a = Vec::new();
let b = &a;
work_with(a); // error!
        

let a = Vec::new();
{
    let b = &a;
}
work_with(a); // ok
        

Borrowing

A mistake that happens in C/C++:

fn main() {
    println!("Your number was: {}", *foo());
}

// Tries to return borrowed reference to `a`
fn foo() -> &u32 {
    let a = 3u32;
    return &a;
}
        

Borrowing

Borrows can be nested


struct MyStruct { inner: u32 }

fn get(s: &MyStruct) -> &u32 {
    &s.inner
}

fn main() {
    let s = MyStruct { inner: 3u32 };
    let inner = get(&s); // same lifetime as `s`
}
        

Borrowing

Borrowed values can become owned values through cloning


fn clone_vector(v: &Vec<u32>) -> Vec<u32> {
    v.clone()
}
        

Memory Management

Rust has fine-grained memory management, but is automatically managed once created.

Memory Management

Each variable has a scope it is valid for, and it is automatically deallocated when it goes out of scope.


fn main() {
    // main owns 'data'
    let data = Box::new(4u32);
    // The data goes out of scope, it is free'd
}
        

Memory Management

Also possible to explicitly free data using drop


fn main() {
    // main owns 'data'
    let data = Box::new(4u32);
    // Do something with data here..
    // ...
    // Now free data
    drop(data);
    // Compile error if we try to use data 
    println!("Trying to use data {}", data);
}
        

Memory Management

Reference counting is another way of managing memory.


use std::rc::Rc;

fn main() {
    let data = Rc::new(3u32); // reference count of 1
    {
        let data2 = data.clone(); // reference count of 2
        use_data(data2); // transfer ownership of `data2`
    } // reference count of 1
    use_data(data); // transfer ownership of `data`
} // reference count of 0, memory deallocated

fn use_data(data: Rc<u32>) {}
        

Mutability

Values are immutable by default in Rust, and must be tagged as being mutable.


let a = 4;
a = 5; // error!
        

let mut a = 4;
a = 5; // ok
        

Mutability

Mutability is also a part of the type of a borrowed pointer.


fn inc(i: &i32) {
    *i += 1; // error!
}
        

fn inc(i: &mut i32) {
    *i += 1; // ok
}
        

Mutability

Borrowed pointers may coerce


let a = &mut 1;
let b: &i32 = a; // ok
let c: &mut i32 = b; // error!
        

Mutability

Values can be frozen by borrowing


let mut a = Vec::new();
{
    let b = &a; // freezes `a`
    a.push(1);  // error!
}
a.push(2); // ok
        

Mutability

Mutability propagates deeply into owned types


struct A { b: B }
struct B { c: i32 }
fn main() {
    let mut a = A { b: B { c: 2 } };
    a.b.c = 3;
    a.b = B { c: 4 };
    a = A { b: B { c: 5 } };

    let frozen = a;
    frozen.b.c = 4; // error!
}
        

Concurrency

Using ownership to prevent data races.

Concurrency

Parallelism is achieved at the granularity of an OS thread but it's also possible to use libraries like threadpool.


// Spawn a child thread to be run in parallel
spawn(move || {
    expensive_computation();
});

other_expensive_computation();
        

Concurrency

Safety is achieve by requiring spawn to use 'move' closure to capture variables.


use std::thread;

fn main() {
    let mut a = Vec::new();
    thread::spawn(move || {
        a.push("foo");
    });
    a.push("bar"); // error!
}
        

Concurrency

Threads can communicate with channels


let (tx, rx) = channel();

spawn(move || {
    tx.send(expensive_computation());
});

// Do some work in the meantime

let answer = rx.recv();
        

Concurrency

Tasks can also share memory


use std::collections::HashMap;
use std::sync::Arc;
use std::thread;

fn main() {
    let mut map = HashMap::new();
    map.insert("tomato", "red");
    map.insert("celery", "green");
    map.insert("carrot", "orange");

    let arc1 = Arc::new(map);
    let arc2 = arc1.clone();

    spawn(move || println!("Celery is `{}`", arc1["celery"]));
    spawn(move || println!("Carrots are `{}`", arc2["carrot"]));
}
        

Concurrency

Using a Mutex it's possible to modify shared state


use std::sync::{Arc, Mutex};
use std::thread;

// Mutex is design so it be owner of a piece of data and
// use borrowing to lend it to others 
fn main() {
    let data = Arc::new(Mutex::new(4u32));
    for _ in 0..10 {
       let thread_data = data.clone();
       thread::spawn(move || {
           let mut data = thread_data.lock().unwrap();
           *data += 1;
        });
    }
}
     

Structs


struct Point {
    x: f32,
    y: f32,
}

fn main() {
    let p = Point { x: 1.0, y: 2.0 };
}
        

Match



fn main() {
  let number = 13;

  println!("Tell me about {}", number);
  match number {
    // Match a single value
    1 => println!("One!"),
    // Match several values
    2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
    // Match an inclusive range
    13...17 => println!("A teen"),
    // Handle the rest of cases
    _ => println!("Ain't special"),
  }
}
        

Enums


enum Shape {
    Circle,
    Square
}

fn test(shape: Shape) {
    match shape {
        Circle => { /* ... */ }
        Square => { /* ... */ }
    }
}
        

Enums

Enums can have data. This is compiled into a union + tag


enum Shape {
    Circle(Point, f64),
    Rectangle(Point, Point),
}

match shape {
    Circle(center, radius) => {
        draw_circle(center, radius)
    }
    Rectangle(ul, lr) => {
        draw_rectangle(ul, lr)
    }
}
        

Traits


trait Hello {
    fn hello(&self);
}
        

Traits - Impl


impl Hello for Point {
    fn hello(&self) {
        println!("Hello!");
    }
}
        

Traits - Impl

Traits can also be applied on existing types


trait Hello {
    fn hello(&self);
}
impl Hello for u32 {
    fn hello(&self) {
        println!("Hello!");
    }
}
fn main() {
    4u32.hello(); // Considered bad style!
}
        

Modules

Rust has modules to split code in logical units


fn function() {
    println!("Called 'function'");
}

mod my {
    pub fn function() {
        println!("Called 'my::function'");
    }
}

fn main() {
    function();
    my::function();
}
        

Unsafe - Break the rules

  • Possible to skip some compiler checks.
  • Used to build safe code around unsafe blocks.
  • Can audit only unsafe blocks for issues.
  • FFI (Foreign Function Interface) is always unsafe.

Unsafe


fn main() {
    let a = 3;

    // Turn off some compiler checks,
    // "I know what I'm doing"
    unsafe {
        let b = &a as *const i32 as *mut i32;
        *b = 4;
    }

    println!("{}", a); // prints 4
}
        

Transmuting

Transform one type to another


unsafe fn transmute<T, U>(t: T) -> U {
    /* ... */
}
        

// Invalid promotion to a mutable pointer
let a: &int = &3;
let b: &mut int = unsafe { transmute(a) };
    

Inline assembly


#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
fn foo() {
    unsafe {
        asm!("nop");
    }
}
    

FFI


extern {
    fn write(fd: i32, data: *const u8, len: u32) -> i32;
}

fn main() {
    let data = b"Hello, world!\n";

    unsafe {
        write(1, &data[0], data.len() as u32);
    }
}
		

Fast  FFI


_ZN4main20h9abc3392beeae1c4saaE:
	.cfi_startproc
	leaq	byte_str2755(%rip), %rsi
	movl	$1, %edi
	movl	$14, %edx
	jmp	write@PLT
     

Cargo

  • Cargo is Rusts package manager.
  • Makes it possible to download dependencies for a project.
  • Invokes rustc or other tools to build projects.
  • Uses crates for packaging up modules in a library.
  • Introduces conventions, making working with Rust projects easier.

Cargo

Defining a Cargo.toml file for a project.


[package]
name = "hello_world"
version = "0.1.0"
authors = ["Your Name you@example.com"]

[dependencies]
regex = "0.1.41"
     

Cargo


extern crate regex;

use regex::Regex;

fn main() {
    let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
    println!("Did our date match? {}", re.is_match("2014-01-01"));
}
     

Cargo


$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading memchr v0.1.5
 Downloading libc v0.1.10
 Downloading regex-synatx v0.2.1
 Downloading memchr v0.1.5
 Downloading aho-corasick v0.3.0
 Downloading regex v0.1.41
   Compiling memchr v0.1.5
   Compiling libc v0.1.10
   Compiling regex-synatx v0.2.1
   Compiling memchr v0.1.5
   Compiling aho-corasick v0.3.0
   Compiling regex v0.1.41
   Compiling foo v0.1.0 (file:///path/to/project/hello_world)
    

Cargo

Easy to test code. Lets say we have a project called adder


#[test]
fn it_works() {
}
    

Cargo


$ cargo test
   Compiling adder v0.0.1 (file:///home/you/projects/adder)
     Running target/adder-91b3e234d4ed382a

running 1 test
test it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
    

Learning Rust

Learning Rust

  • Getting used to ownership and borrowing can be painful.
  • Fight with the compiler that doesn't do what you want.
  • Mainly comes down to understanding the language.

Rust is what C++ should have been

  • C++ suffers from C legacy.
  • I like C but mixing (the C++ way) isn't a good idea.
  • Rust feels like a modern system lang without C legacy.

So should we use it?

  • It would be possible to use for tool code.
  • Using it in the runtime we need console support.
  • It's still a young language (1.0 released 2015, May 15)
  • Hard to predict the future.

What else?

  • The Rust community is awesome and very welcoming.
  • Documentation was better than I expected.
  • I will continue to fiddle around with Rust.
  • I have considered using it for my debugger project.

Links