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:

  1. one or more references (&T) to a resource
  2. 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

  • 1,411
Loading comments...

More from Jupp Müller