RUST

A brief introduction about Rust.

I'm J.D. Nicholls 👋

- Open Source Contributor 👨‍💻

- Game developer (Hobby) 🎮

- Developer who loves UX 💫

- Chocolate lover 🍫

- Founding Full-Stack Engineer 👷

AT

BAXUS

BUT WHY RUST?

- Build reliable and efficient systems software:

   • networking (web servers, mails servers, web browsers).

   • compilers and interpreters.

   • build games.

   • command-line programs.
   • web-assembly programs.

   • applications for embedded devices.

 

Rust strikes a unique balance among performance, safety, and implementation expressions.

⬇️

advantages

- Type safe: the compiler ensure the type of the variables.

- Memory safe: Rust pointers always refer to valid memory.

- Data race free: a value can't be mutated from multiple parts at the same time.

- Zero-cost abstractions: Rust allows the use of high-level concepts, like iteration, interfaces, and functional programming.

- Minimal runtime: no garbage collector to manage memory efficiently.

- Targets bare metal: write an operating system kernel or device drivers

RUST THINKING MODEL

Rust - Memory Allocation; let x = String::from("Hi");

Memory Allocation

Rust - Memory Allocation; let y = x;

⬇️

The new variable has the ownership of the value; x can't be used again.

This transfers the reference string to a string variable; x is the new owner.

Immutability

⬇️

fn main() {
  let x = String::from("hi");
  let y = &x;  // <-- Immutable borrow
  
  println!("{}", x);
  println!("{}", y);
}

The new variable has the right to read

The Lifetime

- RAII (Resource Acquisition Is Initialization) - An analysis mechanism that can accurately calculate resource and variable life.

 

- C-Own, Java-GC, Rust-Ownership & Borrowing.

- Advantages of Rust: Fast as C & Safe as Java.

⬇️

The Compiler

- The compiler does the job; 

Source

Code

HIR

>

>

MIR

LLVM

IR

>

Machine

Code

>

Borrow checking,

Optimizations,

Code generation

Printing a string with params

CONSOLE

fn main() {
  let mut userId = 1;
  println!("The user id is {}", userdId);
  
  userId = 2;
  println!("The new user id is {}", userdId);
}

Printing a hex value

fn main() {
  let color: i32 = 0xff0000;
  println!("The color red is #{:06x}", color);
}

Floats

DATA Types

fn main() {
  let price = 5.0; // Float 64 bits
  
  let version: f32 = 1.0 // Float 32 bits
}
fn main() {
  let enabled = true;
  
  let loading: bool = false;
}

Booleans

fn main() {
  let environment = 'development';
  let cool_emoji = '🤘'; // One character in Rust like 'a', 'A', etc
}

Strings

⬇️

Arrays

fn main() {
  let numbers = [1, 2, 3, 4, 5];
  
  let fixed_size: [i32; 3] = [-1, 2, 3]; // same type
  
  let first = fixed_size[0]; // signed integer (negative numbers)
}
fn main() {
  let names = ("Juan", "David", "Nicholls Cardona");
  println!("First name is {}", names.0);

  let size = (1024, 768);
  let (width, height) = size; // destructured when doing an assignment 
  println!("width: {}, height: {}", width, height);
  
  // define the types
  let pair: (char, i32) = ('a', 17);
}

Tuples

⬇️

Tuples are fixed-length collections of values of different types

Strings

fn main() {
  // reference string, you can't change it once you declare it
  let name = "Juan";
  let api_url: &'static str = "https://myapi.com"; // equivalent
}

⬇️

Strings in Rust vs C

Inmutable

fn main() {
  let mut message = String::new();
  message += "Testing";
  
  let mut strng = String::from("ABC");
  strng.push('D'); // one character
  strng.push_str("EFG");
  
  // assertion failed, expected 'G'
  assert_eq!(strng.pop(), Some('F')); 
}

Mutable

mut makes a variable binding mutable

 StRUCTs 

#![allow(dead_code, unused)]
pub struct User {
  username: String,
  email: String,
  active: bool,
}

fn main() {
  // Initialized using struct literals
  let mut user = User {
    username: String::from("jdnichollsc"),
    email: String::from("jdnichollsc@hotmail.com"),
    active: true,
  };

  user.email = String::from("jdnichollsc@gmail.com");
  println!("user email: {}", user.email);
}

⬇️

Define a structure

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let blackColor = Color(0, 0, 0);
let origin = Point(0, 0, 0);

Define a tuple structure

 ENUMs 

#![allow(dead_code, unused)]

// enum with implicit discriminator starts at 0
enum Status {
    Active,
    Pending,
    Inactive
}

struct User {
    username: String,
    status: Status,
}

fn main() {
    let user = User {
        username: String::from("jdnichollsc"),
        status: Status::Active,
    };
    if matches!(user.status, Status::Active) {
        println!("The user is active");
    } else {
        println!("The user is not allowed");
    }
}

