A mental model, not a syntax tour
Learning Rust.
Ownership is the whole idea.
Rust gives you C-level control and memory safety, with no garbage collector — by making the compiler prove your program is safe before it runs. The price is one big idea you have to internalize: ownership. Get that, and the borrow checker stops fighting you and starts catching your bugs. We build it from first principles, then climb to traits, errors, and fearless concurrency.
Five acts · ~21 stops · drive the borrow checker, scrub a lifetime, watch a move invalidate a value. Real examples cameo from a production Rust codebase (NetWatch).
What you get, and what it costs
Memory safety without a garbage collector
Most languages pick one: manual memory (C/C++ — fast, footguns) or a garbage collector (Go/Java — safe, runtime cost). Rust refuses the trade by moving the bookkeeping to compile time.
- No GC, no pauses. Memory is freed at a deterministic, known point — when its owner goes out of scope.
- No use-after-free, no data races. The compiler rejects them — they're not runtime bugs you hunt, they're build errors.
- "Fearless" is literal. The confidence comes from the compiler, not from your discipline or your test coverage.
The cost is real: you learn to think in ownership, and the compiler says "no" a lot at first. This deck is about making those "no"s legible.
The borrow checker is a theorem prover for "no aliasing bug." Pass it, and a whole class of CVEs is simply impossible in safe Rust.
Three rules, and everything follows
Every value has exactly one owner
The entire system rests on three rules the compiler enforces. Memorize these; the rest of Rust is consequences of them.
- 1. Each value has one owner — a single variable binding responsible for it.
- 2. One owner at a time — assign or pass it, and ownership moves; the old binding is no longer valid.
- 3. Owner drops → value freed — when the owner leaves scope, the value's
Dropruns and its memory is released. Deterministically.
"Borrowing" (next act) is how you let other code use a value without taking ownership — the escape valve that makes rule 2 livable.
Stack values that are cheap to duplicate (i32, bool,
char…) are Copy: assigning them copies. Everything heap-backed
(String, Vec…) moves.
Three ways a value travels
The difference between move, copy, and clone is the difference between fast and safe
Move default for owned
Assigning or passing a heap value transfers ownership — no data is copied, just the pointer. Cheap (a few bytes), but the source binding is invalidated so two owners can never free the same memory.
Copy cheap stack types
Types that are trivially
duplicable (i32, bool, small Copy
structs) are copied on assignment. Both bindings stay valid because each has its own independent value.
Clone explicit deep copy
.clone()
makes an independent deep copy on purpose — you opt into the cost. The source stays valid because you now have
two separate owners of two separate allocations.
Borrow use without owning
Often you don't need to move
or copy — you just want to read or mutate in place. That's a reference
(& / &mut) — the whole of Act II.
The art of idiomatic Rust is largely "borrow by default, move when you must, clone when you mean it." Reflexive cloning to dodge the borrow checker is the classic beginner smell.
Watch what happens to the source binding
After a move, the original is gone — and the compiler knows
The single most common first-week error is use of moved
value. Pick what t does to s and see whether the final
println! compiles.
let s = String::from("netwatch");
A move is just a pointer handoff plus a compile-time note that the source is dead. No runtime cost, no runtime check — the safety is entirely in the type system.
Use a value without taking it
Shared & XOR exclusive &mut
Moving everything everywhere would be miserable. References let you lend access. The one rule that makes them safe — and that the borrow checker enforces:
- Any number of shared
&references (read-only), OR - exactly one exclusive
&mutreference (read-write) — - never both at once. Aliasing XOR mutation. That single invariant is what prevents data races and iterator invalidation.
A reference borrows; it never owns. The borrow must end before the owner is moved or dropped — which is what lifetimes (slide 8) track.
The compiler, simulating your references
Step the checker — then break the rule on purpose
The borrow checker walks your code tracking which references are live at each point. Pick a scenario and step it: watch the live borrows, and watch a real error fire the moment aliasing meets mutation.
borrow check: let mut v = vec![1, 2, 3];
These errors feel like obstacles for a week, then become a sixth sense. The checker is catching the exact bugs that are heisenbugs in C — aliased mutation — at compile time.
A reference may not outlive what it points to
Lifetimes are the spans the checker compares
A lifetime is just the region of code where a reference is valid. The rule is simple: a borrow must not outlive its owner. Scrub the timeline — in the dangling case, watch the owner drop while the reference is still around.
does the borrow outlive the owner?
Most lifetimes are elided — the compiler infers them. You only write 'a
annotations when a function returns a reference and the compiler can't tell which input it borrows from.
A borrowed view into someone else's buffer
Slices are references — so dangling is impossible
A slice (&[T], &str) is a borrowed
window — a pointer and a length — into a Vec or String
it doesn't own. The borrow checker ties the window's lifetime to the buffer's.
- Hold a slice into a
Vec, and you cannot mutate thatVec(e.g.push, which might reallocate) while the slice lives. - That's the same aliasing-XOR-mutation rule preventing the classic C bug: a pointer left dangling by a
realloc.
The bug doesn't need a sanitizer to find at runtime — it doesn't compile.
Make illegal states unrepresentable
Enums carry data; match must handle every case
Rust enums are sum types: each variant can hold its own data. Combined with
exhaustive match, you encode "exactly one of these, with its payload" — and the
compiler won't let you forget a case.
- A variant can be empty, or carry a tuple/struct of data — model the real shape of your domain.
matchis exhaustive: miss a variant and it's a compile error, not a bug that ships.- Matching binds the payload, so you handle the data and the case together.
This is how Option and Result are
built — they're just enums in the standard library.
No exceptions — failure is in the type
Result<T, E> and the ? operator
There are no exceptions to forget to catch. A function that can fail says so in its
return type: Result<T, E> — either Ok(value)
or Err(why).
- The signature is honest: you can't ignore a possible failure, because you have to unwrap the
Resultto get the value. ?is the ergonomic glue: "if this isErr, return it from the whole function; otherwise give me theOkvalue."- Errors propagate up the call stack as values, explicitly — no invisible control flow.
panic! exists for truly unrecoverable bugs — but recoverable
failure is always a Result.
Shared behavior, without inheritance
Traits define what a type can do
A trait is a set of methods a type can implement — Rust's answer to interfaces and the basis of generics. NetWatch's DPI layer is one trait, implemented 15 times.
- Define once, implement per type. Each protocol classifier
impls the sameClassifiertrait. - Static dispatch (
<C: Classifier>) — the compiler generates a specialized copy per type. Zero runtime cost. - Dynamic dispatch (
&dyn Classifier) — one vtable, chosen at runtime. For heterogeneous collections.
Bounds like T: Send + Clone say "any type that can do these" —
generic code that's still fully type-checked.
The billion-dollar mistake, designed out
Absence is a type: Option<T>
Rust has no null. A value that might be missing has type
Option<T> — either Some(v) or
None — and the compiler forces you to handle the None
case before you can touch the value.
- No more accidental null dereferences — the absent case is visible in the type and checked at compile time.
- Rich combinators instead of null checks:
map,unwrap_or,and_then,ok_or. ?works onOptiontoo — propagate "nothing here" cleanly.
The functional core — lazy and zero-cost
Chains that compile down to a tight loop
Iterator adapters (filter,
map, …) are lazy: they build a recipe and do nothing until a consumer
like collect pulls. Each element is dragged through the whole chain, one at a time.
Step it.
(1..=6).filter(|n| n%2==0).map(|n| n*n).collect()
Zero-cost abstraction: this chain optimizes to the same machine code as a hand-written
for loop. Closures (|n| n*n) capture their environment and
cost nothing extra.
When single-owner-on-the-stack isn't enough
Heap, sharing, and interior mutability — opt in by type
Box<T> heap, one owner
Put a
value on the heap with a single owner. For recursive types, large values, or storing a
dyn Trait.
Rc<T> shared, 1 thread
Reference-counted shared ownership within one thread. The value drops when the last
Rc does. Not thread-safe (cheap, non-atomic count).
Arc<T> shared, threads
Atomic Rc — shared ownership across threads. The "A" is the only difference,
and it's what makes it Send.
RefCell<T> runtime borrow check
Interior mutability: mutate through a shared reference, with the borrow rules checked at runtime (panics on violation) instead of compile time.
Arc<Mutex<T>> ← netwatch
The standard "shared mutable state across threads" combo. NetWatch's collectors live behind exactly this — the next slide.
The pattern
Reach for these only when ownership truly needs to be shared or deferred. Most code is plain owned values and borrows — these are the deliberate exceptions.
Data races are a compile error
The ownership rules extend straight to threads
Aliasing-XOR-mutation isn't just for one thread — it's exactly the rule
that prevents data races. The compiler tracks two auto-traits,
Send (safe to move to another thread) and Sync (safe to share),
and refuses unsafe sharing. Toggle the two approaches:
two threads, one piece of data
NetWatch's collectors share state as Arc<Mutex<T>> and stream
events over an mpsc channel — the compiler guarantees no collector thread can race the
UI thread. That guarantee is free at runtime.
Cleanup is tied to ownership
When the owner goes, the cleanup runs — deterministically
There's no finally, no defer, no
garbage-collector finalizer-someday. A value's Drop runs the instant its
owner leaves scope — including on early return or panic.
- RAII everywhere. A file closes, a lock unlocks, a socket detaches — because the guard owning it was dropped.
- You rarely write
Dropby hand; you compose types that already clean up after themselves. - Deterministic, not eventual. You know exactly when resources are released — the closing brace.
This is the same mechanism behind MutexGuard unlocking and
NetWatch's eBPF handle detaching the kprobe.
The reason day-to-day Rust is pleasant
One tool, batteries included
cargo is build system, package manager, test runner, and doc
generator in one. The cohesion is a feature: every project works the same way, and the ecosystem composes.
crates.io— one registry;cargo addand a version is wired in, dependencies resolved.clippy— 600+ lints that catch real bugs and teach idioms as you go.rustfmt— one canonical style; formatting debates simply don't happen.rust-analyzer— a world-class language server: types, completions, and the borrow checker's reasoning live in your editor.
Tests and docs are first-class: cargo test runs unit, integration,
and even the examples inside your doc comments.
The escape hatch — and what it does NOT turn off
unsafe is a promise, not a free-for-all
Sometimes you must do what the compiler can't verify — talk to hardware, FFI, or read raw
kernel memory. unsafe unlocks exactly five extra powers, and nothing
else.
- It lets you dereference raw pointers, call
unsafefns, and a few more — that's the whole list. - It does not disable the borrow checker, type checking, or ownership for everything else.
- It's a promise: "I've upheld the invariants the compiler can't see." Your job is to wrap it in a sound safe API.
Good Rust isolates unsafe to tiny, audited blocks behind safe
interfaces — so the unsafety is contained, not spread.
The right tool, and how to keep learning
Where Rust earns its learning curve
Rust shines where correctness and control both matter — and the ownership tax pays for itself.
- Systems & infra — networking, databases, OS components, embedded. Predictable performance, no GC pauses.
- Anything concurrent — the compiler eliminates data races, so aggressive parallelism is safe.
- Long-lived, high-stakes code — the type system encodes invariants that survive refactors and new maintainers.
- CLI / TUI tools — fast startup, single static binary, great libraries (exactly NetWatch's niche).
Reach for a GC'd language when iteration speed beats control and a few ms of pause don't matter. Rust is a deliberate trade, not a default.
Keep learning
The Book
(doc.rust-lang.org/book) — the canonical intro. Rustlings — fix-the-code
exercises. Rust by Example — runnable snippets. std docs — superb and searchable.
The way in is to write it
Port a small tool you already understand. Let the
compiler teach you ownership through its errors — they're unusually good. Lean on
clippy and rust-analyzer as live tutors.
The whole map, once more
It all comes back to ownership
The bargain Act I
Safety without a GC, proven at compile time. One value, one owner; move, copy, or clone — borrow by default.
Ownership Act II
Shared & XOR
exclusive &mut; the borrow checker enforces it; lifetimes ensure a borrow never
outlives its owner.
Types that work Act III
Enums + exhaustive
match; errors as Result with ?;
traits for shared behavior; Option instead of null.
Real work Act IV
Lazy zero-cost iterators; smart pointers
for sharing; data races as compile errors; deterministic Drop.
Ecosystem Act V
cargo + clippy + rust-analyzer; and
unsafe as a small, audited promise — not a disabling of the rules.
The throughline
Every feature is a consequence of ownership. Once it clicks, the borrow checker is a collaborator, not an obstacle — it's catching your bugs at compile time.
Press O for the full map · ← to revisit any simulation. The fight with the borrow checker is the learning — and it ends.