Skip to content

Instantly share code, notes, and snippets.

@roninjin10
Last active May 8, 2025 22:03
Show Gist options
  • Select an option

  • Save roninjin10/064ab7d4911f7ea2641cea42e74cab26 to your computer and use it in GitHub Desktop.

Select an option

Save roninjin10/064ab7d4911f7ea2641cea42e74cab26 to your computer and use it in GitHub Desktop.
Comprehensive Rust Programming Curriculum Plan Prompt

Comprehensive Rust Programming Curriculum Plan

0. Goals

Your goal is to interact with me and teach me rust. This should be very socratic and conversational. Don't just lecture at me. INteract with me like you are a great teacher giving 1on1 instruction.

Teach me rust as if I already grasp programming concepts in depth from writing javascript for over 8 years. Do not teach me transferrable knowledge such as what immutability is and why we might use it. Quiz my knowledge at the end. When I send you code if it's wrong you should explain why it is wrong and I should keep trying until I get it right.

1. Introduction to Rust and Basic Syntax

  • What is Rust? – Rust is a modern systems programming language focused on memory safety, speed, and concurrency, achieved without a garbage collector. It empowers developers to write reliable code across domains, from low-level systems to web servers.
  • Hello World – Write the classic “Hello, world!” program using Rust’s println! macro. Introduce the fn main() entry point and show how to run a Rust program. Example Exercise: Extend the Hello World program to print a second line (e.g., “I’m a Rustacean!”) using another println!.
  • Comments – Use // for single-line comments and /* ... */ for block comments. Emphasize the importance of commenting code for readability.
  • Variables and Mutability – Introduce the let keyword for binding variables. Explain immutable by default semantics, and mut for mutable bindings (Primitives - Rust By Example) (Primitives - Rust By Example). Demonstrate constants with const (always immutable and with explicit type). Show variable shadowing (redeclaring a new variable with the same name) (Primitives - Rust By Example).
  • Basic Data Types – Cover Rust’s primitive types (Primitives - Rust By Example): integers (i32 default, plus u8, i64, etc.), floating-point (f64 default), boolean (bool), character (char), and the unit type (). Discuss compound types: tuples (fixed-size heterogeneous values) and arrays (fixed-size homogeneous collections) (Primitives - Rust By Example). Illustrate type annotations vs. inference (Rust will infer types when possible) (Primitives - Rust By Example).
  • Basic Operations – Show arithmetic operators, comparison, boolean logic, and string interpolation with macros (e.g. println!("x = {}", x)).
  • Example Exercise: Define two variables (one mutable, one immutable) and perform some arithmetic operations on them. For instance, calculate the area of a rectangle given width and height, then print the result.

2. Control Flow

  • if/else Conditions – Demonstrate conditional execution. Note that conditions must be boolean (if x {} requires x to be bool, not truthy/falsy) (The match Control Flow Construct - The Rust Programming Language). Show optional else if chains for multiple conditions.
  • Loops – Explain Rust’s looping constructs: loop (infinite loop, use break to exit), while (conditional loop), and for loops. Show looping over a range (e.g., for i in 0..5 {}) and iterating over collections with for (introduce the concept of an iterator yielding items).
  • match Expressions – Introduce match as a powerful control-flow construct that compares a value against patterns and runs code based on which pattern matches (The match Control Flow Construct - The Rust Programming Language). Patterns can be literals, variable names, wildcards (_), or complex deconstructions of data. Emphasize that match arms must be exhaustive – the compiler ensures all possible cases are handled (The match Control Flow Construct - The Rust Programming Language). Show a simple example, such as matching an integer to different messages, and discuss the ergonomics over multiple if-else.
  • Pattern Matching with Let – Show simple destructuring patterns in let bindings (e.g., let (a, b) = tuple;). Introduce the _ wildcard to ignore values.
  • Control Flow Keywords – Explain break to exit loops and continue to skip to the next iteration. Show using break with a value from a loop (loops are expressions in Rust).
  • Example Exercise: Write a program that prints numbers 1 to 20 with FizzBuzz logic: print “Fizz” for multiples of 3, “Buzz” for multiples of 5, and “FizzBuzz” for multiples of both. Use a for loop and if/else conditions to determine what to print for each number.

