write secure code. - It’s very difficult to write multithreaded code. These are the problems Rust was made to address. 1 - Systems languages have come a long way in the last 50 years - First virus based on a buffer overflow appeared in 1988 - According to Open Source Vulnerability Database, still 10-15% of reported vulns during last 8 years are buffer overflows 2 - Multithreading is becoming more needed with multicore CPUs - C++ like threading is incredibly hard, even experienced programmers write hard to reproduce bugs
Started by Mozilla employee Graydon Hoare - First announced by Mozilla in 2010 - Community driven development - First stable release: 1.0 in May 2015 - Latest stable release: 1.3 - 46'484 commits on Github - Largest project written in Rust: Servo
unsigned long a[1]; a[3] = 0x7ffff7b36cebUL; return 0; } According to C99, undefined behavior. Output: undef: Error: .netrc file is readable by others. undef: Remove password or make file unreadable by others. 1 - We’re overwriting the return address on the stack. - Jumping right into libc 2 - The user is responsible for safety - We’re not good at that
that no possible execution can exhibit undefined behavior, we say that program is well defined. - If a language’s type system ensures that every program is well defined, we say that language is type safe
type safe. - Python is type safe: >> a = [0] >>> a[3] = 0x7ffff7b36ceb Traceback (most recent call last): File "", line 1, in <module> IndexError: list assignment index out of range >>> - Java, JavaScript, Ruby, and Haskell are also type safe. 1 - Our sample program had no type erorrs, yet exhibits undefined behavior. 2 - An exception is no undefined behavior. 3 - Every program that type safe languages accept is well defined.
Yet they are being used to implement the foundations of a system. Rust tries to resolve that tension. - Rust also allows unsafe code. But the great majority of programs does not need unsafe code.
-> u64 { assert!(n != 0 && m != 0); while m != 0 { if m < n { let t = m; m = n; n = t; } m = m % n; } n } - Program that calculates the greatest common denominator
-> u64 { assert!(n != 0 && m != 0); while m != 0 { if m < n { let t = m; m = n; n = t; } m = m % n; } n } The fn keyword introduces a function definition. Arrow denotes return value.
-> u64 { assert!(n != 0 && m != 0); while m != 0 { if m < n { let t = m; m = n; n = t; } m = m % n; } n } - Rust variable declarations: Name followed by type - [uif](8|16|32|64) and usize / isize
-> u64 { assert!(n != 0 && m != 0); while m != 0 { if m < n { let t = m; m = n; n = t; } m = m % n; } n } - Type is inferred from context or suffix. Otherwise i32.
-> u64 { assert!(n != 0 && m != 0); while m != 0 { if m < n { let t = m; m = n; n = t; } m = m % n; } n } - let introduces a local variable - Type is inferred
-> u64 { assert!(n != 0 && m != 0); while m != 0 { if m < n { let t = m; m = n; n = t; } m = m % n; } n } - Loops and conditions don’t need parentheses, only braces around body
-> u64 { assert!(n != 0 && m != 0); while m != 0 { if m < n { let t = m; m = n; n = t; } m = m % n; } n } - Blocks are expressions - return statement optional
-> T { if a <= b { a } else { b } } - Text marks it as generic function - Defined for any type T where T is Ord - If a type is Ord, it supports a comparison - Ord is a Trait, will be handled later
-> T { if a <= b { a } else { b } } ... min(10i8, 20) == 10; // T is i8 min(10, 20u32) == 10; // T is u32 min(“abc”, “xyz”) == “abc”; // Strings are Ord min(10i32, “xyz”); // error: mismatched types. -
- Algebraic Datatypes - For any type T, an Option<T> may be either: - None, which carries no value - Some(v) which carries the value v of type T - Resemble unions in C / C++, but remember their value type
i32) -> Option<i32> { if d == 0 { return None; } Some(n / d) } - Function that returns a division result, or None if divisor is 0 - (Could also be written as a single expression)
None => println!(“No quotient.”), Some(v) => println!(“Quotient is {}.”, v) } - Similar to a switch statement, but more powerful - Rust offers combinator methods for simplification, more on this later
y: f64, radius: f64, } impl HasArea for Circle { fn area(&self) -> f64 { consts::PI * (self.radius * self.radius) } } - Traits are implemented for structs - Other way of looking at it: Methods (behavior) are attached to data
} trait FooBar : Foo { fn foobar(&self); } - Any trait implementing FooBar must also implement Foo - Example: A trait Number that requires to implement Add, Sub, Mul, Div
No dangling pointers - No buffer overruns 1 - Your program will not crash because you tried to dereference a null pointer 2 - Every value will live as long as it must 3 - Your program will never access elements outside of an array All ensured at compile time. What do they mean?
useful - They can indicate the absence of optional information - They can indicate failures - But they can introduce severe bugs - Rust separates the concept of a pointer from the concept of an optional or error value - Optional values are handled by Option<T> - Error values are handled by Result<T, E> - Many helpful tools to do error handling
fn safe_div(n: i32, d: i32) -> Result<i32, Error> { if d == 0 { return Err(Error::DivisionByZero); } Ok(n / d) } - Good practice to define your own error types instead of using strings
a = match do_subcalc1() { Ok(val) => val, Err(msg) => return Err(msg), } let b = match do_subcalc2() { Ok(val) => val, Err(msg) => return Err(msg), } Ok(a + b) } - Calling a lot of functions returning a result can become tedious
let a = try!(do_subcalc1()); let b = try!(do_subcalc2()); Ok(a + b) } - The try! macro does the same thing, unwrap or early return - Error signature must match! - What if the errors don’t match?
} fn do_calc() -> Result<i32, Error> { let res = do_subcalc(); let mapped = res.map_err(|msg| { println!(“Error: {}”, msg); Error::CalcFailed }); let val = try!(mapped); Ok(val + 1) } - Convert them with helper methods - map_err passes through a successful result while handling an error - Explanation on next slide
or U. Option.map<U, F>(self, f: F) -> Option<U> where F: FnOnce(T) -> U Option.map_or<U, F>(self, default: U, f: F) -> U where F: FnOnce(T) -> U Option.map_or_else<U, D, F>(self, default: D, f: F) -> U where F: FnOnce(T) -> U, D: FnOnce() -> U
result, mapping Some(v) to Ok(v) and None to Err(err). Option.ok_or<E>(self, err: E) -> Result<T, E> Option.ok_or_else<E, F>(self, err: F) -> Result<T, E> where F: FnOnce() -> E - This is only a small selection. - There are similar methods on Result and others. - They all make it easier to work without having null pointers.
to access a heap-allocated value after it has been freed. - No garbage collection or reference counting involved! - Everything is enforced at compile time. 1 - Not an unusual promise, all type safe languages do this 2 3 - How is this done?
single owner at any given time. - Rule 2: You can borrow a reference to a value, so long as the reference doesn’t outlive the value. - Rule 3: You can only modify a value when you have exclusive access to it. - 1: You can move a value from one owner to another, but when a value’s owner goas away the value is freed along with it. - 2: Borrowed references are temporary pointers; they allow you to operate on values you don’t own.
owns its fields - An enum owns its values - Every heap-allocated value has a single pointer that owns it - All values are dropped when their owner is dropped -
foo = pi; let bar = pi; // This is fine! } - Types that implement the “Copy” trait (usually primitive types) are copied implicitly - Examples: char, bool, numeric types
t1 = s.clone(); let t2 = s.clone(); } - Other types can implement Clone trait for explicit cloning - Three independent String objects - Each is owned by the variable binding
{ r: u8, g: u8, b: u8 } - Implementing Copy and Clone is trivial for most types - So it can be auto-generated by the compiler - All values must be Copy / Clone too
print_with_umpff(s); println!(“{}”, s); error: use of moved value: `s` println!(“{}”, s); ^ note: `s` moved here because it has type `collections::string:: String`, which is non-copyable print_with_umpff(s); ^ - Now you know move semantics - Can cause problems though. - Does someone see the problem? - Ownership is moved into function and freed when function returns
s); println!(“New value is {}”, s); - A mutable borrow grants exclusive access - Only one mutable borrow possible at a time - While you borrow a mutable reference to a value, that reference is the only way to access that value at all.
= &x; let y = x; // error: cannot move out of `x` because // it is borrowed - While borrowed, a move must be prevented - Otherwise you might end up with a dangling pointer
&x; // error: `x` does not live // long enough - What is the problem here? - Lifetime of borrow is longer than lifetime of x - This can also be visualized differently:
borrow = &x; // error: `x` does not live // long enough } } - Now it should be obvious. - Using lifetime checking, the compiler guarantees that there are no dangling pointers.
data1 = data.clone(); let t1 = thread::spawn(move || { let mut guard = data1.lock().unwrap(); *guard += 19; }); let data2 = data.clone(); let t2 = thread::spawn(move || { let mut guard = data2.lock().unwrap(); *guard += 23; }); - Arc allow multiple references to the same data. (Safe pointers.) Arcs can be cloned. - Value of an Arc gets dropped when references are 0. - Mutexes own their values. Using lock() acquires mutex. - Locking returns a MutexGuard as proxy. When guard is dropped, lock is released. - Not posible to forget about releasing. - Arc pointer moved into closures
data.lock().unwrap(); assert_eq!(*guard, 42); - Threads need to be joined, otherwise result might not yet be ready. - Again, we need to acquire a Mutex lock. - We don’t need another Arc reference though.