Rust
A safe alternative to C++
@jupp0r
Follow the slides at the URL on the FlipChart
Copy source code into http://play.integer32.com
Don't hesitate to ask questions.
Long discussion at the end.
Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.
Control + Performance
Safety
C
C++
Java
JS
Python
Ruby
Rust = Control + Performance + Safety
fn main() {
println!("Hello, world!");
}
Hello, World!
fn main() {
let x = 5;
Everything is an Expression
let y = {
let z = 5;
z + 12
};
let is_big = if y > 12 { true } else { false };
println!("x={}, y={}, is_big={}", x, y, is_big);
}
// output:
x=5, y=17, is_big=true
expression of type ()
expression of type int
expression of type int
expression of type bool
expression of type ()
fn add_one(x: i32) -> i32 {
x + 1
}
Functions
function name
parameter name
parameter type
return type
return
expression
fn main() {
let x = 5;
x = 6;
println!("{}", x);
}
Immutability by default
// compiler error:
error[E0384]: re-assignment of immutable variable `x`
--> <anon>:3:5
|
2 | let x = 5;
| - first assignment to `x`
3 | x = 6;
| ^^^^^ re-assignment of immutable variable
fn main() {
let mut x = 5;
x = 6;
println!("{}", x);
}
Zero allocations by default
- no implicit copies
- parameters passed by move per default
- types are noncopyable by defalut
- implicit and explicit copies possible via marker traits
Rust is safe
No Segfaults
No Data Races
You get that at no runtime cost
Ownership + Borrowing
- unique (as of September 2016) feature of the rust programming language
- each memory address is assigned a single scope that owns the memory
- memory is deallocated when scope ends
- compiler checks correctness at compile time
- automatic memory management with zero runtime overhead (as opposed to Garbage Collection)
fn take(x: Vec<i64>) {
// what happens in here is not important
}
Ownership
fn main() {
let v = vec![5,6,7];
take(v);
println!("{}", v[0]);
}
// compiler error:
error[E0382]: use of moved value: `v`
--> <anon>:8:20
|
7 | take(v);
| - value moved here
8 | println!("{}", v[0]);
| ^ value used here after move
fn take(x: Vec<i64>) -> Vec<i64> {
// what happens in here is
// not important
x
}
Ownership transfer
function takes ownership of x
ownership is returned to caller
fn main() {
let x = vec![5,6,7];
let y = take(x);
println!("{}", y[0]);
}
This prevents lots of common programming errors at compile time:
- iterator invalidation
- use after free
- data races
Any borrow must last for a scope no greater than that of the owner. Second, you may have one or the other of these two kinds of borrows, but not both at the same time:
- one or more references (&T) to a resource
- exactly one mutable reference (&mut T).
Borrowing
fn append2(x: &mut Vec<i64>) {
x.push(2);
}
fn main() {
let mut x = vec![5, 6, 7];
append2(&mut x);
println!("{:?}", x);
}
// output: [5, 6, 7, 2]
Borrowing
fn main() {
let mut x = 5;
let y = &mut x; // -+ &mut borrow of x starts here
// |
*y += 1; // |
// |
println!("{}", x); // -+ - try to borrow x here
} // -+ &mut borrow of x ends here
Borrow Checking
// compiler error:
error[E0502]: cannot borrow `x` as immutable
because it is also borrowed as mutable
--> <anon>:7:20
|
3 | let y = &mut x;
| - mutable borrow occurs here
...
7 | println!("{}", x);
| ^ immutable borrow occurs here
8 | }
| - mutable borrow ends here
Borrow Checking
fn main() {
let mut x = 5;
{
let y = &mut x;// -+ &mut borrow of x starts here
// |
*y += 1; // |
} // -+ &mut borrow of x ends here
println!("{}", x); // -+ borrow x here
}
// output: 6
Objects
struct Data {
number: i64,
}
impl Data {
fn new(n: i64) -> Data {
Data { number: n }
}
fn set_number(&mut self, n: i64) {
self.number = n;
}
fn number(&self) -> i64 {
self.number
}
}
fn main() {
let mut data = Data::new(5);
data.set_number(7);
println!("{}", data.number());
}
Traits and Generics
struct Circle {
x: f64,
y: f64,
radius: f64,
}
trait HasArea {
fn area(&self) -> f64;
}
impl HasArea for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
}
fn print_area<T: HasArea>(shape: T) {
println!("This shape has an area of {}", shape.area());
}
trait bound
fn main() {
print_area(Circle{x: 0f64, y: 0f64, radius: 1f64});
}
Error Handling
fn divide(x: f64, y: f64) -> Option<f64> {
if y == 0f64 {
Option::None
} else {
Option::Some(x / y)
}
}
fn main() {
// let z = divide(5.0, 0.0).expect("cannot divide by zero"); // will panic
let z = divide(4.0, 2.0).unwrap();
println!("{}", z);
}
Type System
enum Option<T> {
None,
Some(T),
}
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
if denominator == 0.0 {
None
} else {
Some(numerator / denominator)
}
}
fn main {
let result = divide(2.0, 3.0);
match result {
Some(x) => println!("Result: {}", x),
None => println!("Cannot divide by 0"),
};
}
Iterator Trait
// from rusts std library
trait Iterator<T> {
fn next() -> Option<T>;
}
fn main() {
let v = vec![1, 12, 8, 17, 34, 19, 2, 1, 19, 32, 37]
.into_iter() // convert vector into iterator
.filter(|x| x % 2 == 0) // filter even numbers
.map(|x| x / 2) // divide numbers by two
.collect::<Vec<_>>(); // collect iterator into vector
println!("{:?}", v);
}
// output: [6, 4, 17, 1, 16]
Let's build an unique() iterator adapter
struct UniqueIterator<I,T>
{
iter: I,
seen_elements: HashSet<T>,
}
impl<I, T> Iterator for UniqueIterator<I, T>
where I: Iterator<Item = T>,
T: Hash + Eq + Clone
{
type Item = T;
fn next(&mut self) -> Option<T> {
let seen_elements = &mut self.seen_elements;
match self.iter.find(|x: &Self::Item| -> bool {
!seen_elements.contains(x) }) {
None => None,
Some(element) => {
seen_elements.insert(element.clone());
Some(element)
}
}
}
}
Let's build an unique() iterator adapter
trait UniqueIteratorAdapter<T>: Iterator<Item = T> + Sized
where T: Hash + Eq + Clone
{
fn unique(self) -> UniqueIterator<Self, T> {
UniqueIterator {
iter: self,
seen_elements: HashSet::new(),
}
}
}
impl<I, T> UniqueIteratorAdapter<T> for I
where I: Iterator<Item = T> + Sized,
T: Hash + Eq + Clone
{
}
fn main() {
let data = vec![1, 12, 15, 1, 12, 3, 5].into_iter().unique().collect::<Vec<_>>();
println!("{:?}", data);
}
// output:
[1, 12, 15, 3, 5]
impl<I, T> UniqueIteratorAdapter<T> for I
where I: Iterator<Item = T> + Sized,
T: Hash + Eq + Clone
{
}
fn main() {
let data = vec![1, 12, 15, 1, 12, 3, 5].into_iter()
.unique().collect::<Vec<_>>();
println!("{:?}", data);
}
// output:
[1, 12, 15, 3, 5]
Concurrency
Prelude: Closures
fn main() {
let plus_one = |x: i32| x + 1;
let y = plus_one(1);
println!("{}", y);
}
// output: 2
fn make_adder(x: i64) -> Box<Fn(i64) -> i64> {
Box::new(move |y: i64| x + y)
}
fn main() {
let plus_one = make_adder(1);
let y = plus_one(1);
println!("{}", y);
}
// output: 2
Rusts Concurrency Story
- like C++ (but unlike other languages like Go), Rust does not have much in the language itself to leverage concurrency
- but Rust is a great language for writing concurrent programs!
- type system features that enable Rust to avoid segfaults also enable safe usage of concurrency
- safe usage of concurrency means: no data races, no dangling pointers, no undefined behavior, no shutdown bugs, ....
Threads
fn main() {
thread::spawn(
|| println!("Hello from another thread!"));
}
Thread Safety
fn main() {
let mut data = vec![1, 2, 3];
for i in 0..3 {
thread::spawn( || {
data[0] += i;
});
}
thread::sleep(
Duration::from_millis(50));
}
error[E0373]: closure may outlive the current function, but it borrows `data`,
which is owned by the current function
--> src/main.rs:8:24
|
8 | thread::spawn( || {
| ^^ may outlive borrowed value `data`
9 | data[0] += i;
| ---- `data` is borrowed here
|
help: to force the closure to take ownership of `data` (and any other referenced variables),
use the `move` keyword, as shown:
| thread::spawn( move || {
Thread Safety
fn main() {
let mut data = vec![1, 2, 3];
for i in 0..3 {
thread::spawn(move || {
data[0] += i;
});
}
thread::sleep(
Duration::from_millis(50));
}
error[E0382]: capture of moved value: `data`
--> src/main.rs:9:13
|
8 | thread::spawn(move || {
| ------- value moved (into closure) here
9 | data[0] += i;
| ^^^^ value captured here after move
|
= note: move occurs because `data` has type `std::vec::Vec<i32>`,
which does not implement the `Copy` trait
Thread Safety
fn main() {
// Rc is a reference counted smart pointer
let mut data = Rc::new(vec![1, 2, 3]);
for i in 0..3 {
// create a new owned reference
let data_ref = data.clone();
// use it in a thread
thread::spawn(move || {
data_ref[0] += i;
});
}
thread::sleep(Duration::from_millis(50));
}
error[E0277]: the trait bound `std::rc::Rc<std::vec::Vec<i32>>: std::marker::Send` is not satisfied
--> src/main.rs:13:9
|
13 | thread::spawn(move || {
| ^^^^^^^^^^^^^ trait `std::rc::Rc<std::vec::Vec<i32>>: std::marker::Send` not satisfied
|
= note: `std::rc::Rc<std::vec::Vec<i32>>` cannot be sent between threads safely
= note: required because it appears within the type `[closure@src/main.rs:13:23: 15:10 i:i32,
data_ref:std::rc::Rc<std::vec::Vec<i32>>]`
= note: required by `std::thread::spawn`
Thread Safety
fn main() {
// Arc is a atomic reference counted smart pointer
let mut data = Arc::new(vec![1, 2, 3]);
for i in 0..3 {
// create a new owned reference
let data_ref = data.clone();
// use it in a thread
thread::spawn(move || {
data_ref[0] += i;
});
}
thread::sleep(Duration::from_millis(50));
}
error: cannot borrow immutable borrowed content as mutable
--> src/main.rs:14:13
|
14 | data_ref[0] += i;
| ^^^^^^^^
Thread Safety
fn main() {
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
for i in 0..3 {
let data = data.clone();
thread::spawn(move || {
let mut data = data.lock().unwrap();
data[0] += i;
});
}
thread::sleep(Duration::from_millis(50));
}
This compiles because it is free of data races!
Channels
fn main() {
let (tx, rx) = mpsc::channel();
for i in 1..10 {
let tx = tx.clone();
thread::spawn(move || {
let answer = i * i;
tx.send(answer).unwrap();
});
}
for _ in 1..10 {
println!("{}", rx.recv().unwrap());
}
}
// output: 49 64 81 36 25 16 9 4 1 (order indeterministic)
Futures
fn main() {
let mut core = Core::new().unwrap();
let addr = "www.rust-lang.org:443".to_socket_addrs()
.unwrap().next().unwrap();
let socket = TcpStream::connect(&addr, &core.handle());
let tls_handshake = socket.and_then(|socket| {
let cx = ClientContext::new().unwrap();
cx.handshake("www.rust-lang.org", socket)
});
let request = tls_handshake.and_then(|socket| {
tokio_core::io::write_all(socket, "\
GET / HTTP/1.0\r\n\
Host: www.rust-lang.org\r\n\
\r\n\
".as_bytes())
});
let response = request.and_then(|(socket, _)| {
tokio_core::io::read_to_end(socket, Vec::new())
});
let (_, data) = core.run(response).unwrap();
println!("{}", String::from_utf8_lossy(&data));
}
Tooling
Developer Productivy
- not does Rust make developers productive by eliminating memory errors and data races
- but it also comes with lots of tooling to make development productive
- cargo - a package manager, build tool and test runner
- rustup - installs and runs different rust (cross) toolchains
- debugging in recent versions of lldb, gdb, Visual Studio
- rustc can produce static binaries, so docker containers can be very small (~10mb)
- documentation tooling
- code formatting
- static analysis
cargo and crates.io
Error Handling
enum Option<T> {
None,
Some(T),
}
impl<T> Option<T> {
// lots of other methods omitted
fn expect(self, msg: &str) -> T {
match self {
Some(T) => T,
None => panic!(msg),
}
}
fn unwrap(self) -> T {
match self {
Some(T) => T,
None => panic!("Unwrap on None option"),
}
}
}
Creating a Rust project and using a 3rd party json serialization library
Documentation as tests
/// Circle represents a set of all points that have the same distance
/// to a point (x,y)
///
/// # Examples
///
/// ```
/// let circle = rustdoc_test::Circle{x: 0f64, y: 0f64, r: 10f64, z: 1};
/// println!("area: {}", circle.area());
/// ```
pub struct Circle {
pub x: f64,
pub y: f64,
pub r: f64,
}
impl Circle {
pub fn area(&self) -> f64 {
std::f64::consts::PI * self.r * self.r
}
}
Documentation as Tests
cargo test
Compiling rustdoc-test v0.1.0 (file:///Users/jupp/projects/rustdoc-test)
Finished debug [unoptimized + debuginfo] target(s) in 0.25 secs
Running target/debug/deps/rustdoc_test-bd16c1b47357d9ca
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Doc-tests rustdoc-test
running 1 test
test Circle_0 ... FAILED
failures:
---- Circle_0 stdout ----
error[E0560]: struct `rustdoc_test::Circle` has no field named `z`
--> <anon>:3:67
|
3 | let circle = rustdoc_test::Circle{x: 0f64, y: 0f64, r: 10f64, z: 1};
| ^
error: aborting due to previous error(s)
thread 'Circle_0' panicked at 'Box<Any>', ../src/librustc/session/mod.rs:183
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
Circle_0
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
error: test failed
Thanks, Questions?
Rust in Production
(small selection)
rust
By Jupp Müller
rust
- 2,856