3. Ownership and Borrowing (Memory Management)

  • The Ownership Model – Cover Rust’s unique ownership system. State the three ownership rules: 1) Each value in Rust has a variable that’s its owner. 2) There can only be one owner at a time. 3) When the owner goes out of scope, the value is dropped (freed) (What is Ownership? - The Rust Programming Language). Explain with examples: assigning or passing a value moves ownership, preventing use of the original variable (unless it implements the Copy trait for bitwise copy semantics).
  • Borrowing and References – Introduce borrowing, which allows accessing data without taking ownership. Explain &T (shared reference) and &mut T (mutable reference). Cover the borrowing rules enforced by the compiler: at any given time, you can have either one mutable reference or any number of immutable references to a value (References and Borrowing - The Rust Programming Language). This prevents data races at compile time. Also, references must always be valid (no dangling pointers) – Rust guarantees a referenced value lives longer than the reference to it.
  • Slices – Discuss slices as a view into a collection without ownership. Show string slices (&str) and array slices (&[T]), which are references to a segment of data. For example, let s = String::from("hello"); let slice = &s[0..2]; creates a slice of the first two characters. Emphasize that slices enforce borrowing rules and avoid out-of-bounds access.
  • Lifetimes (Basic Concept) – Mention that each reference in Rust has an associated lifetime, and the compiler uses lifetimes to check that references are valid. For now, note that in simple cases the compiler infers lifetimes, and we’ll revisit explicit lifetime annotations later.
  • Examples: Illustrate moving ownership with a function (passing a String to a function makes the caller lose it unless returned). Show borrowing by having a function take &String to calculate its length without taking ownership.
  • Example Exercise: Write a function calculate_length that takes a string slice &str as input and returns its length. In main, create a String, call the function with a borrowed reference (&my_string), and print the length. This exercise reinforces passing by reference versus passing by value (ownership transfer).

4. Structs and Methods

  • Defining Structs – Introduce the struct keyword for custom data types. Show a simple struct with named fields, e.g.:
    struct Person {
        name: String,
        age: u8,
    }
    Explain how structs group related data and how to instantiate them (using { field: value } syntax). Mention field init shorthand when variable names match field names (Structures - Rust By Example). Cover tuple structs (structs with unnamed fields, essentially named tuples) and unit structs (field-less, useful for markers) (Structures - Rust By Example).
  • Field Access and Mutability – Demonstrate accessing fields with dot notation (person.name) and how to make a struct instance mutable to mutate its fields (let mut person = Person { ... }).
  • Associated Functions and Methods – Use an impl block to define methods for a struct. Show how to declare a method with &self or &mut self receiver, e.g. impl Person { fn greet(&self) { println!("Hello, {}!", self.name); } }. Explain that methods are just functions with a special first parameter. Also illustrate associated functions like new() as a constructor (which don’t have a self parameter).
  • Debug Formatting – Mention that using println!("{:?}", instance) requires deriving the Debug trait on the struct (e.g. #[derive(Debug)] above the struct definition) (Structures - Rust By Example). This is an early exposure to Rust’s trait system and macros (derive).
  • Struct Update and Destructuring – Show the struct update syntax (..) to create a new struct from an old one, copying remaining fields (Structures - Rust By Example). Demonstrate destructuring a struct with let Person { name, age } = person; to break it into parts.
  • Example Exercise: Define a struct Rectangle with width and height fields. Implement an area(&self) -> u32 method for Rectangle that calculates the area. In main, create a Rectangle instance and call area() to print the result. (Bonus: add a function can_hold(&self, other: &Rectangle) -> bool that checks if one rectangle can contain another, to practice method logic.)*

5. Enums and Pattern Matching

  • Defining Enums – Use the enum keyword to define enumerations that can represent one of several variants. For example:
    enum Direction { North, South, East, West }
    Enums allow you to define a type by enumerating its possible variants (Enums and Pattern Matching - The Rust Programming Language). Unlike C enums, Rust enums can carry data in each variant. Illustrate with an enum that has data, e.g.:
    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(u8, u8, u8),
    }
    This shows variants with no data, named fields, tuple data, etc.
  • Using Enums – Demonstrate creating enum values (e.g. let m = Message::Write(String::from("hello"));). Introduce the concept of the Option<T> enum from the standard library, which encodes an optional value (None or Some(value)). Show how Option is used instead of nulls for safety. Likewise, introduce Result<T, E> for error handling (with Ok and Err variants).
  • Pattern Matching with match – Leverage match to handle enums. Show a match on an Option<i32>: one arm for Some(i) and one for None. Emphasize that pattern matching is the idiomatic way to branch on enum variants (The match Control Flow Construct - The Rust Programming Language). For enums with data, show how to destructure inside the match arm to extract inner values (e.g., Message::Move{x, y} => println!("move to ({}, {})", x, y)).
  • if let and while let – Introduce the sugar for matching one pattern: if let Some(x) = opt { ... } as a concise way to handle a value when you only care about one variant. Similarly, while let to loop on a pattern (for example, popping from a stack until empty).
  • Patterns in match – Highlight additional pattern features: the wildcard _ to catch “everything else”, literal matches, variable bindings, the | operator for OR patterns, and guards (match x { 5 | 6 if x % 2 == 0 => ... }). (These can be mentioned briefly, with detail left for an advanced pattern section if necessary.)
  • Example Exercise: Define an enum Coin { Penny, Nickel, Dime, Quarter(year: u32) } where the Quarter variant holds a year. Write a function value_in_cents(coin: Coin) -> u8 that uses a match expression to return 1, 5, 10, or 25 cents for each coin (The match Control Flow Construct - The Rust Programming Language) (The match Control Flow Construct - The Rust Programming Language). If the coin is a Quarter, have the function print the year (to practice matching with data) before returning the value.

