Rust

Introduction to

What is Rust?

A modern, multi-paradigm programming language.

  • Memory Safety: Ensures safety without a garbage collector, minimizing memory-related errors.
     
  • Concurrency Made Safe: Its ownership model prevents data races in concurrent code.
     
  • Performance: Offers C/C++ level performance; ideal for system-level and performance-critical applications.
  • Modern Tooling: Features like Cargo (package manager) and detailed compiler error messages enhance developer productivity.
     
  • Robust Ecosystem: Growing library support for diverse development needs, from web to embedded systems.
     
  • Cross-Platform & Integrable: Compatible with various platforms and can be integrated with other languages.
     
  • Industry Adoption: Used by major companies, indicating reliability and effectiveness in real-world applications.

Hello, Rust!

# follow prompt to install rust toolchain using rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# check installation
rustc --version
# should print soemthing like "rustc 1.74.0 (79e9716c9 2023-11-13)""
fn main() {
    println!("Hello, Rust!");
}

Create a file `hello.rs`

Installation

Compile and run

rustc hello.rs
./hello
# Hello, Rust!

Hello, Cargo!

cargo new hello

Creating a new package

cargo run # compile and run
cargo build # build
cargo test # run test
cargo add rand # add a crate

Variables

fn main() {
	let first_name = "Salama"; // &str
    let last_name: = "Ashoush"; // &str
    
    println!("Hello, {} {}!", first_name, last_name);
}

Inference

Variables

fn main() {
 	let first_name: &str = "Salama";
    let last_name: &str = "Ashoush";

    println!("Hello, {} {}!", first_name, last_name);
}

Type annotation

Variables

fn main() {
    let x = 5;

    let x = x + 1;
    
    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

    println!("The value of x is: {x}");

}

Shadowing

Variables

fn main() {
 	let mut first_name: &str = "Salama";
    let last_name: &str = "Ashoush";

    println!("Hello, {} {}!", first_name, last_name);
    
    first_name = "Ramadan";
    println!("Hello once more, {}!", first_name);

}

Mutable variables

Variables

fn main() {
	// constants should shout and init with type annotation
    const ARE_YOU_COOL: bool = true;
    println!("Are you cool?, {}!", ARE_YOU_COOL);

}

Constants

Primitive types

numbers

Primitive types

floats

fn main() {
 	let x = 2.0; // f64
    let y: f32 = 3.0; // f32

}

bool

fn main() {
    let t = true;
    let f: bool = false; // with explicit type annotation
}

Primitive types

Numeric operators

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // Results in -1

    // remainder
    let remainder = 43 % 5;
}

Primitive types

char

fn main() {
   let c = 'z';
   let z: char = 'ℤ'; // with explicit type annotation
   let heart_eyed_cat = '😻';
}

&str

fn main() {
  	let first_name = "Salama"; // &str (string slice)
    
    // you can do lots of stuff with it, eg
    first_name.len(); // 6
    first_name.is_empty(); // false
    first_name.contains("S"); // true
    first_name.replace("S", "A"); // "Alama"
    first_name.to_uppercase(); // "SALAMA"
    first_name.to_lowercase(); // "salama"
    first_name.chars(); // ['S', 'a', 'l', 'a', 'm', 'a'] (iterator)
    first_name.split(" "); // ["Salama"]
  
  
}

Primitive types

Tuple

fn main() {
   let tup = (500, 6.4, 1);
   
   // we can get the values by destructing
   let (x, y, z) = tup;
   println!("The value of y is: {y}");
	
   // or by index
   let five_hundred = tup.0;
   let six_point_four = tup.1;
   let one = tup.2;
   
   // Tuples can include mixed types
   let person = ("Salama", 30, true);
}

Primitive types

Arrays

fn main() {
	// array cant include mixed types
    let a = [1, 2, 3, 4, 5];
    let first = a[0];
    let second = a[1];
    
   	// using destructing
    // you must match all the array
    let [first, second, _3, _4, _5] = a;
    
    // or use the .. to ignore the rest
    let [first,second, ..] = a;
          
    // using type annotation
    let a: [i32; 5] = [1, 2, 3, 4, 5];
   
    // init 3 element array with the same value
    let a = [3; 5];
    a.len(); // 3
    // and many more


}

