AgentSkillsCN

build-rust

Rust 生产级开发模式。适用于:构建 Rust CLI、后端、前端,或原生应用时。涵盖 axum、tonic、sqlx、Leptos、Dioxus、Tauri、clap、tokio 等技术栈。深入探讨生产环境中的常见陷阱(阻塞、取消、互斥锁),以及所有权决策、crate 选型等关键问题。同时覆盖嵌入式开发、FFI 接口、proc-macros 宏扩展,以及代理/数据平面等专业领域。

SKILL.md
--- frontmatter
name: build-rust
description: "Rust production patterns. Use when: building Rust CLI, backend, frontend, or native apps. Covers axum, tonic, sqlx, Leptos, Dioxus, Tauri, clap, tokio. Production gotchas (blocking, cancellation, mutex), ownership decisions, crate selection. Routes to specialized domains: embedded, FFI, proc-macros, proxies/data-plane."

Rust Production Patterns

Production-grade patterns that separate competent from exceptional Rust developers.

Philosophy

  1. Make illegal states unrepresentable — use types to eliminate bugs (Minsky 2010)
  2. Parse, don't validate — transform unstructured data into typed structures (King 2019)
  3. Zero-cost abstractions — high-level code that compiles to optimal machine code (Stroustrup 2012)
  4. Explicit over implicit — no hidden allocations, no surprise behavior (Rust design principle)
  5. Design away lifetime complexity — if fighting the borrow checker, reconsider data model (community convention)
  6. Clone consciously — every .clone() is a decision about allocation (community convention)
  7. Trust but verify safety — Rust prevents data races, not deadlocks (language guarantee)

Decision Frameworks

String Ownership

Heuristic from Steve Klabnik (Rust core team, 12+ years experience): "Following this rule will get you through 95% of situations."

ContextUseWhy
Struct fieldsStringOwned data lives with struct
Function params&strAccept any string via deref
Return (from input)&strZero-cost slice
Return (newly created)StringCaller needs ownership
Conditional modificationCow<'_, str>Clone-on-write

Trait Objects vs Generics

FactorGenericsdyn Trait
PerformanceFaster (static dispatch)Slower (vtable)
Binary sizeLargerSmaller
Heterogeneous collectionsNoYes

Rule: Default to generics. Use dyn for heterogeneous collections or plugin systems.

Error Handling Selection

code
Writing a library?
├── YES → thiserror (callers match on variants)
└── NO (application) → Need pretty diagnostics?
    ├── YES → color-eyre (CLI) or miette (source snippets)
    └── NO → anyhow

Async vs Threads

WorkloadChoiceRule
CPU-boundThreads / spawn_blockingNever block async workers
High-concurrency I/OAsyncScales to millions
Simple concurrencyThreadsAvoid async complexity

Guideline: Keep work between .await points brief (microseconds to tens of milliseconds). Tokio uses cooperative scheduling with budget-based yielding since v0.2.14 — tasks that exceed budget get nudged to yield, but long-running sync work still starves the runtime (tokio preemption blog).

Production Gotchas

Well-established patterns from tokio documentation and production experience.

Blocking in Async

  • Trap: Sync operations inside async tasks starve runtime (tokio shared-state tutorial)
  • Detection: tokio-console shows tasks not yielding (see "task liveliness" metrics)
  • Fix: spawn_blocking() for CPU work; async alternatives for I/O
rust
// Wrong
async fn bad() {
    std::thread::sleep(Duration::from_secs(2)); // Blocks worker
}

// Correct
async fn good() {
    tokio::task::spawn_blocking(|| heavy_computation()).await.unwrap();
}

Mutex Across Await

  • Trap: std::sync::Mutex guard held across .await deadlocks (tokio shared-state tutorial)
  • Detection: Deadlock under load; compiles fine (Clippy lint await_holding_lock catches this)
  • Fix: Drop guard before await, or tokio::sync::Mutex
rust
// Deadlock risk
async fn bad(mutex: Arc<std::sync::Mutex<i32>>) {
    let guard = mutex.lock().unwrap();
    some_async_op().await; // Guard held!
}

// Safe: explicit drop
async fn good(mutex: Arc<std::sync::Mutex<i32>>) {
    {
        let mut guard = mutex.lock().unwrap();
        *guard += 1;
    } // Dropped before await
    some_async_op().await;
}

Cancellation Safety

  • Trap: Futures dropped mid-operation leave invalid state (tokio docs: cancel-safety)
  • Detection: Check API docs for "Cancel safety" section; read is safe, read_line is NOT
  • Fix: Don't hold invalid state across await; use CancellationToken (tokio-util)
  • Checkpoint: Before using select! or timeout, verify each branch's cancellation safety in docs

Feature Flag Unification

  • Trap: Cargo unifies features globally; non-additive features break (Cargo reference: features)
  • Detection: cargo tree --edges features (Cargo book)
  • Fix: Features must be additive; use default-features = false
  • Checkpoint: Before adding feature-gated dependencies, run cargo tree -e features -i <crate> to check resolution

Hidden Allocations

  • Trap: clone(), to_string(), format!(), Vec growth (community pattern)
  • Detection: DHAT, cargo-flamegraph, Clippy perf lints
  • Fix: with_capacity(), SmallVec, Cow, shrink_to_fit()

Reference Cycles

  • Trap: Rc<RefCell<T>> cycles leak memory (Rust Book ch15)
  • Fix: Weak<T> for back-edges; consider arenas

Obsolete Patterns

