Oxidation

A case for rust in critical systems development

Created by Derek J. Goddeau and contributors

What properties do we need in a programming language for Robotics?


Essentially the same as a systems programming language.

  • Memory Safety
  • Minimal Runtime
  • Strong Type System
  • Performance
Memory Safety Minimal Runtime Strong Type System Performance
C
C++
C#
D
Go
Nim
Rust
Java
Python
Haskell
  • Memory Safety
  • Minimal Runtime
  • Strong Type System
  • Performance
  • + Build System
  • + High-Level Abstractions
  • + Data Race Free (!?)

Why should we care?

The Null Reference

I call it my billion-dollar mistake…

At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

- Tony Hoare, inventor of ALGOL W.
Anything is Possible
  • Rust's Guarantees
  • Tooling
  • Adoption and Resources

Meet Safe and Unsafe

Safe and Unsafe

No matter what, Safe Rust can't cause Undefined Behavior.

Sometimes we need to use unsafe.


                            fn index(idx: usize, arr: &[u8]) -> Option {
                                if idx < arr.len() {
                                    unsafe {
                                        Some(*arr.get_unchecked(idx))
                                    }
                                } else {
                                    None
                                }
                            }
                        

Defining Memory Safety

*foo is always ok

Dereferences never fail, and point to values of correct type.

Resulting Properties

  • No null pointer dereferences
  • No dangling pointers.
    • No iterator invalidation.
    • No use-after-free.
    • No double frees.
  • No out-of-bounds accesses.
    • No buffer overflows.
  • No data races.

Memory Safety in Rust

forced initialization + restricted aliasing + ownership = memory safety

(at compile time)

Linear types: Single Owner


                            let x = vec![1, 2, 3];
                            let y = x;
                            let z = x;  // Value used after move
                        


                            f(x);
                            f(x);  // Value used after move
                        


                            fn m(self) {...};
                            x.m();
                            x.m();  // Value used after move
                        

Borrowing: Restricted Aliasing


                            let mut x = vec![1, 2, 3];
                            let y = &x;
                            let z = x;  // Cannot move out of `x` because it is borrowed
                        


                            let y = &x;
                            let z = &x;
                        


                            let y = &mut x;
                            let z = &x;  // Cannot borrow `x` as immutable because it is also borrowed as mutable
                        


                            fn m(&mut self) {...}
                        

Retrun an owned Vec


                        fn scope() -> Vec {
                            vec![1, 2, 4]
                        }
                        

Restricted aliasing


                                fn scope2() {
                                    let v1 = vec![1, 2, 3];
                                    let v2 = v1;            // Ownership has transfered, can't reference `v1`
                                    v1.push(4);             // Use of moved value: `v1`
                                    v2
                                }
                            

Clone to get a "deep copy"


                                fn scope3() {
                                    let v1 = vec![1, 2, 3];
                                    let v2 = v1.clone();
                                    v1.push(4);
                                    v2
                                }
                            

Borrow to avoid the performance hit


                            fn scope4() {
                                fn foo(v1: &Vec) {
                                    for v in v1 {
                                        println!("{}", v);
                                    }
                                }
                                let v1 = vec![1, 2, 3];
                                foo(&v1);               // v1 is still valid
                                v1                      // Ownership has been returned
                            }
                        

Fearless Concurrency

Ownership is transfered to the new threads with the move keyword


                                fn thread() {
                                    let v = vec![1, 2, 3];

                                    let handle = thread::spawn(move || {
                                        println!("Here's a vector: {:?}", v)
                                    });

                                    handle.join().ok();
                                }
                            

Restricted Aliasing also Allows Optimizations


                            fn compute(input: &u32, output: &mut u32) {
                                if *input > 10 {
                                    *output = 1;
                                }
                                if *input > 5 {
                                    *output *= 2;
                                }
                            }
	                    

                        fn compute(input: &u32, output: &mut u32) {
                            let cached_input = *input;      // keep *input in a register
                            if cached_input > 10 {
                                *output = 2;                // x > 10 implies x > 5, so double and exit immediately
                            } else if cached_input > 5 {
                                *output *= 2;
                            }
                        }
	                    

                                                //  input ==  output == 0xabad1dea
                                                // *input == *output == 20
                            if *input > 10 {    // true  (*input == 20)
                                *output = 1;    // also overwrites *input, because they are the same
                            }
                            if *input > 5 {     // false (*input == 1)
                                *output *= 2;
                            }
                                                // *input == *output == 1
	                    

In Rust we know this input is impossible because &mut isn't allowed to be aliased.

Tooling

Build System

cargo build


It was a revelation that I could actually write an operating system and do cargo build and it would build my operating system.

-- Sergio Benitez

Testing

Cargo does unit testing and integration testing.

Integration tests go in a tests directory and are written just as if the code was using the library.

cargo test     &     cargo doc


                            /// Calculate the n-th Fibonacci number using Binet's formula.
                            /// 
                            /// # Example
                            ///
                            /// ```
                            /// let fib = nth_fib(20);
                            /// ```
                            pub fn nth_fib(n: i32) -> i64 {
                                let nth = (PHI.powi(n) - PSI.powi(n)) / 5f64.sqrt();
                                (nth.round() as i64)
                            }
                        

                            #[cfg(test)]
                            mod test {
                                use super::nth_fib;
                                #[test]
                                fn zero_to_five() {
                                    assert_eq!(nth_fib(0), 0);
                                    assert_eq!(nth_fib(1), 1);
                                    assert_eq!(nth_fib(2), 1);
                                    assert_eq!(nth_fib(3), 2);
                                    assert_eq!(nth_fib(4), 3);
                                    assert_eq!(nth_fib(5), 5);
                                }
                            }
	                    

Benchmarking

cargo bench


                                #![feature(test)]

                                extern crate test;

                                pub fn add_two(a: i32) -> i32 {
                                    a + 2
                                }

                                #[cfg(test)]
                                mod tests {
                                    use super::*;
                                    use test::Bencher;

                                    #[test]
                                    fn it_works() {
                                        assert_eq!(4, add_two(2));
                                    }

                                    #[bench]
                                    fn bench_add_two(b: &mut Bencher) {
                                        b.iter(|| add_two(2));
                                    }
                                }
                            

Examples

cargo run --example my_example_name

Rust Format & Clippy

The one true style.

Rustc

Compiler errors that are actually useful!


                            fn main() {
                                let s1 = String::from("Hello");
                                let s2 = s1;

                                println!("{} World!", s1)
                            }
                        

error[E0382]: use of moved value: `s1`
 --> src/main.rs:5:27
  |
3 |     let s2 = s1;
  |         -- value moved here
4 |
5 |     println!("{} World!", s1);
  |                           ^^ value used here after move
  |
  = note: move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
                        

For types that are nearly costless to deep copy like i64 implementing the Copy trait will allow this to work.


                            fn main() {
                                let s = String::from("hello");

                                change(&s);
                            }

                            fn change(some_string: &String) {
                                some_string.push_str(", world");
                            }
                        

error[E0596]: cannot borrow immutable borrowed content `*some_string` as mutable
 --> src/main.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- use `&mut String` here to make mutable
8 |     some_string.push_str(", world");
  |     error[E0596]^^^^^^^^^^^ cannot borrow as mutable
                        

Adoption

Rust Friends
Most loved language last 3 years on stack overflow.

Resources & References