⬇️

Define an enumeration

#![allow(dead_code, unused)]

// enum with explicit discriminator
enum Color {
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff,
}

struct User {
    username: String,
    theme: Color,
}

fn main() {
    let user = User {
        username: String::from("jdnichollsc"),
        theme: Color::Red,
    };
    // Casting enum to integer (formatting)
    println!("The user theme color is #{:06x}", user.theme as i32);
}

Define an enumeration

⬇️

struct QuitMessage;
struct MoveMessage {
    x: i32,
    y: i32,
};
struct WriteMessage(String);
struct ChangeColorMessage(i32, i32, i32);

enum Message {
    Quit(QuitMessage),
    Move(MoveMessage),
    Write(WriteMessage),
    ChangeColor(ChangeColorMessage),
}
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

Combine enum with structs

Destruct an enum with structs

If Else condition

CONDITIONALS

fn main() {
  let age = 18
  
  if age > 17 {
    println!("Allowed");
  } else {
    println!("Not allowed");
  }
}

⬇️

While condition

fn main() {
  let mut counter = 0;
  
  while counter < 10 {
    println!("Count: {}", counter);
    counter += 1;
  }
}

For condition

fn main() {
  let colors = ["#A76B00", "#FFAF1E", "#126136"];
  
  for color in colors.iter() {
    println!("Color {}", color);
  }
}

Loop condition

fn main() {
  let mut counter: i32 = 0;
  let arr = [1, 2, 3, 4];
  
  let index: i32 = loop {
    let count = counter as usize;
    if arr.len() == count {
      break -1;
    }
    if arr[count] == 26 {
      break counter;
    }
    counter += 1;
  };
  println!("The index of 26 number is {}", index);
}

Declaring a void function

FUNCTIONS

fn track_event(name: &str) {
  // do something
}

fn main() {
  track_event("main");
}

⬇️

Returns a number (32-bit signed integer)

pub fn bigger(a: i32, b: i32) -> i32 {
    if a > b {
        return a;
    }
    b // omitting the semicolon at the end of a fn is the same as returning
}

fn main() {
  let big_num = bigger(10, 20);
  println!("Bigger num bwt 10 & 20 is {}", big_num);
}

String FUNCTIONs

Slice

fn main() {
  let hello_world = String::from("Hello world");
  let hello = &hello_world[0..5];
  let world = &hello_world[6..11];
  println!("{} {}", hello, world); // hello world
}

⬇️

StruCT FUNCTIONs

Declare methods on your own types

struct Number {
    value: i32,
}

impl Number {
    // using "&" to avoid moving the ownership of the value
    fn is_odd(&self) -> bool {
        self.value % 2 == 0
    }
}

fn main() {
  let num = Number { value: 10 };
  if num.is_odd() {
    println!("{} is odd", num.value);
  } else {
    println!("{} is even", num.value);
  }
}

Declaring a block with a pair of brackets

Blocks

fn main() {
    let x = "out";
    {
        // has its own scope
        let x = "in"; // this is a different `x`
        println!("{}", x); // print "in"
    }
    println!("{}", x); // print "out"
}

Blocks are also expressions

fn main() {
    let project = 26;
    println!("{}", x); // print 26
}
fn main() {
    let project = { 26 };
    println!("{}", x); // print 26
}

They are equivalent

CLOSURES

Anonymous functions that can capture values from the scope in which they're defined

fn main() {
    let sum = |x:i32| x + 1;
    println!("{}", sum(10)); // print "11"
}
fn twice<F: Fn(i32) -> i32>(x: i32, f: F) -> i32 {
    f(x) + f(x)
}

fn main() {
    let square = |x: i32| { x * x };
    println!("{}", twice(5, square)); // prints 50
}

PATTERN MATCHING

#![allow(dead_code, unused)]

struct Number {
    odd: bool,
    value: i32,
}

fn main() {
    let one = Number { odd: true, value: 1 };
    let two = Number { odd: false, value: 2 };
    print_number(one); // Odd number: 1
    print_number(two); // Even number: 2
}

fn print_number(n: Number) {
    match n.odd {
        true => println!("Odd number: {}", n.value),
        false => println!("Even number: {}", n.value),
    }
}

⬇️

A match has to be exhaustive: at least one arm needs to match.

fn main() {
    let one = Number { odd: true, value: 1 };
    let two = Number { odd: false, value: 2 };
    let three = Number { odd: false, value: 3 };
    print_number(one);
    print_number(two);
    print_number(three);
}

fn print_number(n: Number) {
    match n {
        Number { value: 1, .. } => println!("One"),
        Number { value: 2, .. } => println!("Two"),
        Number { value, .. } => println!("{}", value),
        // if that last arm didn't exist, we would get a compile-time error
    }
}
fn print_number(n: Number) {
    match n.value {
        1 => println!("One"),
        2 => println!("Two"),
        _ => println!("{}", n.value), // "_" can be used as a "catch-all" pattern
    }
}