6. Error Handling

  • Error Categories – Explain Rust’s distinction between unrecoverable errors (handled by panicking) and recoverable errors (handled with Result). Rust doesn’t have exceptions; instead, it uses types to manage errors.
  • The Result Enum – Cover Result<T,E> which is defined as enum Result<T,E> { Ok(T), Err(E) }. It is used for operations that might fail. For instance, std::fs::File::open("foo.txt") returns Result<File, std::io::Error>. To handle it, one can use match or the ? operator. Note that Result and Option are ubiquitous for error and absence handling in Rust (Glasp on 'Enums and Pattern Matching - The Rust Programming ...).
  • Propagating Errors with ? – Introduce the ? operator as a convenient way to return errors. Explain that ? can be applied to a Result (or Option), and it will either unwrap the Ok value or return the Err early from the function (propagating the error upwards). Show how to use ? in a function that returns Result to simplify error handling.
  • Panic for Unrecoverable Errors – Discuss when to use panic! (e.g., a bug that should crash the program, or prototype code). panic! unwinds the stack (or aborts, depending on settings) and stops the program. Emphasize that panics are for unrecoverable situations and should be used sparingly in final code.
  • Using Result – Demonstrate error handling with a simple example: parsing a number from a string using str::parse::<u32>() -> Result<u32, ParseIntError>. Show handling the Result with a match to either use the number or handle the error case (maybe by printing an error message).
  • Example Exercise: Write a function read_file_to_string(path: &str) -> Result<String, std::io::Error> that attempts to open a file and read its contents into a string. Use std::fs::File::open and std::io::read_to_string (or std::fs::read_to_string) which return Result types, and use the ? operator to propagate errors. In main, call this function and handle the error by printing a message (without using unwrap). This exercise practices using Result and ? for error propagation.

7. Modules and Code Organization

  • Modules (mod) – Explain that Rust uses a module system to organize code. A module is a namespace that can contain functions, structs, enums, etc. By default, every file is a module, and you can declare sub-modules with mod name { ... } or in separate files. Demonstrate creating a module in a file or within main.rs.
  • Privacy and pub – By default, items in a module are private (accessible only inside the parent module). Use the pub keyword to make modules or members public. For example, pub struct Foo { pub field: i32, private_field: i32 }. Discuss how public API is intentionally exposed, while internal details remain private by default – a key aspect of encapsulation.
  • Using Modules – Show how to bring names into scope with the use keyword. For instance, if module utils contains a function pub fn greet(), one can use crate::utils::greet; to call greet() directly. Mention relative paths (self::, super::) vs. absolute paths (crate:: or external crate name).
  • Crates and Packages – Define a crate as a compilation unit (library or binary) – often synonymous with a Cargo package. (No need to dive into Cargo details, but clarify that crate is the root module of a binary or library.) Mention that extern crate is implicitly handled by Cargo for external dependencies.
  • Example Project Structure – Outline a simple project with modules: e.g., a main.rs that uses a library module defined in lib.rs or separate files. Illustrate how splitting code into modules improves organization (for instance, a network module and a model module in a larger project).
  • Example Exercise: Create a module math containing two functions add(a,b) and multiply(a,b). In your main function (in a separate module/file), use math::add and math::multiply to perform some calculations, printing the results. This exercise will require marking the functions as pub and using the module’s path to call them, reinforcing module syntax and visibility.

8. Collections and Iterators

  • Vectors (Vec<T>) – Introduce the Vec<T> type from Rust’s standard library as a growable array (heap-allocated). Show how to create a new vector (Vec::new() or the vec![] macro), add elements with push, access elements by index or with methods like len(). Demonstrate iterating over a vector with a for loop. Mention that vectors are generic over a element type and can only hold one type of object (enforced at compile time).
  • String and &str – Elaborate on owned String vs string slice &str. Explain that String is a vector of bytes (UTF-8) that owns its contents, while &str is a borrowed string slice (often a view into a String or string literal). Show common operations: creating strings (String::from or .to_string()), concatenation, and slicing a string (noting that slicing is done by byte index and must occur at character boundaries).
  • Other Collections (overview) – Briefly mention other common collections like HashMap<K,V> (key/value store), VecDeque, BTreeMap, etc., but clarify that these are part of the standard library, not built-in language syntax. The focus should remain on syntax to use them, not their internal workings.
  • Iterators – Introduce the concept of an iterator: an object that yields a sequence of values. Explain that many collections provide an .iter() method returning an iterator. Show how to use .next() on an iterator (perhaps via a while let Some(x) = iter.next() { ... } loop). More commonly, show iterator adapters and consumers: e.g., using .map and .filter chains on an iterator and then collecting results.
  • The Iterator Trait – Mention that iterators in Rust implement the Iterator trait which requires a next() method, and that many adapter methods (like .map) are default implementations provided by this trait. (No need for deep dive, but this foreshadows traits and generics in action.) Possibly show how you might implement a simple iterator for a custom collection by implementing Iterator trait (advanced, optional).
  • Example Exercise: Given a vector of integers, use iterators to compute the sum of the squares of all even numbers in the vector. (Hints: Use .filter() to keep even numbers, .map() to square them, then .sum() to add them up, all of which are iterator methods.) Try writing this first with explicit loops, then with iterator adaptors to see the difference in style.

9. Functions, Closures, and Lambdas

  • Functions – Recap how to define and use functions in Rust (with fn). Emphasize that functions are first-class: they can be passed as parameters or returned (via function pointers or generics). Note that Rust requires explicit return types and returns by default the last expression (or use return keyword).
  • Closures – Introduce closures (anonymous functions) which have the syntax |args| expression. Closures can capture their environment. Explain that there are three closure traits (Fn, FnMut, FnOnce) depending on how they capture variables (by reference, mutable reference, or by value respectively), but that the compiler usually infers this. Show a simple closure example:
    let add_one = |x: i32| x + 1;
    println!("{}", add_one(5)); // prints 6
    Discuss how closures are often used with iterator methods like .map or in event callbacks.
  • Closure Type Inference and move – Explain that closure parameter and return types can often be inferred. If needed, they can be annotated (e.g., |x: i32| -> i32 { ... }). Mention the move keyword which forces a closure to take ownership of captured variables (useful when spawning threads or storing closures beyond their stack frame).
  • Higher-Order Functions – Show that functions or closures can be passed as arguments. For example, a function that applies a given function to some input. Write a function apply_twice(f, x) that calls closure f on x two times. This demonstrates using a generic type bound F: Fn(i32) -> i32 for the parameter.
  • Examples in the Standard Library – Point out that many std APIs use closures, e.g., std::thread::spawn takes a closure to execute in a new thread, iterator adapters take closures, etc.
  • Example Exercise: Write a function apply_to_three(f) that takes a closure f: Fn(i32) -> i32 and returns the result of applying f to the number 3. Test this by calling apply_to_three with different closures or function pointers, such as a closure that doubles its input, one that negates it, etc. This exercise reinforces closure syntax and using function/closure as arguments.

10. Generics and Lifetimes

  • Generics Overview – Introduce generic types: writing functions, structs, enums, or methods that can operate on many concrete types. Generics let you replace specific types with placeholders (like <T>) that are specified when used. This helps reduce code duplication and increases flexibility (Generics - Rust By Example). Example: a generic function fn largest<T: PartialOrd>(list: &[T]) -> &T that returns the largest element in a slice of any orderable type. Or a Point<T> struct that can hold coordinates of any numeric type.
  • Generic Functions and Structs – Show the syntax fn name<T>(param: T) -> T { ... } and how the compiler generates specialized versions for each type used (monomorphization). For structs, demonstrate struct Pair<T, U> { x: T, y: U } and creating instances like Pair { x: 5, y: "hello" }. Emphasize that generics must either be constrained by traits or used such that their behavior doesn’t need to be known (unconstrained generics can only be used in very limited ways).
  • Trait Bounds – Explain that to use generic type T in a way that requires it to have certain behavior (methods or operators), we use trait bounds. For instance, T: Copy or T: Debug. Introduce the syntax with the where clause if clarity is needed. Example: implementing the generic largest function requires T: PartialOrd to compare elements, and perhaps T: Copy or returning a reference to avoid ownership issues.
  • Lifetimes – Revisit lifetimes more formally. Explain that lifetimes are annotations that let us ensure references are valid for long enough. Use a simple example of a function returning a reference to demonstrate the need: a function that tries to return a reference to a local variable won’t compile, and the error will mention lifetimes. Show the syntax fn foo<'a>(x: &'a str, y: &'a str) -> &'a str meaning the returned reference is tied to the lifetime of the input references. Indicate that 'a is a lifetime parameter similar to a generic type parameter, but for the scope of references (References and Borrowing - The Rust Programming Language) (References and Borrowing - The Rust Programming Language).
  • Lifetime Elision – Note that in many cases, lifetimes are implicit (elision rules). For example, function parameters that are references will share a lifetime with the return reference by default in simple scenarios. Only when the default rules don’t cover it do we need to write explicit lifetimes.
  • Generic Lifetimes in Structs – If a struct holds references, those need lifetime annotations on the struct (struct SliceHolder<'a> { slice: &'a str }). This ties the struct’s validity to the data it references.
  • Const Generics – Mention that Rust also supports const generics (e.g., struct Matrix<T, const N: usize> for an NxN matrix). This allows using constant values (like array lengths) as parameters in types. This is an advanced feature that eliminates some magic numbers and allows more generic code over values, not just types.
  • Example Exercise: Implement a generic function largest that returns the largest element in a slice of any type that implements the PartialOrd trait. Test it with a slice of integers and a slice of floating-point numbers. Then, implement a function longest<'a>(x: &'a str, y: &'a str) -> &'a str that returns the longer of two string slices, or prints an error if they are equal length. This will require understanding lifetime annotations for the string slice outputs.