Functions

fn say_hello() {
    println!("Hello, world!");
}
fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn early_returns() {
    if condition {
        return something;
    }
    println!("otherwise!");
}
fn main() {
    let a = 1;
    let b = 2;
    let c = add(a, b);
    println!("{} + {} = {}", a, b, c);

    say_hello();
    early_returns();
}

if

fn main() {
    let number = 3;
	// if expression
    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
    
    // this will error -> if coditions must evaluate to bool
    if number {
        println!("number was three");
    }
    
    // if else if 
    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
    // after all if is an expression
    let number = if condition { 5 } else { 6 };
    // this would error? 
    let number = if condition { 5 } else { "six" };
}

looping

fn main() {
    // infinite loop
    loop {
        println!("again!");
    }
    // loop with break
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2; // break the loop and return the value
        }
    };
    println!("The result is {}", result);
    // for loop
    let a = [10, 20, 30, 40, 50];
    for element in a {
        println!("the value is {}", element);
    }
    // for loop with range
    for number in 1..4 {
        println!("{}!", number);
    }
    // while loop
    let mut number = 3;
    while number != 0 {
        println!("{}!", number);
        number -= 1;
    }
}

looping

fn main() {
    // infinite loop
    loop {
        println!("again!");
    }
    // loop with break
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2; // break the loop and return the value
        }
    };
    println!("The result is {}", result);
    // for loop
    let a = [10, 20, 30, 40, 50];
    for element in a {
        println!("the value is {}", element);
    }
    // for loop with range
    for number in 1..4 {
        println!("{}!", number);
    }
    // while loop
    let mut number = 3;
    while number != 0 {
        println!("{}!", number);
        number -= 1;
    }
}

Vectors

fn main() {
    // vectors can only store values of the same type and are allocated on the heap
    let mut v: Vec<i32> = Vec::new();
    v.push(1);
    // vector macro
    let mut v2 = vec![1, 2, 3];
    // length and capacity
    v2.len(); // 3
    v2.capacity(); // 3 (default)
    v2.push(4);
    v2.len(); // 4
    v2.capacity(); // 6 (doubled)
    // reading elements
    let third: i32 = v[2];
    println!("The third element is {}", third);
    // reading elements with get
    let third = v.get(2); // Some(&3)
    // reading elements out of bounds
    let does_not_exist = &v[100]; // panic
    let does_not_exist = v.get(100); // None
    // iterating over values
    for i in v {
        println!("{}", i);
    }
    // iterating over mutable references
    for i in &mut v {
        *i += 50;
    }
}

Rustlings time

Memory allocation

  • Automatic Management: Memory is managed automatically; variables are popped off when out of scope.
  • Fast Access: Quick memory access, suitable for fixed-size data.
  • Fixed Size at Compile Time: Requires knowing the size of data at compile time.
  • Limited Capacity: Has size limits; excessive allocation can lead to stack overflow.

Stack

Memory allocation

  • Manual Management: Memory must be explicitly allocated and deallocated, typically via smart pointers like Box.
  • Slower Access: Involves pointer dereferencing, slower than stack access.
  • Dynamic Size: Used for data that grows at runtime or has an unknown size at compile time.
  • Ownership System: Rust uses ownership, borrowing, and lifetimes to manage heap memory safely, preventing leaks and dangling pointers.

Heap

Ownership

fn main() {
    // ownership rules
    // 1. Each value in Rust has a variable that’s called its owner.
    let mut s = String::from("hello");
    s.push_str(", world!"); // push_str() appends a literal to a String

    // 2. There can only be one owner at a time.
    // moving ownership to another variable
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{}, world!", s1); // error

    // cloning the heap data (deep copy)
    let s1 = String::from("hello");
    let s2 = s1.clone();

    // copy types
    let x = 5;
    let y = x;
    println!("x = {}, y = {}", x, y);

    // 3. When a variable goes out of scope its value will be dropped
    let s = String::from("hello");
    takes_ownership(s);
    println!("{}", s); // error
}