ObsoleteReplacementReference
lazy_static!std::sync::LazyLockRust 1.80
once_cell (most uses)std::sync::OnceLockRust 1.70
async-stdsmol (or tokio)Deprecated March 2025
structoptclap v4 deriveclap 3.0 release
async-trait (some cases)Native async fn in traitsRust 1.75
async closure workaroundsNative async || {} closuresRust 1.85
ansi_termnu-ansi-termRUSTSEC-2021-0139
wee_allocDefault allocator or TalcRUSTSEC-2022-0054

Note: async-trait still needed for dyn Trait with async methods.

Type Design Patterns

Newtype Pattern

Compile-time type safety for IDs:

rust
struct UserId(u64);
struct OrderId(u64);

fn process_user(id: UserId) { /* ... */ }
// process_user(OrderId(1)); // Won't compile!

Builder Pattern

rust
struct ConfigBuilder {
    required_field: Option<String>,
    optional_field: Option<i32>,
}

impl ConfigBuilder {
    fn required_field(mut self, val: String) -> Self {
        self.required_field = Some(val);
        self
    }

    fn build(self) -> Result<Config, BuilderError> {
        Ok(Config {
            required_field: self.required_field.ok_or(BuilderError::MissingField)?,
            optional_field: self.optional_field.unwrap_or_default(),
        })
    }
}

Use typed-builder crate in production.

Typestate Pattern

Compile-time state machine enforcement:

rust
struct Connection<State> { /* ... */ _state: PhantomData<State> }
struct Disconnected;
struct Connected;
struct Authenticated;

impl Connection<Disconnected> {
    fn connect(self) -> Connection<Connected> { /* ... */ }
}

impl Connection<Connected> {
    fn authenticate(self, creds: &str) -> Connection<Authenticated> { /* ... */ }
}

impl Connection<Authenticated> {
    fn query(&self, sql: &str) -> Result<Data, Error> { /* ... */ }
}
// Can't call query() on unauthenticated connection - won't compile

When to use: Required state transitions, protocol implementations.

Enums Over Booleans

rust
// Bad
fn validate(data: &str, strict: bool) { /* ... */ }

// Good
enum Validation { Strict, Lenient }
fn validate(data: &str, mode: Validation) { /* ... */ }

Error Handling

Library Errors — thiserror

rust
#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("invalid input: {0}")]
    InvalidInput(String),
    #[error("network error")]
    Network(#[from] std::io::Error),
    #[error(transparent)]
    Other(#[from] anyhow::Error),
}

pub type Result<T> = std::result::Result<T, Error>;

Application Errors — anyhow

rust
use anyhow::{Context, Result, bail, ensure};

fn process() -> Result<()> {
    let data = read_file()
        .with_context(|| format!("failed to read config"))?;

    ensure!(!data.is_empty(), "config file is empty");

    if invalid(&data) {
        bail!("invalid configuration format");
    }
    Ok(())
}

Rules:

  • Never .unwrap() in library code
  • Never .expect() without useful message

Production Patterns

Defensive Slice Matching

rust
// Anti-pattern: decoupled check
if !users.is_empty() { let user = users[0]; }

// Better: coupled via match
match users.as_slice() {
    [] => handle_empty(),
    [single] => handle_one(single),
    [first, ..] => handle_multiple(first),
}

Simplify Lifetimes Through Architecture

Insight: "Data lives forever or for duration of event loop."

  • Use Copy IDs instead of references
  • Pass references top-down each frame
  • Trade-off: Lookup indirection vs lifetime elimination

Temporary Mutability Scoping

rust
let sorted = {
    let mut temp = get_data();
    temp.sort();
    temp  // Immutable from here
};

Extension Traits

  • Suffix with Ext (RFC 445)
  • Export in prelude for glob import

Specialized Domains

Load reference based on project context:

DetectedLoad
clap, lexopt, CLI binarycli.md
axum, tonic, sqlx, API/servicebackend.md
leptos, dioxus, wasm-bindgen, browser WASMfrontend.md
tauri, egui, desktop/mobile appnative.md
#![no_std], cortex-m, embassy, rticembedded.md
pingora, rama, proxy, xdsdata-plane.md
bindgen, cbindgen, cxx, PyO3, unsafeffi-unsafe.md
proc-macro = true, syn, quoteproc-macros.md
reqwest, HTTP client, protocolsnetworking.md
Crate selection questionsecosystem.md
Project setup, CI, configstooling.md
Deep async patterns, tokio internalsasync.md

Anti-Patterns

Don'tDoWhy
.unwrap() in libsReturn ResultCallers can't recover
.clone() to fix borrow checkerRedesign ownershipHidden allocation, wrong model
Arc<Mutex<T>> everywhereChannels, message passingDeadlock risk, contention
String for everythingNewtypes, enumsType safety
pub by defaultpub(crate), minimal exposureAPI surface control
Hold mutex across .awaitDrop before awaitDeadlock
lazy_static!LazyLockDeprecated
Block in asyncspawn_blockingStarves runtime

Quick Reference

Commands

bash
cargo clippy -- -W clippy::pedantic  # Lints
cargo nextest run   # Faster tests    |  cargo deny check    # Deps
cargo tree --edges features          # Feature resolution
cargo bloat --release                # Binary size

Common Crates

NeedCrate
Errors (lib)thiserror
Errors (app)anyhow
Serializationserde, serde_json, toml
CLIclap (or lexopt for minimal)
Asynctokio
HTTPreqwest (client), axum (server)
Loggingtracing
Testingproptest, nextest, insta