Rust ❤️ Prodrive

About me

  • Daan de Graaf
  • Software Science @ TU/e
  • Designer @ Prodrive
  • Writing Rust since 2015

What this isn't

  • A tutorial on Rust programming
  • A rant on C, C++ and other languages
  • Rust evangelism

What this is

  • Practical experiences here at Prodrive
  • A highlight of the advantages of Rust
  • When to use Rust

About Rust

  • Created by Mozilla
  • A faster, safer browser

 

  • No garbage collection
  • Strong type system
  • C-style syntax

Empowering everyone to build
reliable and efficient software.

Most loved programming language

(Stack Overflow 2016-2019)

Why use Rust?

  • Performance
  • Reliability
  • Productivity

Performance

Comparable to C and C++:

  • No garbage collection
  • Highly optimized compiler
  • Zero cost abstractions

Rust

C

C++

Asm

Java

Go

C#

Memory safe

High-level

High performance

Low-level

Rust + LLVM

Clang

IR

Machine code

Case in point

pub fn square(num: i32) -> i32 {
    num * num
}
int square(int num) {
    return num * num;
}
square:
    imul    edi, edi
    mov     eax, edi
    ret

Zero cost abstractions

#[derive(Clone)]
struct System {
    name: String,
    active: bool,
    // More stuff ...
}

struct DataStore {
    systems: Vec<System>,
    // More stuff ...
}

fn active_systems(data: &DataStore) -> Vec<Json<System>> {
    data.systems.iter()                         // Create a "stream" of items
        .filter(|s| s.active)                   // Only the ones that are active
        .take(5)                                // No more than five items
        .map(|s| Json(s.clone()))               // Turn it into JSON
        .collect()                              // Store the results in a list
    // `return` keyword is implicit
}

Still equivalent

pub fn any_true(list: &[bool]) -> bool {
    list.iter().any(|x| *x == true)
}
bool any_true(bool *list, size_t len) {
    for(size_t i = 0; i < len; i++) {
        if(list[i]) return true;
    }
    return false;
}
any_true:
        test    rsi, rsi
        je      .L7
        movzx   eax, BYTE PTR [rdi]
        test    al, al
        jne     .L3
        lea     rdx, [rdi+1]
        add     rsi, rdi
        jmp     .L5
.L6:
        add     rdx, 1
        movzx   ecx, BYTE PTR [rdx-1]
        test    cl, cl
        jne     .L8
.L5:
        cmp     rdx, rsi
        jne     .L6
        ret
.L8:
        mov     eax, ecx
.L3:
        ret
.L7:
        xor     eax, eax
        ret
any_true:
        test    rsi, rsi
        je      .L7
        movzx   eax, BYTE PTR [rdi]
        test    al, al
        jne     .L3
        lea     rdx, [rdi+1]
        add     rsi, rdi
        jmp     .L5
.L6:
        add     rdx, 1
        movzx   ecx, BYTE PTR [rdx-1]
        test    cl, cl
        jne     .L8
.L5:
        cmp     rdx, rsi
        jne     .L6
        ret
.L8:
        mov     eax, ecx
.L3:
        ret
.L7:
        xor     eax, eax
        ret

Quiz time!

#include <vector>
#include <iostream>

int main() {
    std::vector<int> list;
    list.push_back(1);
    auto item1 = &list[0];

    std::cout << "Item: " << (*item1) << std::endl;
}
#include <vector>
#include <iostream>

int main() {
    std::vector<int> list;
    list.push_back(1);
    auto item1 = &list[0];
    list.push_back(2);
    auto item2 = &list[1];

    std::cout << "Items: " << (*item1) << ", " << (*item2) << std::endl;
}
fn main() {
    let mut list = Vec::new();
    list.push(1);
    let item1 = &list[0];
    list.push(2);
    let item2 = &list[1];
    println!("Items are: {}, {}", item1, item2);
    
}
error[E0502]: cannot borrow `list` as mutable because it is also borrowed as immutable
 --> src/main.rs:5:5
  |
4 |     let item1 = &list[0];
  |                  ---- immutable borrow occurs here
5 |     list.push(2);
  |     ^^^^^^^^^^^^ mutable borrow occurs here
6 |     let item2 = &list[1];
7 |     println!("Items are: {}, {}", item1, item2);
  |                                   ----- immutable borrow later used here
Items are: 0, 2

Impossible in Rust:

  • Null pointers
  • Use after free
  • Double free
  • Buffer overflows

If it compiles, it runs

Explicit errors

#include <stdio.h>

const char DATA[] = { 0x72, 0x75, 0x73, 0x74};

int main() {
    FILE* fd = fopen("hello_world.txt", "w");
    fwrite(DATA, sizeof(char), sizeof(DATA) / sizeof(char), fd);
}
#include <stdio.h>

const char DATA[] = { 0x72, 0x75, 0x73, 0x74};

int main() {
    FILE* fd = fopen("hello_world.txt", "w");
    if(fd == NULL) {
        perror("Could not open file");
        return 1;
    }
    size_t len = fwrite(DATA, sizeof(char), sizeof(DATA) / sizeof(char), fd);
    if(len != sizeof(DATA) / sizeof(char)) {
        perror("Could not write to file");
        return 1;
    }
    if(fclose(fd)) {
        perror("Could not close file");
        return 1;
    }
    return 0;
}
use std::fs::File;
use std::io::{Write, Error};