fn takes_ownership(some_string: String) {
    // some_string comes into scope here
    println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing memory is freed.

Borrowing

fn main() {
    // 1. You can have as many immutable references as you want at any given time.
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);

    // 2. You can have exactly one mutable reference.
    let r3 = &mut s; // error
    println!("{}", r3);
    let r4 = &mut s; // error

    // 3. You can't have a mutable reference while you have an immutable one.
    let r5 = &s; // error you can't borrow `s` as mutable because it is also borrowed as immutable
    println!("{}", r5);
    r4.push_str(" world");

    // Rust is smart enough to know that this is not a problem
    let mut s2 = String::from("hello");
    let r1 = &s2;
    println!("{} and {}", r1, r2);
    // r1 is no longer used after this point
    let r2 = &mut s2;
    println!("{}", r2);
    r2.push_str(" world");
    // r2 is no longer used after this point
    let r3 = &s2;
    println!("{}", r3);
}

Structs

struct User {
    username: String,
    email: String,
}
// tuple struct
struct Point(i32, i32, i32);
// unit struct
struct Unit

fn main() {
    // create instance
    let mut user = User {
        username: String::from("user"),
        email: String::from("ss@ss.com"),
    };
    println!("username: {}", user.username);
    println!("email: {}", user.get_username());
    // update
    user.username = String::from("user_new");
    println!("username: {}", user.username);
    // update from other instance
    let user2 = User {
        username: String::from("user2"),
        ..user
    };
    
    let p = Point(1, 2, 3);
    println!("p.0: {}", p.0);
    
    let unit_struct = UnitStruct;
  }

Impl 

// implementation block
impl User {
    // constructor function (associated function) (no self)
    fn new(username: String, email: String) -> Self {
        Self {
            username,
            email,
            sign_in_count: 1,
            active: true,
        }
    }

    // method (self is a reference to the instance of the struct)
    fn get_username(&self) -> &String {
        &self.username
    }
    // method (self is a mutable reference to the instance of the struct)
    fn set_username(&mut self, username: String) {
        self.username = username;
    }
}

fn main() {
    // calling associated function with ::
    let mut user3 = User::new(String::from("user3"), String::from("ss@ss.com"));
    // calling method with .
    println!("username: {}", user3.get_username());
    // calling method with . with mutable reference and passing String
    user3.set_username(String::from("user3_new"));
    println!("username: {}", user3.get_username());
}

Impl 

// implementation block
impl User {
    // constructor function (associated function) (no self)
    fn new(username: String, email: String) -> Self {
        Self {
            username,
            email,
            sign_in_count: 1,
            active: true,
        }
    }

    // method (self is a reference to the instance of the struct)
    fn get_username(&self) -> &String {
        &self.username
    }
    // method (self is a mutable reference to the instance of the struct)
    fn set_username(&mut self, username: String) {
        self.username = username;
    }
}

fn main() {
    // calling associated function with ::
    let mut user3 = User::new(String::from("user3"), String::from("ss@ss.com"));
    // calling method with .
    println!("username: {}", user3.get_username());
    // calling method with . with mutable reference and passing String
    user3.set_username(String::from("user3_new"));
    println!("username: {}", user3.get_username());
}

Enums

// enums are types which have a few definite values
enum Movement {
    Up,
    Down,
}
// with values
enum Movement2 {
    Up(u8),
    Down(u8),
}
// impl block
impl Movement2 {
    fn match_movement(&self) {
        match self {
            Movement2::Up(1) => println!("Up 1"),
            Movement2::Down(1) => println!("Down 1"),
            _ => println!("No match"),
        }
    }
}
fn main() {
    // without values
    let move_up = Movement::Up;
    match move_up {
        Movement::Up => println!("Moving up"),
        Movement::Down => println!("Moving down"),
    }
    // with values
    let move_down = Movement2::Down(1);
    move_down.match_movement();
}

Traits

struct Dog { name: String, age: i8 }
struct Cat { lives: i8 } // No name needed, cats won't respond anyway.

trait Pet {
    fn talk(&self) -> String;

    fn greet(&self) {
        println!("Oh you're a cutie! What's your name? {}", self.talk());
    }
}

impl Pet for Dog {
    fn talk(&self) -> String { format!("Woof, my name is {}!", self.name) }
}

impl Pet for Cat {
    fn talk(&self) -> String { String::from("Miau!") }
}

fn main() {
    let captain_floof = Cat { lives: 9 };
    let fido = Dog { name: String::from("Fido"), age: 5 };

    captain_floof.greet();
    fido.greet();
}

