Goodnight Wiki / Rust in Practice

Rust in Practice

Rust's reputation is built on two pillars: memory safety without garbage collection, and fearless concurrency. Both are real. But what's it actually like to write 100,000 lines of Rust for a real system? The answer, based on several practitioners who've done exactly that, is "better than expected at refactoring, worse than expected at everything else."

The Refactoring Superpower

Bunnie huang's retrospective on building the Xous microkernel OS -- a from-scratch RTOS for the Precursor security device -- captures something that other Rust advocates often undersell. The real value of Rust's type system isn't preventing memory bugs (though it does that). It's that when you need to refactor, the compiler tells you exactly what broke.1

His cable analogy is apt: in Rust, every wire in a cable chase, no matter how tangled, is separable and clearly labeled on both ends. Change a type in a struct and the compiler shows you every site that needs updating. In C or Python, you're "buzzing out" cables with manual tests, never quite sure you found everything.

The concrete example: Xous had a trust-level variable stored in two places (Canvas and Context) because the code evolved organically. This caused intermittent bugs that manifested as off-by-one errors and race conditions. In C, this would mean a risky rewrite. In Rust, clippy flagged an unused variable, bunnie pulled the thread, and in a few hours had a clean refactoring that eliminated the duplicate state. The compiler enforced that the change was complete.1

This is Rust's real pitch for systems programming: not "you won't have bugs" but "when you need to fix deep structural problems, the type system has your back."

The Syntax Tax

The less charitable view: Rust syntax is dense to the point of line noise. Bunnie's example is iconic:

Trying::to_read::<&'a heavy>(syntax, |like| { this.can_be(maddening) }).map(|_| ())?;

Type annotations, lifetime parameters, closures, method chaining, the ? operator, turbofish syntax -- all layered on top of each other. Then add macros and conditional compilation directives that don't follow Rust syntax rules at all. The learning curve is steep, and the documentation tends to be either ELI5 or formal specification, with nothing in between.1

This isn't a fundamental criticism -- you get used to it. But it does mean Rust selects for programmers who are willing to invest significant upfront learning time, which limits adoption in contexts where that investment doesn't pay off.

The Complexity Shell Game

Rust's standard library is rich and addictive. HashMap, Vec, Arc, Mutex -- coming from C, these feel luxurious. But bunnie makes a sharp point: using std doesn't make your system simpler, it just sweeps complexity under the rug. The collections module alone is 10k+ lines of high-complexity code. For an auditable-by-one-human security kernel, your attack surface includes all of std, which dwarfs the kernel itself.1

Worse, std is tied to specific Rust versions because it's where the unsafe "sausage gets made" -- turning raw hardware operations like memory allocation and thread creation into safe Rust abstractions. This means Xous has to track the six-week Rust release train, updating their forked std each cycle. The aspiration of "engineering yourself out of a job" -- making the system stable enough to stop touching -- is undercut by the language's relentless forward motion.

Move Semantics and Self-Reference

Armin Ronacher identifies what I think is Rust's most underappreciated design constraint: objects move. In C++, a constructor receives a this pointer to pre-allocated memory, so the object knows where it lives. In Rust, constructors return values by move, and the caller decides placement. This means self-referential structures are fundamentally incompatible with Rust's ownership model.2

The practical consequence is that Rust code uses handles instead of pointers -- stored offsets rather than absolute addresses, recomputed on demand. This pattern pervades memory-mapped data structures, tree implementations, anything where a node needs to reference itself or its siblings. It works, but it's a fundamentally different way of thinking about data layout.

Reference counting (Rc and Arc) fills the gap where ownership is genuinely shared. Ronacher's advice: don't think of refcounting as dirty or a failure of design. Combined with interior mutability (RwLock, RefCell), it's the standard way to handle patterns that don't fit the single-owner model. The pattern RwLock<Arc<Config>> -- where you swap entire immutable configs atomically rather than mutating individual fields -- is idiomatic Rust, and it's more naturally parallelizable than the setter-heavy style of object-oriented code.2

The Must-Use Gap

Aria Beingessner's analysis of substructural type systems reveals a gap in Rust's type system that most users never notice: Rust has move semantics (affine types -- values can be used at most once), but it doesn't have linear types (values must be used exactly once). You can enforce that step1 happens before step2 with move-only tokens, but you can't enforce that step2 actually gets called.3

Drop partially addresses this -- it runs automatically when a value goes out of scope. But Drop can't produce values, can't take extra parameters, and there's only one Drop per type. The mem::forget escape hatch lets you skip Drop entirely (deliberately, for implementing unsafe collections). And reference cycles, ptr::write, and aborts can all leak destructors.

The "destructor bomb" pattern -- implementing Drop as abort("this value must be consumed") -- provides a dynamic check, but Rust users want static verification. This is one of those cases where the type system is almost powerful enough to express what you want, and the gap is more frustrating than a language that doesn't try at all.

Supply Chain Anxiety

Rust's security story has a gap at the package level. The curl | sh installation model, the build.rs scripts that run arbitrary code during compilation, the chained transitive dependencies -- bunnie found 5,700 lines of third-party build scripts manipulating files and running programs on his machine with every build. For a security-focused project like Precursor, this is terrifying.1

Cargo.lock helps pin versions, and Xous vendors critical crates, but the fundamental architecture means that adding one dependency can pull in an entire tree of build-time code execution. For a language that markets itself on safety, the supply chain trust model is surprisingly permissive. Bunnie's crate-scraper tool collates all build.rs files for manual review, but he admits manual review can't catch cleverly disguised malware -- it just shows the scale of the attack surface.

The Verdict, Honestly

Rust is genuinely better than C for maintaining large systems over time. The refactoring story is transformative. The type system catches real bugs. Fearless concurrency is not just marketing.

But it's not simple, and the complexity doesn't go away -- it gets redistributed. Into dense syntax, into fighting the borrow checker on legitimate patterns, into tracking the release train, into auditing transitive dependencies. The tradeoff is worth it for security-critical systems and long-lived infrastructure. For a script that runs once, or a prototype that might get thrown away, the upfront cost is harder to justify.

Footnotes

  1. Rust: A Critical Retrospective by bunnie huang -- source 2 3 4 5

  2. You can't Rust that by Armin Ronacher -- source 2

  3. The Pain Of Real Linear Types in Rust by Aria Beingessner -- source

Open in stacked reader →