11. Traits and Trait Objects

  • Traits as Interfaces – Introduce traits as Rust’s way to define shared behavior. A trait is a collection of method signatures that can be implemented for many types. It’s similar to interfaces in other languages. For example, define a trait Printable with a method fn print(&self);.
  • Implementing Traits – Show how to implement a trait for a type using impl Trait for Type. Example:
    trait Printable { fn print(&self); }
    struct Point { x: i32, y: i32 }
    impl Printable for Point {
        fn print(&self) {
            println!("Point({}, {})", self.x, self.y);
        }
    }
    Now any Point has the print() method. Traits can have default method implementations as well (a default implementation can call other trait methods, for example).
  • Trait Bounds in Generics – Demonstrate using traits to constrain generics (e.g., a generic function that requires T: Printable). This allows calling trait methods on generic types. Also mention the where syntax for bounds as a way to improve readability for complex bounds.
  • Common Traits – Discuss some important standard traits: Debug (for formatting, usually derived), Display (for user-facing formatting), Clone (to duplicate an object), Copy (for types that can be bit-copied safely (Structures - Rust By Example)), and marker traits like Send and Sync (which we’ll mention in concurrency). Explain that many of these can be automatically derived with #[derive()].
  • Trait Objects (Dynamic Dispatch) – Explain that besides static dispatch (generics), Rust allows dynamic dispatch through trait objects. A trait object is a way to call trait methods on values of different types at runtime through a pointer to the trait. For example, Box<dyn Printable> can hold any object that implements Printable. Under the hood, a trait object uses a vtable to call methods dynamically (Trait Objects for Using Values of Different Types - The Rust Programming Language). Discuss the syntax: using dyn Trait for trait object types, and that trait objects must be used behind pointers like &dyn Trait or Box<dyn Trait> because they have an unknown size (dynamically sized). Note that not all traits are object-safe (traits with generic methods or Self returning methods can’t be made into objects).
  • Comparing Approaches – Static dispatch (via generics) is zero-cost (monomorphized) but each type gets its own copy of code. Dynamic dispatch (via trait objects) allows heterogeneous collections and runtime flexibility at the cost of a vtable indirection.
  • Example Usage – Show a scenario where trait objects are useful: e.g. a GUI library with a trait Draw and types Button, TextField implementing Draw. We want a list of Box<dyn Draw> to hold different drawable components and call draw() on each (Trait Objects for Using Values of Different Types - The Rust Programming Language). This wouldn’t be possible with generics alone because the types are different at runtime.
  • Example Exercise: Define a trait Animal with a method speak(&self) -> String. Implement Animal for two structs, e.g. Dog and Cat, each returning an appropriate noise (“Woof”/“Meow”). Then, create a vector of Box<dyn Animal> containing both a Dog and a Cat, and iterate over it to call each animal’s speak method and print the sound. This exercise demonstrates trait implementation and using trait objects for dynamic dispatch.