Deriving

#[derive(Debug, Clone, Default)]
struct Player {
    name: String,
    strength: u8,
    hit_points: u8,
}

fn main() {
    let p1 = Player::default(); // Default trait adds `default` constructor.
    let mut p2 = p1.clone();    // Clone trait adds `clone` method.
    p2.name = String::from("EldurScrollz");
    // Debug trait adds support for printing with `{:?}`.
    println!("{:?} vs. {:?}", p1, p2);
}

Option

fn main() {
    let name = "Löwe 老虎 Léopard Gepardi";
    let mut position: Option<usize> = name.find('é');
    
    println!("find returned {position:?}");
    assert_eq!(position.unwrap(), 14); // will get the wrapped value or panic
    
    position = name.find('Z');
    println!("find returned {:?}", position);
    assert_eq!(position.expect("Character not found"), 0); // expect is the same but with custom error
    
    // you can do pattern matching on it
    let l = match name.find('l') {
        Some(position) => position,
        None => 0,
    };
}

Result

use std::fs::File;
use std::io::Read;

fn main() {
    let file: Result<File, std::io::Error> = File::open("diary.txt");
    match file {
        Ok(mut file) => {
            let mut contents = String::new();
            if let Ok(bytes) = file.read_to_string(&mut contents) {
                println!("Dear diary: {contents} ({bytes} bytes)");
            } else {
                println!("Could not read file content");
            }
        },
        Err(err) => {
            println!("The diary could not be opened: {err}");
        }
    }
}

Hashmaps

use std::collections::HashMap;

fn main() {
    let mut page_counts = HashMap::new();
    page_counts.insert("Adventures of Huckleberry Finn".to_string(), 207);
    page_counts.insert("Grimms' Fairy Tales".to_string(), 751);
    page_counts.insert("Pride and Prejudice".to_string(), 303);

    if !page_counts.contains_key("Les Misérables") {
        println!("We know about {} books, but not Les Misérables.",
                 page_counts.len());
    }
    for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] {
        match page_counts.get(book) {
            Some(count) => println!("{book}: {count} pages"),
            None => println!("{book} is unknown.")
        }
    }
    // Use the .entry() method to insert a value if nothing is found.
    for book in ["Pride and Prejudice", "Alice's Adventure in Wonderland"] {
        let page_count: &mut i32 = page_counts.entry(book.to_string()).or_insert(0);
        *page_count += 1;
    }
    println!("{page_counts:#?}");
    // there are no hashmap! like vec! but you can create one from any iterator
    let page_counts = HashMap::from([
    	("Harry Potter and the Sorcerer's Stone".to_string(), 336),
    	("The Hunger Games".to_string(), 374),
  	]);
}

Generics

/// Pick `even` or `odd` depending on the value of `n`.
fn pick<T>(n: i32, even: T, odd: T) -> T {
    if n % 2 == 0 {
        even
    } else {
        odd
    }
}

fn main() {
    println!("picked a number: {:?}", pick(97, 222, 333));
    println!("picked a tuple: {:?}", pick(28, ("dog", 1), ("cat", 2)));
}

Generics

#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn coords(&self) -> (&T, &T) {
        (&self.x, &self.y)
    }

    // fn set_x(&mut self, x: T)
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
    println!("{integer:?} and {float:?}");
    println!("coords: {:?}", integer.coords());
}

Generics

fn duplicate<T: Clone>(a: T) -> (T, T) {
    (a.clone(), a.clone())
}

// with where
fn duplicate<T>(a: T) -> (T, T)
where
    T: Clone,
{
    (a.clone(), a.clone())
}

fn main() {
    let foo = String::from("foo");
    let pair = duplicate(foo);
    println!("{pair:?}");
}

Trait bounds

Generics

// Syntactic sugar for:
//   fn add_42_millions<T: Into<i32>>(x: T) -> i32 {
fn add_42_millions(x: impl Into<i32>) -> i32 {
    x.into() + 42_000_000
}

fn pair_of(x: u32) -> impl std::fmt::Debug {
    (x + 1, x - 1)
}

fn pair_of<T: Debug>(x: u32) -> T {
    (x + 1, x - 1)
}

