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.
- 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 thefn 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 anotherprintln!. - Comments – Use
//for single-line comments and/* ... */for block comments. Emphasize the importance of commenting code for readability. - Variables and Mutability – Introduce the
letkeyword for binding variables. Explain immutable by default semantics, andmutfor mutable bindings (Primitives - Rust By Example) (Primitives - Rust By Example). Demonstrate constants withconst(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 (
i32default, plusu8,i64, etc.), floating-point (f64default), 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.
if/elseConditions – Demonstrate conditional execution. Note that conditions must be boolean (if x {}requiresxto bebool, not truthy/falsy) (The match Control Flow Construct - The Rust Programming Language). Show optionalelse ifchains for multiple conditions.- Loops – Explain Rust’s looping constructs:
loop(infinite loop, usebreakto exit),while(conditional loop), andforloops. Show looping over a range (e.g.,for i in 0..5 {}) and iterating over collections withfor(introduce the concept of an iterator yielding items). matchExpressions – Introducematchas 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
letbindings (e.g.,let (a, b) = tuple;). Introduce the_wildcard to ignore values. - Control Flow Keywords – Explain
breakto exit loops andcontinueto skip to the next iteration. Show usingbreakwith 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
forloop andif/elseconditions to determine what to print for each number.
- 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
Copytrait 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
Stringto a function makes the caller lose it unless returned). Show borrowing by having a function take&Stringto calculate its length without taking ownership. - Example Exercise: Write a function
calculate_lengththat takes a string slice&stras input and returns its length. Inmain, create aString, 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).
- Defining Structs – Introduce the
structkeyword for custom data types. Show a simple struct with named fields, e.g.:Explain how structs group related data and how to instantiate them (usingstruct Person { name: String, age: u8, }
{ 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
implblock to define methods for a struct. Show how to declare a method with&selfor&mut selfreceiver, 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 likenew()as a constructor (which don’t have aselfparameter). - Debug Formatting – Mention that using
println!("{:?}", instance)requires deriving theDebugtrait 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 withlet Person { name, age } = person;to break it into parts. - Example Exercise: Define a struct
Rectanglewith width and height fields. Implement anarea(&self) -> u32method forRectanglethat calculates the area. Inmain, create aRectangleinstance and callarea()to print the result. (Bonus: add a functioncan_hold(&self, other: &Rectangle) -> boolthat checks if one rectangle can contain another, to practice method logic.)*
- Defining Enums – Use the
enumkeyword to define enumerations that can represent one of several variants. For example: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 Direction { North, South, East, West }
This shows variants with no data, named fields, tuple data, etc.enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(u8, u8, u8), }
- Using Enums – Demonstrate creating enum values (e.g.
let m = Message::Write(String::from("hello"));). Introduce the concept of theOption<T>enum from the standard library, which encodes an optional value (NoneorSome(value)). Show howOptionis used instead of nulls for safety. Likewise, introduceResult<T, E>for error handling (withOkandErrvariants). - Pattern Matching with
match– Leveragematchto handle enums. Show amatchon anOption<i32>: one arm forSome(i)and one forNone. 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 letandwhile 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 letto 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 functionvalue_in_cents(coin: Coin) -> u8that uses amatchexpression 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.
- 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
ResultEnum – CoverResult<T,E>which is defined asenum Result<T,E> { Ok(T), Err(E) }. It is used for operations that might fail. For instance,std::fs::File::open("foo.txt")returnsResult<File, std::io::Error>. To handle it, one can usematchor the?operator. Note thatResultandOptionare 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 aResult(orOption), and it will either unwrap theOkvalue or return theErrearly from the function (propagating the error upwards). Show how to use?in a function that returnsResultto 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 usingstr::parse::<u32>() -> Result<u32, ParseIntError>. Show handling theResultwith amatchto 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. Usestd::fs::File::openandstd::io::read_to_string(orstd::fs::read_to_string) which returnResulttypes, and use the?operator to propagate errors. Inmain, call this function and handle the error by printing a message (without usingunwrap). This exercise practices usingResultand?for error propagation.
- Modules (
mod) – Explain that Rust uses a module system to organize code. Amoduleis a namespace that can contain functions, structs, enums, etc. By default, every file is a module, and you can declare sub-modules withmod name { ... }or in separate files. Demonstrate creating a module in a file or withinmain.rs. - Privacy and
pub– By default, items in a module are private (accessible only inside the parent module). Use thepubkeyword 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
usekeyword. For instance, if moduleutilscontains a functionpub fn greet(), one canuse crate::utils::greet;to callgreet()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
crateis the root module of a binary or library.) Mention thatextern crateis implicitly handled by Cargo for external dependencies. - Example Project Structure – Outline a simple project with modules: e.g., a
main.rsthat uses alibrarymodule defined inlib.rsor separate files. Illustrate how splitting code into modules improves organization (for instance, anetworkmodule and amodelmodule in a larger project). - Example Exercise: Create a module
mathcontaining two functionsadd(a,b)andmultiply(a,b). In yourmainfunction (in a separate module/file), usemath::addandmath::multiplyto perform some calculations, printing the results. This exercise will require marking the functions aspuband using the module’s path to call them, reinforcing module syntax and visibility.
- Vectors (
Vec<T>) – Introduce theVec<T>type from Rust’s standard library as a growable array (heap-allocated). Show how to create a new vector (Vec::new()or thevec![]macro), add elements withpush, access elements by index or with methods likelen(). Demonstrate iterating over a vector with aforloop. 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 ownedStringvs string slice&str. Explain thatStringis a vector of bytes (UTF-8) that owns its contents, while&stris a borrowed string slice (often a view into aStringor string literal). Show common operations: creating strings (String::fromor.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 awhile let Some(x) = iter.next() { ... }loop). More commonly, show iterator adapters and consumers: e.g., using.mapand.filterchains on an iterator and then collecting results. - The
IteratorTrait – Mention that iterators in Rust implement theIteratortrait which requires anext()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 implementingIteratortrait (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.
- 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 usereturnkeyword). - 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:Discuss how closures are often used with iterator methods likelet add_one = |x: i32| x + 1; println!("{}", add_one(5)); // prints 6
.mapor 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 themovekeyword 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 closurefonxtwo times. This demonstrates using a generic type boundF: Fn(i32) -> i32for the parameter. - Examples in the Standard Library – Point out that many std APIs use closures, e.g.,
std::thread::spawntakes 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 closuref: Fn(i32) -> i32and returns the result of applyingfto the number 3. Test this by callingapply_to_threewith 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.
- 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 functionfn largest<T: PartialOrd>(list: &[T]) -> &Tthat returns the largest element in a slice of any orderable type. Or aPoint<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, demonstratestruct Pair<T, U> { x: T, y: U }and creating instances likePair { 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
Tin a way that requires it to have certain behavior (methods or operators), we use trait bounds. For instance,T: CopyorT: Debug. Introduce the syntax with thewhereclause if clarity is needed. Example: implementing the genericlargestfunction requiresT: PartialOrdto compare elements, and perhapsT: Copyor 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 strmeaning the returned reference is tied to the lifetime of the input references. Indicate that'ais 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
largestthat returns the largest element in a slice of any type that implements thePartialOrdtrait. Test it with a slice of integers and a slice of floating-point numbers. Then, implement a functionlongest<'a>(x: &'a str, y: &'a str) -> &'a strthat 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.
- 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
Printablewith a methodfn print(&self);. - Implementing Traits – Show how to implement a trait for a type using
impl Trait for Type. Example:Now anytrait Printable { fn print(&self); } struct Point { x: i32, y: i32 } impl Printable for Point { fn print(&self) { println!("Point({}, {})", self.x, self.y); } }
Pointhas theprint()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 thewheresyntax 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 likeSendandSync(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 implementsPrintable. 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: usingdyn Traitfor trait object types, and that trait objects must be used behind pointers like&dyn TraitorBox<dyn Trait>because they have an unknown size (dynamically sized). Note that not all traits are object-safe (traits with generic methods orSelfreturning 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
Drawand typesButton,TextFieldimplementingDraw. We want a list ofBox<dyn Draw>to hold different drawable components and calldraw()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
Animalwith a methodspeak(&self) -> String. ImplementAnimalfor two structs, e.g.DogandCat, each returning an appropriate noise (“Woof”/“Meow”). Then, create a vector ofBox<dyn Animal>containing both aDogand aCat, and iterate over it to call each animal’sspeakmethod and print the sound. This exercise demonstrates trait implementation and using trait objects for dynamic dispatch.
- 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!) – Introducemacro_rules!macros, which use pattern matching on tokens. For example, show a simple macro:This macro takes any number of comma-separated expressions and creates a vector (similar to the built-inmacro_rules! my_vec { ($($x:expr),*) => { { let mut v = Vec::new(); $( v.push($x); )* v } }; } let v = my_vec![1, 2, 3];
vec!macro). Explain the components:$()for repetition,exprfragment 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 theproc_macroAPI 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 calledrepeat_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 to2 + 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 traitHelloMacrowith a functionhello()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.
- 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
unsafefunctions or methods, 3) Accessing or modifying mutable static variables, 4) Implementingunsafetraits, 5) Accessing fields ofuniontypes. Emphasize that using unsafe means the programmer is responsible for upholding invariants the compiler normally checks. - Unsafe Blocks – Show how to use the
unsafekeyword to create an unsafe block:Point out that inside thelet mut x = 5; let r = &x as *const i32; // raw pointer unsafe { println!("{}", *r); // dereferencing raw pointer (unsafe) }
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 Tand*mut Ttypes (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 fnmeaning the caller must ensure certain conditions. For example, anunsafe fnmight require that a pointer argument is valid. Likewise,unsafe traitindicates 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 functionunsafe fn get_element(arr: *const i32, index: usize) -> i32that 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.)
- Threads in Rust – Introduce
std::thread::spawnfor creating new threads. Each thread runs a closure. Example:and useuse std::thread; thread::spawn(|| { println!("Hello from a thread!"); });
thread::sleeporjointo 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
Sendtrait. Types that areSendcan be transferred to another thread. Similarly, theSynctrait 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,Rcis notSend(non thread-safe ref counting), butArcisSendandSync. Cite that: “A type isSendif it is safe to send it to another thread. A type isSyncif it is safe to share between threads (immutable reference isSend)” (Send and Sync - - MIT). - Message Passing – Introduce channels (
std::sync::mpsc) as a way for threads to communicate safely. Show creating a channel withmpsc::channel(), and usingtx.send(val)in one thread andrx.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 byArc<Mutex<i32>>. Explain howArcprovides shared ownership across threads, andMutexprovides interior mutability with locking to ensure only one thread accesses the data at a time. Demonstrate locking withlock().unwrap()to get aRefMut. 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 usingArc::cloneto share the counter and locking the mutex inside each thread to update the count, demonstrating safe shared-state concurrency.
- 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/awaitsyntax, which is supported in the language. - Async Functions – Introduce the
async fnsyntax, which transforms a function into returning aFuture(an object that represents a value that may not be ready yet). For example:This function, when called, returns an impl of theasync fn hello() { println!("Hello, async world!"); }
Futuretrait. It does not execute until polled by an executor. .await– Insideasynccontexts, you can call.awaiton 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 liketokio::time::sleep). For example:Explain thatasync fn delayed_print() { println!("Waiting..."); sleep(Duration::from_secs(1)).await; println!("Done!"); }
.awaityields 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 manuallyblock_onfrom thefuturescrate). 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
Pinto 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 useasync-std::task::sleepor similar). Then write anasync fn process_data()that awaitsfetch_data()and prints a message when done. Use an executor to runprocess_data()(for example, withasync_std::task::block_onor 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.)
- 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:This declares the Cextern "C" { fn abs(input: i32) -> i32; } unsafe { println!("abs(-3) = {}", abs(-3)); }
absfunction 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:If this Rust code is compiled into a library, a C program can link to it and call#[no_mangle] pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }
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:
libctypes 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 thatCStringandCStrare used to handle RustString<-> 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
cxxcrate; 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
strlenfunction: declare it withextern "C" { fn strlen(s: *const c_char) -> size_t; }(you’ll need to importstd::os::raw::c_charandlibc::size_tor similar). Then, inmain, create aCStringfrom a Rust string and pass its pointer tostrlen, verifying that you get the correct length. This exercise requires usingunsafeand dealing with C string pointers, illustrating basic FFI mechanics.
- 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 viawasm-bindgen, you typically have a subset of std available (no direct file or network I/O unless via JS). - Using
wasm-bindgen– Introduce thewasm-bindgentool which bridges Rust and JavaScript. Show an example of exposing a Rust function to JavaScript:This code imports the JSuse wasm_bindgen::prelude::*; #[wasm_bindgen] extern "C" { fn alert(s: &str); } #[wasm_bindgen] pub fn greet(name: &str) { alert(&format!("Hello, {}!", name)); }
alertfunction and exports a Rustgreetfunction to be callable from JS (01.28.2022 - WebAssembly/Working with wasm-bindgen). In JavaScript, after compiling, one could callwasmModule.greet("World")and it would show an alert. - WASM Module Integration – Outline that after writing such Rust code, you would use tools like
wasm-packto compile and generate JS bindings, which produces a.wasmbinary and JavaScript glue code. The JS code can then import the WebAssembly module. (Avoid going too deep intowasm-packusage 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 (usingwasm-packor Cargo with target wasm32) and write a short snippet of JavaScript to call thisaddfunction, 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.
- 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!
Ok let's start. I want to start with lesson 1