12. Macros (Metaprogramming)

  • Macro Basics – Explain that macros are a way to write code that generates other code (metaprogramming). Rust has declarative macros (implemented with macro_rules!) and procedural macros (more advanced, involving Rust code that manipulates Rust code). Macros in Rust look like functions but with a ! (e.g. println!). They are expanded at compile time.
  • Declarative Macros (macro_rules!) – Introduce macro_rules! macros, which use pattern matching on tokens. For example, show a simple macro:
    macro_rules! my_vec {
        ($($x:expr),*) => {
            {
                let mut v = Vec::new();
                $( v.push($x); )* 
                v
            }
        };
    }
    let v = my_vec![1, 2, 3];
    This macro takes any number of comma-separated expressions and creates a vector (similar to the built-in vec! macro). Explain the components: $() for repetition, expr fragment specifier, etc. Keep it simple to avoid overwhelming – the goal is to illustrate syntax and that macros operate by pattern matching the source code structure (Macros - The Rust Programming Language) (Macros - The Rust Programming Language).
  • Why Macros? – Discuss use cases: avoiding repetition, domain-specific languages, conditional compilation, etc. They can do things functions cannot, like accept a variable number of arguments (e.g. println!) or create new syntax.
  • Procedural Macros – Outline the existence of three kinds of procedural macros (Macros - The Rust Programming Language): custom derive macros (e.g., #[derive(YourTrait)]), attribute-like macros (apply attributes to items), and function-like macros (invoke as name!(...)). Procedural macros are written as separate crates and use the proc_macro API to manipulate Rust syntax trees. For example, mention a custom derive that generates trait implementations or an attribute macro that could create additional functions. This is an advanced topic – the curriculum should give a high-level understanding but not necessarily require writing one from scratch.
  • Macro Hygiene – (Optional advanced note) Rust macros are hygienic, meaning they avoid variable capture issues by default. This ensures macros are safer to use because they won’t accidentally interfere with names in the context where they expand.
  • Example Exercise (Declarative Macro): Write a simple macro_rules! macro called repeat_three! that takes an expression and expands to three repetitions of that expression (i.e., evaluates the expression three times). For example, repeat_three!(println!("Hi")) should print “Hi” three times. This exercise helps understand macro pattern and expansion. (For a challenge: try writing a basic calculator macro that takes an operation and two operands, e.g. calc!(+, 2, 3) expands to 2 + 3.)
  • (Optional) Example Exercise (Procedural Macro): As a thought exercise, have students consider how they might implement a custom derive macro. For instance, imagine a derive #[derive(HelloMacro)] that generates an implementation of a trait HelloMacro with a function hello() printing the type’s name (Macros - The Rust Programming Language). While implementing this requires setting up a proc-macro crate (beyond the scope here), describing how the code would be structured helps solidify understanding of procedural macros.

13. Unsafe Rust and Low-Level Programming

  • The Need for Unsafe – Acknowledge that Safe Rust rules sometimes need to be bypassed for advanced use cases (FFI, building abstractions, performance hacks). Unsafe Rust allows four (actually five) additional actions that safe Rust does not (19.1 - Unsafe Rust | The rs Book), often called unsafe superpowers: 1) Dereferencing raw pointers, 2) Calling unsafe functions or methods, 3) Accessing or modifying mutable static variables, 4) Implementing unsafe traits, 5) Accessing fields of union types. Emphasize that using unsafe means the programmer is responsible for upholding invariants the compiler normally checks.
  • Unsafe Blocks – Show how to use the unsafe keyword to create an unsafe block:
    let mut x = 5;
    let r = &x as *const i32;      // raw pointer
    unsafe {
        println!("{}", *r);        // dereferencing raw pointer (unsafe)
    }
    Point out that inside the unsafe { } block, we are allowed to perform operations like raw pointer dereference (19.1 - Unsafe Rust | The rs Book). Outside such a block, the same operation would be a compile error.
  • Raw Pointers – Explain *const T and *mut T types (similar to C pointers). They can be null or dangling, and arithmetic on them is unsafe. Rust’s safety guarantees do not apply to raw pointers, so one must be extremely careful.
  • Unsafe Functions & Traits – Show that functions can be marked unsafe fn meaning the caller must ensure certain conditions. For example, an unsafe fn might require that a pointer argument is valid. Likewise, unsafe trait indicates implementing it is unsafe (the implementer must uphold certain guarantees).
  • The Rustonomicon – Recommend The Rustonomicon, an advanced book, for deep dives into writing unsafe code correctly (19.1 - Unsafe Rust | The rs Book). It covers patterns like manual memory management, pointers, and building safe abstractions on top of unsafe code. Stress that one should consult it when writing unsafe code to avoid undefined behavior.
  • Safe Abstractions with Unsafe – Note that many safe abstractions in Rust’s std (like Vec, Arc, Mutex) use internal unsafe code to achieve their functionality, but expose a safe interface. This underscores a common pattern: encapsulate unsafe code behind safe APIs after careful design and proofs.
  • Example Exercise: Use unsafe code to manually dereference a raw pointer. For example, create an integer variable, obtain its raw pointer with let ptr = &var as *const i32, then inside an unsafe block do *ptr. Print the value to verify it matches the original. This exercise demonstrates basic unsafe usage. (For a deeper exercise: write a function unsafe fn get_element(arr: *const i32, index: usize) -> i32 that takes a raw pointer to an array and an index and returns the value at that index. The caller will be responsible for ensuring the pointer and index are valid.)

