CS3984 Computer Systems in Rust



Hello World

  1. Create source code file

    fn main() {
        println!("Hello, world!");
    }
    
    
    
  2. Compile program

    $ rustc main.rs
    
    
    
  3. Run program

    $ ./main
    
    
    

Hello World (Cargo)

  1. Create project and enter project directory

    $ cargo new hello_world
    $ cd hello_world
    
    
    
  2. Edit source code file

    fn main() {
        println!("Hello, world!");
    }
    
    
    
  3. Build and run project

    $ cargo run
    
    
    

Quickstart: Printing

fn main() {
    println!("Hello world!");                    // Hello world!

    // Interpolation
    let x = 5;
    println!("This is x: {}", x);                // This is x: 5
    println!("This is x: {x}");                  // This is x: 5

    // Debug format
    let s = "hello world!";
    println!("Sentence: {s}");                   // Sentence: hello world!
    println!("Sentence: {s:?}");                 // Sentence: "hello world!"

    // Multiple variables
    let y = 6;
    println!("{x} + {y} = {sum}", sum = x + y);  // 5 + 6 = 11
}


Primitives

Scalar Types

  • Integers - i8, i16, u32, u64
  • Floating point numbers - f32, f64
  • Characters (Unicode scalar values) - 'a', 'æ'
  • Booleans - true, false
  • Unit type - ()

Compound Types

  • Arrays - [1, 2, 3, 4, 5]
  • Tuples - (1, 5.0, false)

Numeric Variables

fn main() {
    // Explicit type annotation
    let x: i8 = 5;

    // Numeric types can use type annotation suffix
    let y = 5i8;
    let z = 5.0f32;

    // Implicit type inference
    // Default integer has type `i32`
    let v = 5;
    // Default float has type `f64`
    let w = 5.0;

    // Integer literals can have base prefix
    let hex_literal = 0xff;
    let octal_literal = 0o77;
    let binary_literal = 0b1111;
}


Variable Assignment

  1. Variables are immutable by default:

    fn main() {
        let immutable = 5;
        immutable = 6;
        println!("The value of immutable is {immutable}");
    }
    
    
    
  2. Mutable variables are declared with the mut keyword:

    fn main() {
        let mut mutable = 5;
        mutable = 6;
        println!("The value of mutable is {mutable}");
    }
    
    
    

Scope

  1. The scope of a variable is the region of code where the variable name can be used to refer to a value.

    fn main() {
        let x = 5;
    }
    
    
    

Scope

  1. Different values can be referred to by the same name if the variables are in different scopes:
    fn main() {
        let x = 5;
        {
            let x = 10;
            println!("The inner value of x is {x}");
        }
        println!("The outer value of x is {x}");
    }
    
    
    

Variable Assignment and Scope

  1. Immutable variables in the same scope can be overwritten with shadowing:
    fn main() {
            let immutable = 5;
            println!("The value of immutable is {immutable}");
    
            let immutable = 6;
            println!("The value of immutable is {immutable}");
    }
    
    
    

Variable Assignment and Scope

  1. Shadowing can overwrite the type of a variable:
    fn main() {
            let x = "hello";
            println!("The value of x is {x}");
    
            let x = x.len();
            println!("The value of x is {x}");
    }
    
    
    

Integer Overflow

Unsigned integer overflow wraps.
Signed integer overflow is undefined behavior.

#include <stdio.h>
#include <stdint.h>

int main(void) {
    int32_t s = INT32_MAX;
    s++;
    uint32_t u = UINT32_MAX;
    u++;

    printf("s = %d\n", s);
    printf("u = %d\n", u);
}


In debug mode, integer overflow panics.
In release mode, integer overflow wraps.

fn main() {
    let mut s = i32::MAX;
    s += 1;
    let mut u = u32::MAX;
    u += 1;

    println!("s = {s}");
    println!("u = {u}");
}


Integer Overflow In Rust

These methods are available for numeric types to explicitly handle overflow:

fn main() {
    let s = i32::MAX;
    let wrapping = s.wrapping_add(1);
    let overflowing = s.overflowing_add(1);
    let checked = s.checked_add(1);
    let saturating = s.saturating_add(1);
    println!("wrapping: {wrapping:?}");
    println!("overflowing: {overflowing:?}");
    println!("checked: {checked:?}");
    println!("saturating: {saturating:?}");
}


wrapping: -2147483648overflowing: (-2147483648, true)checked: Nonesaturating: 2147483647


Tuples

  • Tuples are fixed-length, heterogenous collections of values.
  • Tuples are indexed by position using dot access.
  • Tuples can be destructured.
    fn main() {
        let mut tup = (1.0, true, "crab");
        println!("Third element = {:?}", tup.2);
    
        tup.1 = false;
        println!("Tuple = {:?}", tup);
    
        let (_first, _second, third) = tup;
        println!("Third element = {:?}", third);
    }
    
    
    

Unit Type

  • A unit is a tuple with 0 elements.
fn main() {
    let x = ();
    println!("Value: {:?}", x);
}


  • Expressions that do not return any value return the unit value.
fn empty_function() {}
fn main() {
    let return_value = empty_function();
    println!("Value: {:?}", return_value);
}


Arrays

  • Arrays are fixed-length, homogenous collections of values.
  • Arrays are indexed by position using indexing.
  • Array access is bounds-checked at both compile and runtime.
fn main() {
    let mut arr = ["Jan", "Fob", "Mar", "Apr", "May"];
    println!("Third element = {:?}", arr[2]);

    arr[1] = "Feb";
    println!("Array = {:?}", arr);
}


Arrays (2)

  • The type of an array is specified using the element type and number of elements.
fn main() {
    let days: [i32; 3] = [1, 2, 3];
    let months: [&str; 5] = ["Jan", "Feb", "Mar", "Apr", "May"];
}


  • An array can be initialized with the same value for all elements.
fn main() {
    let five_crabs: [&str; 5] = ["crab"; 5];
    println!("{:?}", five_crabs);
}


Functions

  • Defining a function requires the fn keyword, a function name, a set of parentheses, and a set of curly brackets.

    The curly brackets denote the function body.

    fn empty_function() {}
    
    
    
  • Functions can have parameters, which are listed in the parentheses and must contain the type of the parameter.

    fn accepts_numbers(x: u8, y: i32) {
        // Do something with x and y
    }
    
    
    

Functions

  • Functions can have a return value. The type of the return value must be listed after the parentheses and prefixed with a right arrow.

    fn returns_number() -> i32 {
    }
    
    
    

Functions (2)

  • Function bodies are a list of statements optionally ending in an expression.

    • Expressions are code that produce values, and may cause side effects.
    • Ending an expression with a semicolon produces an expression statement, where the result of the expression is ignored.
    fn print_number(x: u8) {
        println!("x = {x}");
    }
    
    
    
  • An expression at the end of the function body is the return value of the function.

    fn print_number_and_return(x: u8) -> u8 {
        println!("x = {x}");
        x
    }
    
    
    

if Expressions

if expressions allow branching code depending on a boolean condition.

fn main() {
    let value = 2;
    if value < 3 {
        println!("Value is less than 3");
    } else if value < 5 {
        println!("Value is less than 5");
    } else {
        println!("Value is greater or equal to 5");
    }
}


if Expressions (2)

Since if expressions are not statements, they return values that can be assigned to variables.

fn main() {
    let value = 2;
    let statement: &str = if value < 5 {
        "Value is less than 5"
    } else {
        "Value is greater or equal to 5"
    };

    println!("Statement: {statement}");
}


if Expressions (3)

All branches of if expressions must return a value of the same type:

fn main() {
    let statement = "crab";
    let points = if statement == "hello world!" {
        100
    } else {
        "this is not a number"
    };

    println!("Points: {points}");
}


Loops

The loop keyword is used to make an unconditional loop.

fn main() {
    loop {
        println!("crab");
    }
}


Loops (2)

The break and continue keywords allow controlling the flow of the loop.

