Test MDX File V11
ThreadLab Demo
Section titled “ThreadLab Demo”Rust Thread Lab
What can cross the thread boundary? Three walls — 'static, Send, and Mutex — and how to cross each.
1 · Declare in main
3 · Spawn the thread
Format:
Rust Ownership Lab
Section titled “Rust Ownership Lab”Rust Ownership Lab
Move, clone, copy — and watch the compiler track who owns the heap.
Click a statement to execute it
String — owned, moved
i32 — copied, never moved
Rust Borrow Lab
Section titled “Rust Borrow Lab”Rust Borrowing Lab
Move ownership, share a reference, or take an exclusive one — and watch the rules play out.
How does the function take its argument?
// Function definition
fn take(s: String) {
s.push_str(", world");
}
// Caller
fn main() {
let s = String::from("hi");
take(s);
// s has been moved — using it here would not compile
}Execution phase
Before callOwnership transfers into the function. After the call, the caller can no longer use `s` — Rust will refuse to compile any code that does.
Borrow rule for this mode
No borrows are involved. Once moved, the binding is gone for the caller. Period.
What main sees after the call
Rust Lifetime Lab
Section titled “Rust Lifetime Lab”Rust Lifetime Lab
See what the compiler sees: which references are valid, for how long, and why a dangling pointer never escapes Rust alive.
Pick a scenario
fn main() { let r = dangle(); println!("{}", r);} fn dangle() -> &String { let s = String::from("hi"); &s}Execution phase
Before callThe local `s` dies when `dangle` returns, but the reference outlives it. Rust spots the lifetime mismatch at compile time and refuses to build the program.
Rust Box Lab
Section titled “Rust Box Lab”Rust Box Lab
Put a value on the heap, write through the box, move ownership — and watch Rust free it for you.
Click a statement to execute it
Stack vs heap — same value, different storage
Through the box — read and write the heap value
Move ownership — Box is not Copy
Rust Slice Lab
Section titled “Rust Slice Lab”Rust Slice Lab
View a range of someone else's allocation. See exactly where slices succeed and where they don't.
Pick a scenario
fn main() { let v = vec![10, 20, 30, 40, 50]; let s = &v[1..4]; println!("{:?}", s); // [20, 30, 40]}Step through execution
1 / 3A slice borrows a contiguous range of someone else's allocation. The 16-byte fat pointer (ptr + length) sits on the stack; the data lives wherever the source put it. Here `s` covers v[1..4] — the elements 20, 30, 40.
Rc Lab
Section titled “Rc Lab”Rust Rc Lab
Share one heap allocation between many owners. Watch the strong count tell you when the data dies.
Click a statement to execute it
Allocate / share — Rc::clone bumps the strong count
Special cases — move, drop, weak
TraitObject Lab
Section titled “TraitObject Lab”Rust Trait Object Lab
A trait object is a fat pointer — and the second half points at a vtable. Step through the dispatch to see how.
Scenario
trait Animal { fn speak(&self) -> &str; fn legs(&self) -> u32;} struct Dog { legs: u32 }struct Cat { legs: u32 }struct Snake { legs: u32 } impl Animal for Dog { fn speak(&self) -> &str { "woof" } fn legs(&self) -> u32 { self.legs }}// (Cat: "meow", Snake: "hiss" omitted for brevity) fn main() { let animal: Box<dyn Animal> = Box::new(Dog { legs: 4 }); println!("{}", animal.speak()); println!("{} legs", animal.legs());}Dispatch trace
At restBinding declared. The fat pointer is wired up: data → heap, vtable → .rodata.
Why a trait object is 16 bytes, not 8
Move Semantics Demo
Section titled “Move Semantics Demo”Move Semantics in Rust
Passing a value by-value to a function **moves** it. Once `s` is moved into `take_ownership`, the binding `s` in `main` is invalidated — using it on the next line is a compile error. Notice this is the *same* mechanism as `let s2 = s;` — function parameters are just bindings, and binding to a non-`Copy` value moves rather than copies.
C++ touchstone: by default C++ would copy `s` into the parameter (calling `std::string`'s copy constructor) and `s` would still be usable. To get Rust's behaviour you'd explicitly write `take_ownership(std::move(s))`, after which the C++ standard says `s` is in a "valid but unspecified state" — usable, but its contents are anyone's guess. Rust just removes the binding from the type system, so there is no zombie state to worry about.
Integrated:
Shadowing Demo
Section titled “Shadowing Demo”Shadowing in Rust
Each let x = … creates a brand-new variable that happens to reuse the name x. The previous x is shadowed, still alive in memory until the new binding takes its place, but no longer reachable by that name. This compiles even though every x is immutable, because we're not mutating anything: we're introducing fresh bindings.
Copy Clone Demo
Section titled “Copy Clone Demo”Copy and Clone in Rust
i32 implements the Copy trait, so let y = x; makes a bit-for-bit copy of the value and both bindings stay valid afterwards. Copy is reserved for types whose whole value lives on the stack with no owned heap data — primitives (i32, f64, bool, char), shared references (&T), and tuples or arrays of Copy types. The duplication is cheap and safe, so Rust just does it for you on assignment.
Borrow Demo
Section titled “Borrow Demo”Immutable & Mutable Borrows in Rust
A shared borrow &s gives read-only access to s without taking ownership. As many &s references can coexist as you like — they all see the same value, none can change it, and s itself stays usable too. Read-only access is safe to share, so Rust doesn't restrict it.
LifetimeDemo
Section titled “LifetimeDemo”Lifetimes in Rust
Every value has a lifetime — the span of code during which it is alive. x is declared inside the inner block, so its lifetime ends at the closing }. But r is declared in the outer scope and is still in use on the println! line. A reference cannot outlive what it points at, so the compiler refuses. This is the classic dangling-pointer bug — caught at compile time, before the program ever runs.
SliceDemo
Section titled “SliceDemo”Array & String Slices in Rust
A slice is a view into a contiguous run of elements — it doesn't own anything, it just borrows. The range 1..4 is half-open: it includes index 1 but stops before index 4, giving you elements at positions 1, 2, and 3. Under the hood s is a 16-byte fat pointer: an address (pointing at arr[1]) plus a length (3). No allocation, no copy — slicing is essentially free.
MutLab Demo
Section titled “MutLab Demo”Rust Mutability Lab
Toggle mut on the binding and on the reference. To mutate through r, you need both.
let x: i32 = 5;
Configure the source
Run a statement
RustMemoryLab Demo
Section titled “RustMemoryLab Demo”Rust Memory Lab
Drag a Rust type onto the stack. Owned types — Box, String, Vec — also claim space on the heap.
Type Palette — drag onto the stack
Declared bindings
| Type | Name | Value | Stack addr | Stack | Heap |
|---|---|---|---|---|---|
| Box<i32> | a | 42 | 0x7FFFFFE0 | 8 B | 4 B |
| String | b | hello | 0x7FFFFFE8 | 24 B | 5 B |
Error Handling Demo
Section titled “Error Handling Demo”Idiomatic Error Handling in Rust
Anything that might be absent in Rust is wrapped in Option<T>: there are only two possibilities, Some(value) or None, and the compiler forces you to handle both. match is the standard way to do it: each arm pulls the value out (if any) and decides what to do. Compare to languages where the same code returns null and you can dereference it without thinking. Rust just doesn't let you reach the value without first acknowledging it might not be there.
Generics Demo
Section titled “Generics Demo”Generics & Trait Bounds in Rust
<T: PartialOrd> declares a type parameter T together with a trait bound - any type used as T must implement PartialOrd, the trait that supplies <, >, <=, and >=. The bound is the contract the compiler needs in order to allow x > biggest inside the body. Without it, the compiler couldn't know whether > is even defined for whatever T turns out to be - see scenario 3 for what happens if you forget.
C++ touchstone: this is a function template - template <typename T> T const& largest(std::vector<T> const& v). Pre-C++20, the constraint was implicit and only surfaced as a wall of template-instantiation errors when > failed for some T. With C++20 concepts (requires std::totally_ordered<T>) you can finally say the constraint up front - exactly what Rust's trait bounds have done since day one.
Traits Demo
Section titled “Traits Demo”Traits & `impl` in Rust
A `trait` declares a set of method signatures any type can opt in to. An `impl Trait for Type` block supplies the bodies. Call the method on a value with the usual `d.hello()` dot-syntax — the compiler resolves it to `Dog`'s implementation at compile time. There's no inheritance involved: `Dog` is a plain struct that just happens to satisfy the `Greet` contract.
C++ touchstone: a Rust trait is conceptually a pure-virtual interface or a C++20 concept. The `impl Greet for Dog` block is the equivalent of `class Dog : public Greet` plus method definitions — but Rust splits the type definition and the interface conformance into separate blocks, so `Dog` doesn't need to know about `Greet` at the point it's defined.
String Types Demo
Section titled “String Types Demo”String vs &str in Rust
Rust has two string types and the difference is ownership, not content. &str is a borrow, a 16-byte fat pointer (data ptr + length) into bytes that live somewhere else; for a literal, those bytes are baked into the program's read-only data segment. String is an owned, growable buffer on the heap, represented on the stack as 24 bytes (ptr + length + capacity). Same characters, same .len(), very different storage and ownership stories.
C++ touchstone: String is std::string, an owning, heap-backed, growable buffer. &str is closest to std::string_view (C++17): a non-owning view of someone else's bytes. The standard library and idiomatic APIs in modern C++ have been moving the same direction Rust started: take a view when you only need to read.
© 2026 Derek Molloy, Dublin City University. All rights reserved.