rust-in-action
Rust in Action Skill
Apply the systems programming practices from Tim McNamara's "Rust in Action" to review existing code and write new Rust. This skill operates in two modes: Review Mode (analyze code for violations of Rust idioms and systems programming correctness) and Write Mode (produce safe, idiomatic, systems-capable Rust from scratch).
The key differentiator of this book: Rust is taught through real systems — a CPU simulator, key-value store, NTP client, raw TCP stack, and OS kernel. Practices focus on correctness at the hardware boundary, not just language syntax.
Reference Files
practices-catalog.md— Before/after examples for ownership, smart pointers, bit ops, I/O, networking, concurrency, error wrapping, and state machines
How to Use This Skill
Before responding, read practices-catalog.md for the topic at hand. For ownership/borrowing issues read the ownership section. For systems/binary data read the data section. For a full review, read all sections.
Mode 1: Code Review
When the user asks you to review Rust code, follow this process:
Step 1: Identify the Domain
Determine whether the code is application-level, systems-level (binary data, I/O, networking, memory), or concurrent. The review focus shifts accordingly.
Step 2: Analyze the Code
Check these areas in order of severity:
- Ownership & Borrowing (Ch 4): Unnecessary
.clone()? Value moved when a borrow would suffice? Use references where full ownership is not required. - Smart Pointer Choice (Ch 6): Is the right pointer type used?
Box<T>for heap,Rc<T>for single-thread shared,Arc<T>for multi-thread shared,RefCell<T>for interior mutability (single-thread),Mutex<T>for interior mutability (multi-thread).Cow<T>when data is usually read but occasionally mutated. - Error Handling (Ch 3, 8):
.unwrap()or.expect()where?belongs? For library code, define a custom error type that wraps downstream errors viaFromimpl. Never leak internal error types across the public API boundary. - Binary Data & Endianness (Ch 5, 7): Are integer byte representations explicit? Use
to_le_bytes()/from_le_bytes()/to_be_bytes(). Validate with checksums when writing binary formats. Useserde+bincodefor structured serialization. - Memory (Ch 6): Is
unsafeminimized? Raw pointer use must be bounded by a safe abstraction. Stack vs heap allocation: prefer stack; useBoxonly when size is unknown at compile time or you need heap lifetime. - File & I/O (Ch 7): Use
BufReader/BufWriterfor large files. HandleENOENT,EPERM,ENOSPCdistinctly — don't collapse I/O errors to strings. Usestd::fs::Pathfor type-safe path handling. - Networking (Ch 8): TCP state is implicit in OS — model explicit state machines with enums. Use trait objects (
Box<dyn Trait>) only when heterogeneous runtime dispatch is needed. Preferimpl Traitfor static dispatch. - Concurrency (Ch 10): Closures passed to threads must be
'staticor usemove. Shared mutable state needsArc<Mutex<T>>. Use channels for message passing over shared state. Thread pool patterns over spawning one thread per task. - Time (Ch 9): Don't use
std::time::SystemTimefor elapsed measurement — it can go backwards. Usestd::time::Instantfor durations. For network time, NTP requires epoch conversion (NTP epoch: 1900 vs Unix: 1970 — offset 70 years = 2_208_988_800 seconds). - Idioms: Iterator adapters over manual loops.
for item in &collectionnotfor i in 0..collection.len().if let/while letfor single-variant matching. Exhaustivematch— no silent wildcard arms.
Step 3: Report Findings
For each issue, report:
- Chapter reference (e.g., "Ch 6: Smart Pointers")
- Location in the code
- What's wrong (the anti-pattern)
- How to fix it (the idiomatic / systems-correct approach)
- Priority: Critical (safety/UB/data corruption), Important (idiom/correctness), Suggestion (polish)
Step 4: Provide Fixed Code
Offer a corrected version with comments explaining each change.
Mode 2: Writing New Code
When the user asks you to write new Rust code, apply these core principles:
Language Foundations (Ch 2)
-
Use cargo, not rustc directly (Ch 2).
cargo new,cargo build,cargo test,cargo doc. Add third-party crates viaCargo.toml— never manually link. -
Prefer integer types that match the domain (Ch 2). Use
u8for bytes,u16/u32/u64for protocol fields sized to spec,i64for timestamps. Avoid defaultusizefor domain values. -
Use
loopfor retry/event loops;whilefor condition-driven;forfor iteration (Ch 2). Never useloop { if cond { break } }wherewhile cond {}is clearer.
Compound Types & Traits (Ch 3)
-
Model domain state with enums, not stringly-typed flags (Ch 3). Enums with data (
enum Packet { Ack(u32), Data(Vec<u8>) }) replace boolean + optional pairs and make invalid states unrepresentable. -
Implement
new()as the canonical constructor (Ch 3).impl MyStruct { pub fn new(...) -> Self { ... } }. UseDefaultfor zero-value construction. -
Implement
std::fmt::Displayfor user-facing output,Debugvia derive (Ch 3). DeriveDebug; hand-implementDisplay. Never use{:?}in user-facing messages. -
Use
pub(crate)to limit visibility to the crate; keep internals private (Ch 3). Public API surface should be minimal and intentional. -
Document public items with
///rustdoc comments (Ch 3). Include examples in doc comments —cargo testruns them.
Ownership, Borrowing & Smart Pointers (Ch 4, 6)
-
Use references where full ownership is not required (Ch 4). Pass
&Tfor read,&mut Tfor write. Only transfer ownership when the callee must own (e.g., storing in a struct). -
Choose smart pointers by use case (Ch 6):
Box<T>— heap allocation, single owner, unknown size at compile timeRc<T>— shared ownership, single-threadedArc<T>— shared ownership, multi-threadedCell<T>— interior mutability forCopytypes, single-threadedRefCell<T>— interior mutability for non-Copy, single-threaded, runtime borrow checksCow<'a, T>— clone-on-write, avoids allocation when data is only readArc<Mutex<T>>— shared mutable state across threads
-
Never use
Rcacross thread boundaries (Ch 6). The compiler enforces this —Rcis notSend. UseArcinstead. -
Minimize
unsafeblocks; wrap them in safe abstractions (Ch 6). Raw pointers (*const T,*mut T) must be bounded within a module or function that upholds safety invariants. Document the safety contract with// SAFETY:comments.
Data Representation (Ch 5)
-
Be explicit about endianness in binary protocols (Ch 5, 7). Use
u32::to_le_bytes(),u32::from_be_bytes()etc. Never assume native endianness when writing to disk or network. -
Use bit operations to inspect and build packed data (Ch 5). AND (
&) to isolate bits, OR (|) to set bits, shift (<<,>>) to position. Use named constants for masks:const SIGN_BIT: u32 = 0x8000_0000. -
Validate binary data with checksums (Ch 7). For key-value stores and file formats, store a CRC or hash alongside data. Verify on read before trusting.
Files & Storage (Ch 7)
-
Use
BufReader/BufWriterfor file I/O (Ch 7). RawFile::read()makes a syscall per call.BufReaderbatches reads into user-space buffer. -
Use
serde+bincodefor binary serialization (Ch 7). Add#[derive(Serialize, Deserialize)]; letbincode::serialize/deserializehandle encoding. Useserde_jsonfor human-readable formats. -
Use
std::path::PathandPathBuffor file paths (Ch 7). Never build paths with string concatenation. Usepath.join(),path.extension(),path.file_name().
Networking (Ch 8)
-
Model protocol state explicitly with enums (Ch 8). A TCP connection has states (SYN_SENT, ESTABLISHED, CLOSE_WAIT, etc.). Encode them as enum variants — the compiler enforces valid transitions.
-
Wrap library errors in a domain error type (Ch 8). When a function calls multiple libraries (network + I/O + parse), define an enum that wraps each. Implement
From<LibError> for DomainErrorso?converts automatically. -
Use trait objects only for heterogeneous runtime dispatch (Ch 8).
Vec<Box<dyn Animal>>is correct when you have a mixed collection. For a single concrete type,impl Traitis zero-cost.
Concurrency (Ch 10)
-
Use
moveclosures when passing to threads (Ch 10).thread::spawn(move || { ... })transfers ownership of captured variables into the thread. This is required when the closure outlives the current stack frame. -
Use
Arc::clone()explicitly, not.clone()on a value (Ch 10).Arc::clone(&ptr)is idiomatic — it's cheap (increments a reference count). Avoid.clone()on the inner value. -
Use channels for work distribution;
Arc<Mutex<T>>for shared state (Ch 10). Channels (std::sync::mpsc) are simpler and safer. Use shared state only when channels don't fit (e.g., result collection). -
Use thread pools over raw
thread::spawnper task (Ch 10). Spawning one thread per request doesn't scale. Userayon,tokio, or a manual pool with a bounded queue.
Time (Ch 9)
-
Use
Instantfor elapsed time,SystemTimefor wall clock (Ch 9).SystemTimecan go backwards (NTP adjustments, leap seconds).Instantis monotonic. -
Apply the NTP epoch offset when working with network time (Ch 9). NTP timestamps count seconds from 1900-01-01; Unix timestamps count from 1970-01-01. Offset:
2_208_988_800u64seconds.
Smart Pointer Selection Guide (Ch 6)
Is the data shared across threads?
├── Yes → Arc<T> (read-only) or Arc<Mutex<T>> (mutable)
└── No
├── Shared (multiple owners, single thread)?
│ └── Rc<T> (read-only) or Rc<RefCell<T>> (mutable)
└── Single owner
├── Size unknown at compile time / recursive type?
│ └── Box<T>
├── Usually read, occasionally cloned/modified?
│ └── Cow<'a, T>
└── Interior mutability needed?
├── Copy type → Cell<T>
└── Non-Copy → RefCell<T>
Code Structure Templates
Binary Protocol Field (Ch 5, 7)
/// Parse a 4-byte big-endian u32 from a byte buffer at offset.
fn read_u32_be(buf: &[u8], offset: usize) -> Result<u32, ParseError> {
buf.get(offset..offset + 4)
.ok_or(ParseError::UnexpectedEof)
.map(|b| u32::from_be_bytes(b.try_into().unwrap()))
}
const FLAGS_MASK: u8 = 0b0000_1111; // isolate lower 4 bits
fn extract_flags(byte: u8) -> u8 {
byte & FLAGS_MASK
}
Library Error Type (Ch 8)
#[derive(Debug)]
pub enum AppError {
Io(std::io::Error),
Network(std::net::AddrParseError),
Parse(String),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppError::Io(e) => write!(f, "I/O error: {e}"),
AppError::Network(e) => write!(f, "network error: {e}"),
AppError::Parse(msg) => write!(f, "parse error: {msg}"),
}
}
}
impl std::error::Error for AppError {}
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From<std::net::AddrParseError> for AppError {
fn from(e: std::net::AddrParseError) -> Self { AppError::Network(e) }
}
State Machine with Enum (Ch 8)
#[derive(Debug, Clone, PartialEq)]
enum ConnectionState {
Idle,
Connecting { addr: std::net::SocketAddr },
Connected { stream: std::net::TcpStream },
Closed,
}
impl ConnectionState {
fn connect(addr: std::net::SocketAddr) -> Result<Self, AppError> {
let stream = std::net::TcpStream::connect(addr)?;
Ok(ConnectionState::Connected { stream })
}
}
Thread Pool Pattern (Ch 10)
use std::sync::{Arc, Mutex};
use std::sync::mpsc;
use std::thread;
type Job = Box<dyn FnOnce() + Send + 'static>;
struct ThreadPool {
sender: mpsc::Sender<Job>,
}
impl ThreadPool {
fn new(size: usize) -> Self {
let (sender, receiver) = mpsc::channel::<Job>();
let receiver = Arc::new(Mutex::new(receiver));
for _ in 0..size {
let rx = Arc::clone(&receiver);
thread::spawn(move || loop {
let job = rx.lock().expect("mutex poisoned").recv();
match job {
Ok(f) => f(),
Err(_) => break, // channel closed
}
});
}
ThreadPool { sender }
}
fn execute(&self, f: impl FnOnce() + Send + 'static) {
self.sender.send(Box::new(f)).expect("thread pool closed");
}
}
Priority of Practices by Impact
Critical (Safety, Correctness, UB)
- Ch 4: Borrow, don't clone — never move when a reference suffices
- Ch 6: Choose the right smart pointer —
Rcis not thread-safe; don't share across threads - Ch 6: Wrap
unsafein safe abstractions — document safety contracts with// SAFETY: - Ch 5/7: Explicit endianness — wrong byte order silently corrupts binary data
- Ch 9: Use
Instantfor elapsed time —SystemTimecan go backwards
Important (Idiom & Maintainability)
- Ch 3: Domain enums over stringly-typed state — invalid states should not compile
- Ch 8: Wrap downstream errors in a domain error type with
Fromimpls - Ch 8:
impl Traitoverdyn Traitwhen types are homogeneous - Ch 10:
moveclosures for threads — required when closure outlives the stack frame - Ch 10:
Arc::clone()idiom — makes cheap pointer clone explicit
Suggestions (Systems Polish)
- Ch 2: Size integer types to the protocol spec —
u8for bytes,u16for ports - Ch 5: Named bit-mask constants —
const SIGN_BIT: u32 = 0x8000_0000 - Ch 7:
BufReader/BufWriterfor all file I/O — syscall batching - Ch 7: Checksums on binary writes — detect corruption on read
- Ch 9: NTP epoch offset constant —
const NTP_UNIX_OFFSET: u64 = 2_208_988_800
More from booklib-ai/skills
effective-java
>
21lean-startup
>
19clean-code-reviewer
Reviews code against Robert C. Martin's Clean Code principles. Use when users share code for review, ask for refactoring suggestions, or want to improve code quality. Produces actionable feedback organized by Clean Code principles with concrete before/after examples.
17domain-driven-design
>
16design-patterns
>
14refactoring-ui
>
14