RUST

A brief introduction about Rust.

I'm J.D. Nicholls 👋

Digital nomad 🎒 | Mentor 👨‍🏫 | Speaker 🗣️ | Developer 👨‍💻 | OSS Contributor 🍫 | Creator of @proyecto26 🧚

 

Founding Full-Stack Engineer 👷

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

 HashMAPS 

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert("Blue", 10);
scores.insert("Red", 10);
scores.insert(String::from("LOL"), 0);

for (color, value) in scores.iter() {
  // ...
}

# Using get method (Option<&T>)
let name = String::from("Testing");
let value = scores.get(&name).copied().unwrap_or(0);

HashMaps store values by key, the keys can be booleans, integers, strings, or custom types with traits.

Optional value when there’s no an entry for the key

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

Declaring a block with a pair of brackets

References

fn main() {
    let x = 2;
    {
        let a = &x;
        assert_eq!(*x, 2);
    }
    println!("{}", x); // print "2"
}

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"),
        3 ..= 5 => println!("Number between 3 and 5"),
        _ => 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"
}

⬇️

Writing tests in Rust

A test in Rust is a function that’s annotated with the test attribute

#[cfg(test)] mod tests { 
  #[test] 
  fn simple_example() { 
    let result = 3 + 5; 
    assert_eq!(result, 8); 
  }
}

The #[cfg(test)] annotation on the tests module tells Rust to compile and run the test code only when you run cargo test, not when you run cargo build.

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 J.D Nicholls

Rust

A brief introduction about Rust

  • 739