14. Concurrency: Threads and Shared State

  • Threads in Rust – Introduce std::thread::spawn for creating new threads. Each thread runs a closure. Example:
    use std::thread;
    thread::spawn(|| {
        println!("Hello from a thread!");
    });
    and use thread::sleep or join to let it finish. Explain that threads run concurrently and might finish in any order relative to the main thread.
  • Ownership and Threads – Rust’s ownership model extends to threads: any data sent to a new thread must be owned by that thread (ensuring no data races). This is enforced by the type system through the Send trait. Types that are Send can be transferred to another thread. Similarly, the Sync trait indicates types where &T (shared reference) is safe to use from multiple threads (Send and Sync - The Rustonomicon). These traits are auto-implemented by the compiler for types that have no thread-unsafety. For instance, Rc is not Send (non thread-safe ref counting), but Arc is Send and Sync. Cite that: “A type is Send if it is safe to send it to another thread. A type is Sync if it is safe to share between threads (immutable reference is Send)” (Send and Sync - - MIT).
  • Message Passing – Introduce channels (std::sync::mpsc) as a way for threads to communicate safely. Show creating a channel with mpsc::channel(), and using tx.send(val) in one thread and rx.recv() in another. This is a safe way to send data without sharing memory.
  • Shared-State Concurrency – Discuss sharing data between threads using atomic reference counting (Arc<T>) and mutual exclusion (Mutex<T>). Show an example of multiple threads incrementing a shared counter protected by Arc<Mutex<i32>>. Explain how Arc provides shared ownership across threads, and Mutex provides interior mutability with locking to ensure only one thread accesses the data at a time. Demonstrate locking with lock().unwrap() to get a RefMut. Emphasize that misuse (like forgetting to lock) is prevented by the type system (you cannot access the data inside without locking).
  • Deadlocks – Briefly warn that while Rust prevents data races at compile time, it cannot prevent logical deadlocks (two threads waiting on each other’s locks). Proper mutex ordering or using higher-level concurrency primitives can mitigate this.
  • Example Exercise: Implement a multi-threaded program where 10 threads each increment a shared counter 1000 times. Use an Arc<Mutex<i32>> for the counter. After spawning all threads, join them, and then print the final counter value (expected 10,000). This exercise will involve using Arc::clone to share the counter and locking the mutex inside each thread to update the count, demonstrating safe shared-state concurrency.