fn main() {
    let many = add_42_millions(42_i8);
    println!("{many}");
    let many_more = add_42_millions(10_000_000);
    println!("{many_more}");
    let debuggable = pair_of(27);
    println!("debuggable: {debuggable:?}");
}

Trait bounds using impl

Pattern matching

fn main() {
    let input = 'x';
    match input {
        'q'                       => println!("Quitting"),
        'a' | 's' | 'w' | 'd'     => println!("Moving around"),
        '0'..='9'                 => println!("Number input"),
        key if key.is_lowercase() => println!("Lowercase: {key}"),
        _                         => println!("Something else"),
    }
}

Pattern matching

fn main() {
    describe_point((1, 0));
}

fn describe_point(point: (i32, i32)) {
    match point {
        (0, _) => println!("on Y axis"),
        (_, 0) => println!("on X axis"),
        (x, _) if x < 0 => println!("left of Y axis"),
        (_, y) if y < 0 => println!("below X axis"),
        _ => println!("first quadrant"),
    }
}

Pattern matching

#[rustfmt::skip]
fn main() {
    let triple = [0, -2, 3];
    println!("Tell me about {triple:?}");
    match triple {
        [0, y, z] => println!("First is 0, y = {y}, and z = {z}"),
        [1, ..]   => println!("First is 1 and the rest were ignored"),
        [.., b] => // tails ignore head,
        [a@..,b] => // tail aliasing the head,
        _         => println!("All elements were ignored"),
    }
}

Pattern matching

fn sleep_for(secs: f32) {
    let dur = if let Ok(dur) = std::time::Duration::try_from_secs_f32(secs) {
        dur
    } else {
        std::time::Duration::from_millis(500)
    };
    std::thread::sleep(dur);
    println!("slept for {:?}", dur);
}

fn main() {
    sleep_for(-10.0);
    sleep_for(0.8);
}

Pattern matching

fn sleep_for(secs: f32) {
    let wahatever = if let Ok(d) = std::time::Duration::try_from_secs_f32(secs) {
        d
    } else {
        std::time::Duration::from_millis(500)
    };
    std::thread::sleep(whatever);
    println!("slept for {:?}", whatever);
}

fn main() {
    sleep_for(-10.0);
    sleep_for(0.8);
}

Pattern matching

fn sleep_for(secs: f32) {
    let wahatever = if let Ok(d) = std::time::Duration::try_from_secs_f32(secs) {
        d
    } else {
        std::time::Duration::from_millis(500)
    };
    std::thread::sleep(whatever);
    println!("slept for {:?}", whatever);
}

fn main() {
    sleep_for(-10.0);
    sleep_for(0.8);
}

Pattern matching

fn sleep_for(secs: f32) {
    let dur = if let Ok(dur) = std::time::Duration::try_from_secs_f32(secs) {
        dur
    } else {
        std::time::Duration::from_millis(500)
    };
    std::thread::sleep(dur);
    println!("slept for {:?}", dur);
}

fn main() {
    sleep_for(-10.0);
    sleep_for(0.8);
}

Modules

mod foo {
    pub fn do_something() {
        println!("In the foo module");
    }
}

mod bar {
    pub fn do_something() {
        println!("In the bar module");
    }
}

fn main() {
    foo::do_something();
    bar::do_something();
}

Modules

mod foo {
    pub fn do_something() {
        println!("In the foo module");
    }
}

mod bar {
    pub fn do_something() {
        println!("In the bar module");
    }
}

fn main() {
    foo::do_something();
    bar::do_something();
}

Rustlings

Project

  • Bevy (for building games) https://arewegameyet.rs/
     
  • Actix, Sqlx (for building web API) https://www.arewewebyet.org/
  •  Leptos, dioxus, yew (for frontend apps)
     
  • Tauri, egui (for desktop applications) https://areweguiyet.com/
     
  • Clap, ratatui (for CLI apps) https://rust-cli.github.io/book/index.html

https://wiki.mozilla.org/Areweyet

Resources

  • https://doc.rust-lang.org/book/title-page.html
  • https://google.github.io/comprehensive-rust/index.html
  • https://doc.rust-lang.org/rust-by-example/
  • https://practice.rs/why-exercise.html

Introduction to Rust

By Salama Ashoush

Introduction to Rust

  • 143