TRAITS

trait Signed {
    fn is_strictly_negative(self) -> bool;
}

⬇️

Abstract definition of shared behavior with types

Orphan rules:

- one of your traits on anyone's type.

- anyone's trait on one of your types.

- but not a foreign trait on a foreign type

impl Signed for Number {
    fn is_strictly_negative(self) -> bool {
        self.value < 0
    }
}

fn main() {
    let n = Number { odd: false, value: -44 };
    println!("{}", n.is_strictly_negative()); // prints "true"
}

NOTE:

You can't use "n" after printing the value (Ownership)

USING TRAITS

Your trait on a foreign type

impl Signed for i32 {
    fn is_strictly_negative(self) -> bool {
        self < 0
    }
}

fn main() {
    let n: i32 = -44; // primitive type
    println!("{}", n.is_strictly_negative()); // prints "true"
}

⬇️

Some traits are markers - they don't say that a type implements some methods, they say that certain things can be done with a type.

i32 implements trait Copy

USING TRAITS

Foreign trait on your type

#![allow(dead_code, unused)]
struct Number {
    value: i32,
    is_positive: bool,
}

// An "impl" block is always for a type
impl std::ops::Neg for Number {
    type Output = Self; // Self means the current type (Number)

    fn neg(self) -> Self {
        let new_value = -self.value;
        let is_positive = new_value > 0;
        Self {
            value: new_value,
            is_positive: is_positive,
        }        
    }
}

fn main() {
    let n = Number { is_positive: true, value: 1 };
    let m = -n; // this is only possible because we implemented `Neg`
    println!("{}", m.is_positive); // prints "false"
}

⬇️

MISC

// avoid warnings about unused values
let _x = 26;

// call a function but throws away its result
let _ = get_something();

// Re-binding variables
let x = 5;
let x = x + 5; // now x is equal to 10

// Destructuring a Tuple ignoring the first field
let (_, right) = slice.split_at(middle);

// Semi-colon marks the end of a statement
let x = vec![1, 2, 3, 4, 5, 6, 7, 8]
    .iter()
    .map(|x| x + 3)
    .fold(0, |x, y| x + y);
    
// Multiple statements inside a block
let x = {
    let y = 1; // first statement
    let z = 2; // second statement
    y + z // return the result of the operation
};

// Dots are used to access fields of a value
let user = get_some_struct();
user.username; // this is "jdnichollsc"

⬇️

// Call a method of a value
let username = "jdnichollsc";
username.len(); // this is 11

// Use double-colon "::" to call external functions
let least = std::cmp::min(3, 8);
// std is a library (crate)
// cmp is a module (source file)
// min is a function

// Import names from other namespaces
use std::cmp::min;
use std::cmp::max;

let min_value = min(7, 1);
let max_value = max(7, 1);

// Use "globs" to import multiple names
use std::cmp::{min, max};

// Use a "wilcard" to import every symbol from a namespace
use std::cmp::*;

// Call methods as regular functions using types (namespaces)
let x = str::len("baxus"); // this is 5

// Rust inserts some non-primitive types at the beginning of every module
let v = Vec::new(); // The full path is std::vec::Vec

⬇️

// Declaring a struct using double precision values
struct Vec2 {
    x: f64,
    y: f64
}

// Initializing a struct from another struct
let v3 = Vec2 {
    x: 14.0,
    ..v2 // "struct update" can only happen in last position (without comma)
};

// Destructuring a struct
let v = Vec2 { x: 3.0, y: 6.0 };
let Vec2 { x, .. } = v; // ignore certain values
println!("X is {}", x); // X is 3

⬇️

Embedded

- Hardware with limited resources (Energy, Memory, Space, etc)

- Limited connection between devices.

 

Regulators

- Stable APIs (Qualified toolchain for Rust)

 

Architectures

- ARM Cortex-A/M
- RISC-V

- XTensa

 

It includes header files, build systems (Yocto, CMake), Chip/Manufacturer specific IDEs

Ecosystem

- SVD2Rust

- PALs

- HALs

- Board Crates

- Peripheral Crates

- RTOS; RTIC, Tock, Hubris

- Async; Embassy

COMMON ERRORS

Memory Allocation

Memory allocation issue with Rust; borrow of moved value
Memory allocation issue with Rust; does not long enough
fn main() {
  let x = String::from("hi");
  let y = x;  // <-- value moved here
  
  println!("{}", x); // Error
}
fn main() {
  
  let y = {
    let x = String::from("hi");
    &x
    // ^^ does not live long enough
  };
  
  println!("{}", y);
}

CREDITS & RESOURCES

Rust

By Juan David Nicholls

Rust

A brief introduction about Rust

  • 499