// We return a Result, which could be one of:
// * Ok    Everything went well.
// * Err   There was an error (which will be printed to stdout).
fn main() -> Result<(), Error> {
    // Create a new file
    let mut file = File::create("hello_world.txt")?;
    file.write(&[0x72, 0x75, 0x73, 0x74])?;
    // Report that everything went okay
    Ok(())
    // File is automatically closed here
}
use std::fs::File;
use std::io::{Write, Error};

fn main() {
    // Create a new file
    let mut file = File::create("hello_world.txt");
    file.write(&[0x72, 0x75, 0x73, 0x74]);
}


error[E0599]: no method named `write` found for type `Result<File, Error>`
 --> src/main.rs:7:10
  |
7 |     file.write(&[0x72, 0x75, 0x73, 0x74]);
  |          

Cargo

Rust's awesome package manager

Build any Rust project:

[daagra@prodrive rust_project]$ cargo build

Need a web server?

[package]
name = "rust_project"
version = "0.1.0"

[dependencies]
actix-web = "0.7"

Show me the docs!

[daagra@prodrive rust_project]$ cargo doc --open

case I: redfish_proxyd

  • Web server for remote cluster management
  • REST API server written in C
    • Concurrency bottlenecks
    • Low developer velocity
    • Memory leaks
  • Proof of concept in Rust

Thomas Sioutas

Concurrency

redfish_proxyd

redfishd

redfishd

redfishd

...

Poll

Options in C:

  • Stay sequential
  • pthreads, tricky synchronisation

Options in Rust:

  • Native safe threads
  • Futures

Back to the future

HTTP GET System

Parse JSON

HTTP GET Chassis

Parse JSON

Combine to Server struct

Encode JSON

Response

JSON serialization

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct EthernetInterface {
    host_name: String,
    #[serde(rename = "FQDN")]
    fqdn: String,
    full_duplex: Option<bool>,
    interface_enabled: Option<bool>,
    ipv4_addresses: Vec<Ipv4Addr>,
}
{
    "HostName": "localhost",
    "FQDN": "localhost.lan",
    "InterfaceEnabled": false,
    "Ipv4Addresses": [
        "192.168.1.1"
    ]
}
EthernetInterface {
    host_name: "localhost",
    fqdn: "localhost.lan",
    full_duplex: None,
    interface_enabled: Some(true),
    ipv4_addresses: [
        Ipv4Addr(192, 168, 1, 1),
    ]
}

Architecture

main()

redfishd poller

files, network, ...

1. Create/start

Router

  • Bind server
  • Route requests

2. Create/start

Injected

Outcome I: Adoption

Rust replaces original implementation

Krzystof++

Case II: IOT_STM32

  • Prodrive Framework for Internet of Things
  • Close to bare metal (FreeRTOS)
  • C - Rust - C sandwich

Rust on embedded

  • Architectures
    • ARM Cortex-M, Cortex-A
    • RISC-V
    • MIPS
  • Board support crates
  • Embedded HAL

Embedded HAL

PWM

I2C

SPI

Timers

Watchdog

Board support crates

Driver crates

Implements

Depends on

C interop

  • Vital use-case for Rust
  • Zero overhead
  • Tooling: Binding generation in both directions

C- Rust - C sandwich

// Implemented in Rust
extern void sum_array(int *arr, size_t len, void (*cb)(int sum));

void callback(int sum) {
    printf("Sum: %d\n", sum);
}

int main() {
    int numbers[] = {1, 2, 3, 4};
    sum_array(numbers, 4, callback);
}
use core::slice;

type Callback = extern "C" fn(sum: i32);

// no_mangle attribute to stick to a C style name in the object file.
#[no_mangle]
pub extern "C" fn sum_array(arr: *const i32, len: usize, cb: Callback) {
    // Convert from an unsafe C array to a safe Rust version
    let array = unsafe { slice::from_raw_parts(arr, len) };
    let sum = array.iter().sum();
    cb(sum);
}

CBOR parsing

  • Complex and risky operation
    • Input supplied by third party
    • Large data volume
    • Scattered on different files
  • Leverage serialization framework
  • Needs bindings to PTFS

CBOR parsing: result

use crate::parse::ParseError;
use crate::ptfs::File;
use cstr_core::CStr;

#[derive(Debug, Deserialize, Default)]
pub struct Operator {
    #[serde(rename = "oID")]
    o_id: u32,
    #[serde(rename = "fID")]
    f_id: u16,
    #[serde(rename = "aLvl")]
    a_lvl: u8,
    #[serde(rename = "exp")]
    exp_datetime: u32,
    #[serde(rename = "pMode")]
    p_mode: u8,
}

pub fn parse_operator(path: &CStr) -> Result<Operator, ParseError> {
    let file = File::open(path)?;
    // Reserve space to store our parsed operator
    let mut operator = Operator::default();
    // Buffer for temporary parse data
    let mut buffer = [0u8; 64];
    file.deserialize_into(&mut buffer, &mut operator)?;
    Ok(operator)
}

Outcome II: Exploration

Further experiments in progress

Patrick

Bottom line

Rust promises great things ...

and delivers!

When not to use Rust

  • Low performance and/or safety requirements
  • Large libraries without good bindings
    • OpenCV
  • Exotic platforms without LLVM support
    • xtensa (ESP32), for now
    • armv5
    • avr

When should I consider Rust?

  • You need C-like performance
  • Safety and stability are important
  • You need concurrency
  • Precondition: libraries for your domain are available
    • Or you can write them
      yourself

Thank you!

Questions?

References

Rust @ Prodrive

By wildarch

Rust @ Prodrive

  • 384