Module 1: Why Rust Exists

Rust What and Why

Learning objectives

  • Explain the core mental model behind Rust What and Why
  • Apply Rust What and Why within Why Rust Exists
  • Identify important boundaries, trade-offs, and failure modes
  • Produce concrete evidence from the practice exercise

Related: Rust Ownership | Rust Stack Heap and Memory Layout | Rust Setup and Cargo | Go What and Why | Rust Index


The Problem Rust Was Built To Solve

Before Rust, systems programmers had exactly two options, and both of them were bad in different ways.

Option 1: manual memory management (C, C++). You get full control over layout, allocation, and lifetime. You also get use-after-free, double-free, buffer overruns, data races, and iterator invalidation. Microsoft and Chrome have both published the same number independently: roughly 70% of their security CVEs are memory-safety bugs. Not logic bugs. Not crypto bugs. Memory bugs.

Option 2: garbage collection (Java, Go, Python, C#). The GC eliminates use-after-free and double-free by refusing to let you free anything. In exchange you accept a runtime, a heap you do not control, and pause times you cannot fully predict. For a web service that is a fine trade. For a memory-constrained microcontroller, a kernel module, or a DPI-C library called from inside a simulation loop, it is not a trade you are allowed to make.

Rust's thesis is that this was always a false dichotomy. The reason C is unsafe is not that it lacks a GC — it is that C has no idea who owns a piece of memory or how long a pointer is allowed to live. Those facts exist in the programmer's head and in comments. Rust's insight: encode those facts in the type system, check them at compile time, and emit exactly the same machine code you would have written by hand.

        SAFETY                          CONTROL
   ─────────────────                ─────────────────

   C / C++          ░░░░░░░░░░      ████████████████   full control, no safety net
   Java / Go / C#   ████████████    ░░░░░░░░           safe, but a GC owns your heap
   Python           ████████████    ░░░                safe, and control is not the point

   Rust             ████████████    ████████████████   both — paid for at compile time
                                                       ▲
                                                       │
                                          the borrow checker is the bill

Verification analogy: this is the same move as replacing a bank of assert statements that fire at runtime with formal property checking that proves the property before you ever run a simulation. Same guarantee, moved earlier, and it costs you nothing at runtime — but you now have to write your design in a way the prover can actually reason about. That last clause is the entire Rust learning curve.


The Core Idea: Ownership as a Compile-Time Arbiter

Rust's central rule is almost embarrassingly small:

Every value has exactly one owner. When the owner goes out of scope, the value is dropped. You may have either one mutable reference or any number of shared references — never both at once.

That second sentence is called aliasing XOR mutability, and it is the whole game.

Hardware analogy that actually holds: think of an AXI or AHB bus with a single arbiter. Many masters may read a slave region concurrently — that is safe, nobody's view changes under them. But a write requires exclusive grant. If the arbiter ever handed out a write grant while a read burst was in flight to the same address, you would get a non-deterministic read — the classic data race. Rust's borrow checker is that arbiter, except it runs at compile time and refuses to elaborate the design if the grants can ever overlap.

Where the analogy breaks down: a bus arbiter serializes conflicting requests at runtime, so the design still works, just slower. Rust does not serialize anything. It rejects the program. There is no runtime arbitration and no runtime cost — which is precisely why it has to be so strict.


What "Zero-Cost Abstraction" Actually Means

Rust inherits this phrase from C++ (Stroustrup: what you don't use, you don't pay for; what you do use, you couldn't hand-code better). Rust takes it further because it has more compile-time information.

// High-level, iterator-chained, closure-heavy:
fn sum_of_even_squares(v: &[u32]) -> u32 {
    v.iter().filter(|&&x| x % 2 == 0).map(|&x| x * x).sum()
}

// Compiles to essentially this, with no allocation, no vtable, no bounds check
// in the hot loop, and typically auto-vectorized:
fn sum_of_even_squares_manual(v: &[u32]) -> u32 {
    let mut total = 0;
    let mut i = 0;
    while i < v.len() {
        let x = v[i];
        if x % 2 == 0 { total += x * x; }
        i += 1;
    }
    total
}

The iterator version is not "compiled down to a loop" by luck. filter and map are generic structs with an inlinable next(); monomorphization stamps out a concrete type per call site, inlining collapses the chain, and LLVM sees a plain loop. See Rust Generics and Monomorphization for the mechanism and Rust Performance and Zero Cost Abstractions for how to verify it on your own code.

Contrast with Python: in Python Comprehensions and Functional Tools, filter and map produce real heap objects and every element goes through a dynamic dispatch on __next__. The abstraction is genuinely costly. In Rust it genuinely is not — but you pay by having the types be fully known at compile time, which is why Rust has no duck typing.


Rust vs C++

They occupy the same performance niche, so the comparison is the sharpest one.

DimensionC++Rust
Memory safetyOpt-in, by discipline and smart pointersGuaranteed by default; opt out with unsafe
Default moveCopy (unless you write move ctors)Move (unless the type is Copy)
Uninitialized valuesLegal and commonImpossible in safe code
Data racesUB, detected at runtime if you are luckyRejected at compile time via Send/Sync
GenericsTemplates — duck-typed, errors at instantiationTraits — bounded, errors at definition
Build/depsCMake + vcpkg/Conan + header archaeologycargo build, one manifest
Error handlingExceptions and/or error codes, bothResult<T, E> in the type signature

The generics difference is underrated. A C++ template error tells you that something 40 frames deep failed to instantiate. A Rust trait bound error tells you, at the definition site, that T needs T: Display — because the bound is part of the contract, not discovered by substitution. This is exactly the difference between a SystemVerilog interface class (a declared contract) and hoping a parameterized class happens to be instantiated with a type that has the right methods.


Rust vs Go

These are frequently compared and rarely competitors. They solve different problems.

          Rust                                  Go
   ────────────────────────           ────────────────────────
   no runtime, no GC                  small runtime, concurrent GC
   safety proven at compile time      safety provided at run time
   complexity is in the language      complexity is in your program
   ~hours to first compile success    ~minutes to first program
   errors: Result<T, E> in the type   errors: (T, error) by convention
   concurrency: Send + Sync, checked  concurrency: goroutines, race at runtime

   pick Rust when:                    pick Go when:
     unpredictable pauses are fatal     network service, many I/O-bound tasks
     you have no OS / no allocator      team turnover is high
     the binary must be small           you need it shipping this quarter
     a data race would be a CVE         GC pauses are irrelevant

Go decided that the programmer's time is the scarce resource and that a GC plus a runtime race detector is an acceptable price. Rust decided that runtime failure is the unacceptable outcome and that programmer time spent satisfying the compiler is a worthwhile investment. Both are defensible. See Go What and Why for the other side of this argument stated properly, and Rust Concurrency and Send Sync versus Go Memory Model for how differently the two treat shared state.


Rust vs Python

Not competitors at all — which is why they compose so well. The most common real-world pattern is Python for orchestration and Rust for the inner loop (pyo3, maturin); this is what ruff, polars, pydantic-core, and uv all are.

PythonRust
TypingDynamic, duck (Python Data Model)Static, trait-bounded (Rust Traits)
MemoryRefcount + cycle GC (Python Memory Model and Copying)Ownership, deterministic drop (Rust Ownership)
ConcurrencyGIL-bound threads (Python Concurrency)True parallelism, statically checked
ErrorsExceptions (Python Exceptions and Debugging)Result, no hidden control flow
IterationGenerators (Python Iterators and Generators)Zero-cost iterator adapters
Feedback loopInstantCompile, then instant

The mental shift that matters: in Python, an object's lifetime is nobody's business — the refcount handles it. In Rust, an object's lifetime is part of its type. That is the single biggest adjustment coming from Python, and it is why Rust Lifetimes feels alien at first.


What Rust Actually Costs You

Being honest about this matters more than the marketing.

  1. The learning cliff is real and front-loaded. Expect one to three weeks of fighting
  2. the borrow checker before ownership becomes intuition rather than obstruction. Nothing in Python, Java, or SystemVerilog prepares you for it.

  3. Compile times. Monomorphization means the compiler stamps out a copy of every
  4. generic per concrete type. A large project with heavy generics compiles slowly. This is the direct cost of the zero-cost abstraction.

  5. Some data structures are genuinely hard. Doubly linked lists, graphs with back-edges,
  6. and observer patterns all involve shared mutable aliasing — exactly what the language is designed to forbid. You end up in Rc<RefCell<T>> (Rust Interior Mutability) or arena/index-based designs.

  7. Async is a second language. async fn, Pin, Send bounds across await points,
  8. and executor choice constitute a substantial sub-dialect. See Rust Async and Futures.

  9. Ecosystem depth is uneven. Excellent for CLI tools, parsers, embedded, networking,
  10. WASM. Thinner for scientific computing, GUI, and anything where Python owns the field.

Rule of thumb: if you would have reached for C or C++, reach for Rust. If you would have reached for Python, Rust is probably the wrong tool unless the inner loop is the bottleneck.


Where Rust Fits in Hardware and Verification Work

This is not hypothetical — it shows up in a few concrete places.

  • DPI-C replacement. A DPI shim is C code linked into the simulator's
  • address space. A segfault there takes down a 12-hour regression. Rust compiles to a C ABI (extern "C", #[no_mangle]) and gives you a memory-safe implementation with identical calling convention and zero runtime.

  • Firmware and embedded. #![no_std] Rust targets Cortex-M and RISC-V directly. The
  • embedded-hal ecosystem plus type-state register access means a peripheral misconfiguration becomes a compile error rather than a silicon bring-up mystery.

  • Post-processing and EDA tooling. Parsing multi-gigabyte VCD/FSDB dumps, coverage
  • merging, log triage. These are exactly the workloads where Python is 50x too slow and the logic is too fiddly for awk.

  • Emulation and prototyping hosts. Anything sitting in the datapath between a
  • emulator and a host where throughput and predictable latency matter.


Examples

Example 1: The bug that cannot be written

fn main() {
    let reference;
    {
        let value = 42;
        reference = &value;   // borrow `value`
    }                         // `value` dropped here
    println!("{}", reference); // ← use after free
}
error[E0597]: `value` does not live long enough
 --> src/main.rs:5:21
  |
5 |         reference = &value;
  |                     ^^^^^^ borrowed value does not live long enough
6 |     }
  |     - `value` dropped here while still borrowed
7 |     println!("{}", reference);
  |                    --------- borrow later used here

The exact same code in C compiles cleanly, usually appears to work, and fails in production six months later on a different compiler version. The Rust version cannot be built. Note that the compiler names the drop point and the later use — it is reconstructing the lifetime argument for you. See Rust Lifetimes.

Example 2: The data race that cannot be written

use std::thread;

fn main() {
    let mut counter = 0;
    let handle = thread::spawn(|| {
        counter += 1;      // ← trying to mutate a stack local from another thread
    });
    counter += 1;
    handle.join().unwrap();
}
error[E0373]: closure may outlive the current function, but it borrows `counter`
error[E0499]: cannot borrow `counter` as mutable more than once at a time

Two independent rules fire: the closure might outlive main's frame, and two mutable borrows would exist simultaneously. The fix names the sharing strategy explicitly:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let c2 = Arc::clone(&counter);

    let handle = thread::spawn(move || {
        *c2.lock().unwrap() += 1;
    });

    *counter.lock().unwrap() += 1;
    handle.join().unwrap();

    println!("{}", *counter.lock().unwrap()); // always 2
}

The type now says exactly what is happening: Arc = shared ownership across threads, Mutex = exclusive access at runtime. In C++ or Go, the sharing strategy lives in a comment. See Rust Concurrency and Send Sync and contrast with Go Memory Model, where the equivalent race compiles fine and is only caught if -race happens to observe it.

Example 3: Errors that cannot be ignored

use std::fs;

fn read_config(path: &str) -> Result<String, std::io::Error> {
    let contents = fs::read_to_string(path)?;   // `?` propagates on Err
    Ok(contents.trim().to_string())
}

fn main() {
    match read_config("sim.cfg") {
        Ok(cfg) => println!("config: {cfg}"),
        Err(e)  => eprintln!("could not read config: {e}"),
    }
}

fs::read_to_string returns Result. You cannot get at the String without acknowledging the Err arm — there is no way to "forget" to check. Compare to Go, where contents, _ := os.ReadFile(path) silently discards the error and compiles (Go Errors and Wrapping), or C, where you ignore the return value by doing nothing at all. See Rust Error Handling.


What Trips People Up

  • "The compiler is fighting me." It is not adversarial; it is reporting that your
  • program's ownership story is ambiguous. Nine times out of ten the fix is not a workaround — it is that the design genuinely had two owners for one piece of data.

  • Reaching for clone() to silence errors. It works, and it is the correct escape valve
  • while learning. But a .clone() you cannot justify is a design smell, not a fix.

  • Expecting OOP inheritance. There is none. Rust composes with traits
  • (Rust Traits); the closest analogue to a virtual class hierarchy is a trait object (Rust Trait Objects and Dynamic Dispatch), and it is deliberately less powerful.

  • Trying to build a linked list on day two. It is the classic trap: the simplest data
  • structure in C is one of the hardest in Rust, for reasons that only make sense once ownership has clicked. Do it in week four, not week one.

  • Believing unsafe means "the safety is off". unsafe disables exactly five extra
  • abilities (raw pointer deref, static mut access, union field reads, calling unsafe fns, implementing unsafe traits). Borrow checking still runs. See Rust Unsafe Rust.


Related

  • Rust Setup and Cargo — installing the toolchain and the first working project
  • Rust Ownership — the single rule everything else is built on
  • Rust Stack Heap and Memory Layout — what is actually stored where
  • Rust Basic Course — ordered learning path from zero
  • Go What and Why — the opposing design philosophy, stated fairly
  • Python What and Why — the language Rust most often gets embedded into
  • Rust Index — full topic map

Practice lab

Build the smallest runnable Rust program that demonstrates Rust What and Why. Add a focused test or Clippy check, trigger one relevant compiler or runtime failure deliberately, and explain the evidence used to correct it.

Review questions

  1. What problem does Rust What and Why solve, and what assumptions does it rely on?
  2. Which boundary or failure case is easiest to miss, and how would you expose it?
  3. What alternative design would you consider, and what trade-off would change the decision?
  4. What artifact, trace, test, or metric proves that your implementation is correct?

Completion evidence

  • A working artifact, annotated trace, or reproducible experiment
  • At least one normal case and one deliberately failing or boundary case
  • A concise explanation of the design choice and its trade-offs
  • Saved output showing how correctness was evaluated