fn main() {
    let mut x = 0;
    loop {
        println!("{x}");
        x += 1;

        // `continue` immediately starts the next iteration of the loop
        if x == 6 {
            continue; // Skip the number 6
        }

        // `break` immediately ends the loop
        if x == 8 {
            break; // Stop printing if the number is now 8
        }
    }
}


Loops (3)

Loops can return values through the break keyword.

fn main() {
    let mut x = 0;
    let y = loop {
        x += 1;

        if x == 5 {
            break x * 10;
        }
    };
    println!("y = {y}");
}


Loops (4)

Loops can be labelled with a single quote. Loop labels can be used with break and continue.

#![allow(unreachable_code, unused_labels)]

fn main() {
    'outer: loop {

        'inner: loop {
            // break 'inner;
            break 'outer;
        }

        println!("After inner loop");
        break;
    }
    println!("After outer loop");
}


while Loops

while loops allow looping based on a boolean condition.

fn main() {
    let mut n = 0;

    while n != 10 {
        println!("Seen {n} sheep jumping over a fence.");
        n += 1;
    }

    println!("Zzzzz...");
}


for Loops

for loops allow iterating over a collection.

fn main() {
    let months = ["Jan", "Feb", "Mar"];

    for element in months {
        println!("It is now {element}");
    }
}


The while loop from the previous example may be concisely written using a for loop and a Range.

fn main() {
    for n in 0..10 {
        println!("Seen {n} sheep jumping over a fence.");
    }
    println!("Zzzzz...");
}


Structs

Structs are defined by specifying the name of the struct, as well as its fields. Each field of the struct has a name and a type.

struct User {
    active: bool,
    username: String,
    sign_in_count: u64,
}


An instance of a struct can be created by supplying each field with a concrete value for that type:

fn main() {
    let user = User {
        active: true,
        username: String::from("crabby"),
        sign_in_count: 1,
    };
}


Structs (2)

Fields of the struct can be accessed using dot access, including for mutation.

fn main() {
    let mut user = User {
        active: true,
        username: String::from("crabby"),
        sign_in_count: 1,
    };
    user.active = false;
    println!("{}", user.username);
}


Structs (3)

When initializing a struct, fields that are not explicitly given can be initialized using values from another struct:

fn main() {
    let user_1 = User {
        active: true,
        username: String::from("crabby"),
        sign_in_count: 1,
    };
    let user_2 = User {
        username: String::from("crabby"),
        // Note that this line cannot end in a comma
        ..user_1
    };
}


Tuple Structs

Tuple structs are structs that do not have named fields; think of them as named tuples. Tuple struct fields can be accessed using dot notation.

struct Point(i32, i32, i32);

fn main() {
    let mut center = Point(0, 0, 0);
    center.1 = 1;
    println!("{} {} {}", center.0, center.1, center.2);
}


Struct Methods

Struct methods are functions that are attached to an instance of a struct. They allow access to the struct the method is called on through the self parameter.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect_1 = Rectangle { width: 30, height: 50 };
    println!("Area of the rectangle: {} square pixels.", rect_1.area());
}


Enumerations

Enumerations, or enums, allow you express that a certain type has one of a few known-before-hand set of values.

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let ip_four = IpAddrKind::V4;
    let ip_six = IpAddrKind::V6;
}


Enumerations (2)

Enums can store values in the variants. Each variant can store a different type.

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

fn main() {
    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback =IpAddr::V6(String::from("::1"));
}


Enumerations (3)

There are various ways of embedding values in enum variants:

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

fn main() {
    let msg_a = Message::Quit;
    let msg_b = Message::Move { x: 5, y: 6 };
    let msg_c = Message::Write(String::from("hello world!"));
    let msg_d = Message::ChangeColor(255, 255, 255);
}


Enumerations (4)

You can map enum variants to values with a match statement:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}


Enumerations (5)

Match arms (the => ...) can run multiple lines of code:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        },
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}


Enumerations (6)

Enum variants that store values…

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}


Enumerations (7)

…can be extracted in a match statement:

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {state:?}!");
            25
        }
    }
}