15. Asynchronous Programming (Async/Await)

  • Why Async? – Explain that asynchronous programming allows handling many tasks concurrently on a few threads by interleaving work (useful for I/O-bound tasks). Rust’s approach to async is via futures and the async/await syntax, which is supported in the language.
  • Async Functions – Introduce the async fn syntax, which transforms a function into returning a Future (an object that represents a value that may not be ready yet). For example:
    async fn hello() {
        println!("Hello, async world!");
    }
    This function, when called, returns an impl of the Future trait. It does not execute until polled by an executor.
  • .await – Inside async contexts, you can call .await on futures to suspend execution until they complete. Show an example with a made-up future or a real one like an async timer (if using external crate like tokio::time::sleep). For example:
    async fn delayed_print() {
        println!("Waiting...");
        sleep(Duration::from_secs(1)).await;
        println!("Done!");
    }
    Explain that .await yields control while the future isn’t ready, allowing other tasks to run on the same thread.
  • Futures and Executors – Note that the Rust standard library provides the Future trait but doesn’t include a built-in executor for polling futures to completion. In practice, one uses an async runtime (like Tokio, async-std, etc.) to execute async functions (for example, with #[tokio::main] macro or manually block_on from the futures crate). This is more of a tooling detail, but necessary to actually run async code.
  • Pinning (Advanced) – (Optional) Mention that under the hood, async state machines use the concept of Pin to guarantee memory stability for self-referential structs, but learners can usually ignore this while using async/await at a high level.
  • Concurrency in Async – Clarify that spawning independent tasks (e.g., tokio::spawn) allows futures to run concurrently. Multiple tasks on one OS thread will cooperatively schedule via .await points. Also mention that async and threads can mix – you can have multiple OS threads each running multiple tasks.
  • Example Exercise: Write a simple asynchronous function async fn fetch_data() that pretends to fetch data by, say, sleeping for 2 seconds (you can use async-std::task::sleep or similar). Then write an async fn process_data() that awaits fetch_data() and prints a message when done. Use an executor to run process_data() (for example, with async_std::task::block_on or within a #[tokio::main] context). This exercise helps get familiar with async/await syntax. (If not using an external runtime, you can have students conceptually write the code and explain how it would be called.)

16. Foreign Function Interface (FFI)

  • Calling C from Rust – Explain that Rust can interface with other languages, primarily via the C ABI. Introduce the extern "C" block to declare external functions. For example:
    extern "C" {
        fn abs(input: i32) -> i32;
    }
    unsafe {
        println!("abs(-3) = {}", abs(-3));
    }
    This declares the C abs function from libc, and we call it within an unsafe block (foreign functions are considered unsafe since Rust can’t enforce their contracts) (Calling C function - help - The Rust Programming Language Forum).
  • Rust Functions callable from C – Show how to expose Rust functions to C by using extern "C" on a Rust function and possibly the #[no_mangle] attribute to prevent name mangling:
    #[no_mangle]
    pub extern "C" fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    If this Rust code is compiled into a library, a C program can link to it and call add. Emphasize that using #[no_mangle] means the function name will appear exactly as is in the compiled output, which is required for C to find it.
  • Data Types in FFI – Cover basics of ensuring FFI uses compatible types: libc types or repr(C) on structs. For example, if passing a struct, it should be marked #[repr(C)] to have a C-compatible memory layout. Use simple types (integers, floats, pointers) in FFI to avoid issues. Mention that CString and CStr are used to handle Rust String <-> C string (char*) conversions safely.
  • Safety Considerations – Reinforce that FFI calls are unsafe: Rust can’t guarantee the safety of external code. One must ensure pointers passed to C are valid, that C functions uphold their contracts, etc. Often, wrapping unsafe FFI calls in safe abstractions is best.
  • Other FFIs – Note that Rust can interface with languages beyond C, but often through a C-compatible layer (e.g., calling C++ might be done via extern "C" wrappers or using cxx crate; calling Rust from Python via FFI, etc., is possible but out of scope).
  • Example Exercise: Using the libc functions, call a C standard library function from Rust. For instance, use the C strlen function: declare it with extern "C" { fn strlen(s: *const c_char) -> size_t; } (you’ll need to import std::os::raw::c_char and libc::size_t or similar). Then, in main, create a CString from a Rust string and pass its pointer to strlen, verifying that you get the correct length. This exercise requires using unsafe and dealing with C string pointers, illustrating basic FFI mechanics.

17. Advanced Applications: WebAssembly with Rust

  • What is WebAssembly (WASM)? – Briefly explain WebAssembly as a portable binary instruction format that runs in browsers and other WASM runtimes. Rust is well-suited for WebAssembly because of its performance and lack of runtime GC.
  • Compiling Rust to WASM – Note that Rust can target WASM by using the appropriate compilation target (e.g., wasm32-unknown-unknown). The focus here is not on the build process, but on writing Rust code that can be integrated with JavaScript.
  • WASM and no_std – Mention that WebAssembly programs often run in environments without an OS, so Rust’s standard library might be limited. However, when targeting web via wasm-bindgen, you typically have a subset of std available (no direct file or network I/O unless via JS).
  • Using wasm-bindgen – Introduce the wasm-bindgen tool which bridges Rust and JavaScript. Show an example of exposing a Rust function to JavaScript:
    use wasm_bindgen::prelude::*;
    #[wasm_bindgen]
    extern "C" {
        fn alert(s: &str);
    }
    #[wasm_bindgen]
    pub fn greet(name: &str) {
        alert(&format!("Hello, {}!", name));
    }
    This code imports the JS alert function and exports a Rust greet function to be callable from JS (01.28.2022 - WebAssembly/Working with wasm-bindgen). In JavaScript, after compiling, one could call wasmModule.greet("World") and it would show an alert.
  • WASM Module Integration – Outline that after writing such Rust code, you would use tools like wasm-pack to compile and generate JS bindings, which produces a .wasm binary and JavaScript glue code. The JS code can then import the WebAssembly module. (Avoid going too deep into wasm-pack usage as it’s tooling, but provide a conceptual overview.)
  • Limitations and Considerations – Note that not everything is available in WASM (for example, multithreading in WASM requires WASM threads support and JS workers; certain system calls are absent). But for many applications (like computational tasks, games, etc.), Rust/WASM is very powerful.
  • Example Exercise: Create a small Rust function intended for WebAssembly that adds two numbers and returns the result. Use #[wasm_bindgen] to expose it to JavaScript. For instance, #[wasm_bindgen] pub fn add(a: i32, b: i32) -> i32 { a + b }. Compile this to WebAssembly (using wasm-pack or Cargo with target wasm32) and write a short snippet of JavaScript to call this add function, verifying that it produces the correct result. This exercise reinforces how to expose Rust code to another environment (the browser) and hints at the power of Rust in WebAssembly.

18. Conclusion and Next Steps

  • Summary – This curriculum covered Rust from basic syntax and ownership principles through advanced features like generics, traits, macros, unsafe code, and interfacing with other systems. Each topic built on the previous, emphasizing Rust’s core concepts: safety, ownership, and fearless concurrency.
  • Practice and Projects – Encourage consolidating this knowledge by building something tangible. Ideas: a command-line program (to practice structs, error handling), a simple web server (uses traits, multi-threading), or a small web app in WASM. Practicing by writing code and solving problems is crucial to internalize Rust’s concepts.
  • Further Resources – Recommend authoritative references for continued learning:
    • The Rust Programming Language (the “Rust Book”) – an in-depth resource that mirrors many topics above with examples (What is Ownership? - The Rust Programming Language) (The match Control Flow Construct - The Rust Programming Language).
    • Rust by Example – an online collection of example programs covering Rust concepts in action, useful for seeing more code samples.
    • The Rust Reference – for the exact language rules and more detail on topics like trait objects and memory models (useful as you dive deeper or need clarification on specifics).
    • The Rustonomicon – for mastering unsafe Rust, exploring the dark corners and advanced design patterns to ensure safety when you must use unsafe (19.1 - Unsafe Rust | The rs Book).
    • Rustlings (exercise collection) or the official Rust By Practice to get hands-on experience.
    • Community forums (users.rust-lang.org) and the Rust Discord for asking questions – the Rust community is very welcoming to learners.
  • Continuing Education – Rust is a fast-evolving language (with new features like const generics, async improvements, etc. stabilizing in recent editions). Encourage keeping up with the Rust Release blog posts and trying out new features on the stable/beta channel as one grows in Rust expertise. Each project you build will likely introduce new concepts (like designing APIs, using nightly features, or advanced libraries) that build upon this solid foundation. Happy coding, and welcome to the Rustacean community!

Next steps

Ok let's start. I want to start with lesson 1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment