Presentation for the Frostbite Rendering Team (+ Friends!) by Daniel Collin / @daniel_collin
To Alex Crichton (of Mozilla Research) for allowing me to use some of his slides in this presentation.
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.
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.
C/C++ | Java/.NET | Python | |
More control, less safety |
Less control, more safety |
More control, more safety
Type system enforces ownership and borrowing
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
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
fn foo() -> Box<u32> {
let three = Box::new(3u32);
return three; // transfer ownership
}
fn main() {
// acquire ownership of return value
let my_three = foo();
}
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);
}
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 prevents moving
let a = Vec::new();
let b = &a;
work_with(a); // error!
let a = Vec::new();
{
let b = &a;
}
work_with(a); // ok
fn main() {
println!("Your number was: {}", *foo());
}
// Tries to return borrowed reference to `a`
fn foo() -> &u32 {
let a = 3u32;
return &a;
}
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`
}
Borrowed values can become owned values through cloning
fn clone_vector(v: &Vec<u32>) -> Vec<u32> {
v.clone()
}
Rust has fine-grained memory management, but is automatically managed once created.
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
}
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);
}
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>) {}
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 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
}
Borrowed pointers may coerce
let a = &mut 1;
let b: &i32 = a; // ok
let c: &mut i32 = b; // error!
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 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!
}
Using ownership to prevent data races.
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();
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!
}
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();
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"]));
}
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;
});
}
}
struct Point {
x: f32,
y: f32,
}
fn main() {
let p = Point { x: 1.0, y: 2.0 };
}
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"),
}
}
enum Shape {
Circle,
Square
}
fn test(shape: Shape) {
match shape {
Circle => { /* ... */ }
Square => { /* ... */ }
}
}
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)
}
}
trait Hello {
fn hello(&self);
}
impl Hello for Point {
fn hello(&self) {
println!("Hello!");
}
}
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!
}
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();
}
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
}
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) };
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
fn foo() {
unsafe {
asm!("nop");
}
}
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);
}
}
_ZN4main20h9abc3392beeae1c4saaE:
.cfi_startproc
leaq byte_str2755(%rip), %rsi
movl $1, %edi
movl $14, %edx
jmp write@PLT
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"
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 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)
Easy to test code. Lets say we have a project called adder
#[test]
fn it_works() {
}
$ 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 @rustlang: Borrow checker -> borrow checker -> borrow checker -> borrow checker -> understanding -> productivity -> happiness.
— Lori Holden (@lholden) September 8, 2015