Saltar a contenido

12 - Reading Other People's Code

What this session is

About 45 minutes. You'll learn the strategy for reading code you didn't write. Same shape as the equivalent page in Go/Java/Python from scratch - adapted for Rust idioms.

The trick

When you open a new codebase, don't try to read it linearly. Pick a small thread; follow only it; let the rest stay fuzzy. A first read of a real Rust project takes 5 minutes and produces a one-paragraph mental map; not three hours and confusion.

Five-minute orientation

For any Rust project:

  1. Read the README. What does this do? If unclear, the project is too unfinished.

  2. List the top-level files/directories:

  3. Cargo.toml - manifest.
  4. Cargo.lock - pinned versions (for binaries).
  5. src/ - source.
  6. src/lib.rs - library entry point.
  7. src/main.rs - binary entry point.
  8. src/bin/ - multiple binary entries.
  9. tests/ - integration tests.
  10. examples/ - runnable examples.
  11. benches/ - benchmarks (criterion).
  12. docs/ - long-form docs (some projects).
  13. .github/workflows/ - CI.

  14. Open Cargo.toml. Name? Version? Dependencies? Tells you the ecosystem.

  15. Find the entry point. For a library: src/lib.rs. Read it - often a list of pub mod foo; declarations and re-exports. That tells you the public API.

  16. Read one test or example. Tests show you what the code is supposed to do, with concrete code.

After this: write a one-paragraph summary. If you can't, repeat.

Tools for reading Rust

  • rust-analyzer in your editor - go-to-definition, find-references, inline type display. Use these constantly.
  • cargo doc --open - generate and open the project's documentation in your browser. Sorted, navigable, includes all public APIs.
  • docs.rs - the public hosted documentation for every published crate. Search " docs.rs".
  • cargo test --no-run - compiles tests without running them. Useful for "does this build" before diving in.
  • grep -r 'pattern' src/ - old-school but works.
  • cargo expand (cargo install cargo-expand) - shows what macros expand to. Useful when you encounter a #[derive(...)] or other macro you don't understand.

A worked example: reading serde_json::Value

serde_json::Value is the dynamic JSON value type. Let's say we just encountered it.

  1. cargo doc --open (in a project using serde_json) - search for Value.
  2. Documentation says: "Represents any valid JSON value." Variants: Null, Bool(bool), Number(Number), String(String), Array(Vec<Value>), Object(Map<String, Value>).
  3. So Value is an enum (page 05 of this path!). Read a few of its methods: is_string(), as_str(), etc.
  4. Look at examples in the docs. Most public docs include them.

Five minutes, mental model.

Things you'll see that look scary in Rust

  • Lifetime annotations (<'a>, &'a str) - page 06. Most of the time the compiler infers; you see them in library code. For reading: "this reference must live as long as that."
  • Macros (println!, vec!, derive!) - the ! marks them. Macros expand to code at compile time. For reading: treat as "a function with magic."
  • Box<dyn Trait> - page 09. Heap-allocated trait object.
  • Pin<Box<...>> - used in async. Means "this can't be moved in memory after creation." Don't worry about it unless you're writing async runtimes.
  • async and await - async/await syntax. Used heavily in web/networking. Functions return a "future" that runs when polled by a runtime (Tokio, async-std).
  • unsafe { ... } - escape hatch. The author takes responsibility for memory safety inside. Recognize; skim.
  • impl Trait in return position - fn foo() -> impl Iterator<Item = i32>. Means "returns some type that implements Iterator." The actual type is hidden from callers.
  • Rc<RefCell<T>> / Arc<Mutex<T>> - shared ownership with interior mutability. Used when you need shared mutable state across functions or threads.
  • Generic chaos: <T: Iterator<Item = U>, U: Clone> - complex generic bounds. Skim; understand the gist.
  • Procedural macros (#[proc_macro_attribute], etc.) - code that generates code. Used by serde, tokio, actix. Recognize; treat as "magic."

You will hit things you don't recognize. Knowing when to skim past vs dig in is the skill.

Reading vs understanding

You can read code without deeply understanding why it's shaped that way. A first PR to a project often involves reading 1000 lines, understanding 100, modifying 5. That ratio is normal.

Exercise

No coding this time. Reading.

Pick a small Rust project on GitHub:

  • fdehau/tui-rs or its successor ratatui-org/ratatui - terminal UI library. Reasonable size.
  • BurntSushi/ripgrep - fast grep. Bigger but very well-organized.
  • clap-rs/clap - CLI parser. Substantial but excellently documented.
  • For something tiny: seanmonstar/num_cpus - one purpose, one function (num_cpus::get()).

Apply orientation: 1. README - what does it do? 2. Layout - what files exist? 3. Cargo.toml - dependencies? 4. Find the entry point. Trace the most-public function for 5 minutes. 5. Read one test or example.

Write a paragraph: what does this do? How is it organized? What surprised you?

What you might wonder

"What if I don't understand?" Note, skip, keep going. Often what confused you on day 1 makes sense after a week.

"What about huge projects like rust-analyzer itself?" Same technique, just more applied. Read the top-level architecture docs first; then pick one component and orient on just that.

Done

  • Apply five-minute orientation to any Rust project.
  • Use rust-analyzer, cargo doc, docs.rs.
  • Distinguish reading from understanding.
  • Recognize common "looks scary, isn't" patterns.

Next: Picking a project →

Comments