rust-expert

Apply idiomatic Rust patterns with strong safety, performance, and maintainability guarantees.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "rust-expert" with this command: npx skills add oimiragieo/agent-studio/oimiragieo-agent-studio-rust-expert

Rust Expert

Apply idiomatic Rust patterns with strong safety, performance, and maintainability guarantees.

Core Principles

  • Model correctness through the type system; make invalid states unrepresentable.

  • Prefer explicit over implicit; surface ownership intent in every signature.

  • Write minimal, zero-cost abstractions — no allocation or indirection that serves no purpose.

  • Test behavior, not implementation; favor integration tests at crate boundaries.

  1. Ownership, Borrowing, and Lifetimes

Ownership Patterns

// Prefer moves when value is consumed; prefer borrows when value is shared. fn process(data: Vec<u8>) -> Vec<u8> { /* consumes / } fn inspect(data: &[u8]) -> usize { data.len() / borrows */ }

// Clone only when necessary and explicitly documented. // Anti-pattern: clone() to silence borrow checker errors — fix the design instead.

Borrowing Rules (enforce at design time)

  • Only one mutable reference OR any number of immutable references at a time.

  • References must not outlive the value they point to.

  • Use Cow<'a, T> when you need borrow-or-own semantics without forcing allocation.

use std::borrow::Cow;

fn normalize(s: &str) -> Cow<str> { if s.contains(' ') { Cow::Owned(s.replace(' ', "_")) } else { Cow::Borrowed(s) } }

Lifetime Patterns

// Explicit lifetime annotation: only when compiler cannot infer. struct Parser<'input> { source: &'input str, pos: usize, }

impl<'input> Parser<'input> { fn next_token(&mut self) -> &'input str { // Returns a slice of the original input — lifetime ties result to source. &self.source[self.pos..] } }

// RPIT (Return Position Impl Trait) avoids lifetime noise in many cases. fn words(s: &str) -> impl Iterator<Item = &str> { s.split_whitespace() }

Anti-Patterns to Avoid

  • clone() inside hot loops to avoid borrow checker.

  • unsafe to bypass lifetime checks — redesign instead.

  • 'static bounds that force heap allocation when borrowing suffices.

  • Storing &mut T in structs — prefer owned data or RefCell<T> .

  1. Error Handling

Decision Matrix

Scenario Tool

Library crate errors thiserror — typed, composable

Application / binary errors anyhow — ergonomic ? chaining

Domain-specific context Custom enum via thiserror

Infallible conversions From / Into — zero overhead

thiserror (Library Crates)

use thiserror::Error;

#[derive(Debug, Error)] pub enum ConfigError { #[error("missing required field: {field}")] MissingField { field: &'static str }, #[error("invalid value for {field}: {source}")] ParseError { field: &'static str, #[source] source: std::num::ParseIntError }, #[error(transparent)] Io(#[from] std::io::Error), }

anyhow (Application / Binary)

use anyhow::{Context, Result};

fn load_config(path: &str) -> Result<Config> { let raw = std::fs::read_to_string(path) .with_context(|| format!("reading config from {path}"))?; toml::from_str(&raw).context("parsing config TOML") }

Error Propagation Rules

  • Never use .unwrap() in library code; use .expect() only in tests and main() where the invariant is self-evident.

  • Add .context() / .with_context() at every error boundary to preserve the call chain.

  • Map errors at crate boundaries — do not leak internal error types in public APIs.

  1. Trait System

Generics vs Trait Objects

// Prefer generics (monomorphized, zero-cost) when types are known at compile time. fn serialize<S: Serialize>(value: &S) -> Vec<u8> { /* ... */ }

// Use dyn Trait only when you need heterogeneous collections or runtime dispatch. fn handlers() -> Vec<Box<dyn EventHandler>> { /* ... */ }

Where Clauses

// Prefer where clauses for readability when bounds are complex. fn merge<K, V>(a: HashMap<K, V>, b: HashMap<K, V>) -> HashMap<K, V> where K: Eq + Hash, V: Clone, { /* ... */ }

Blanket Implementations and Orphan Rules

  • Implement standard traits (Display , From , Iterator ) for your types.

  • Respect the orphan rule: you may only implement a foreign trait for a local type.

  • Use the newtype pattern to work around orphan restrictions.

struct Wrapper(Vec<u8>);

impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self.0) } }

Rust 1.75+ Async in Traits (RPITIT)

// Stable as of Rust 1.75 — no more async-trait macro needed. trait DataSource { async fn fetch(&self, id: u64) -> anyhow::Result<Record>; }

// Return-Position Impl Trait in Traits (RPITIT) — also stable in 1.75. trait Transformer { fn transform(&self, input: &str) -> impl Iterator<Item = String>; }

  1. Async / Await with Tokio

Tokio Runtime Setup

#[tokio::main] async fn main() -> anyhow::Result<()> { // Default: multi-thread runtime (all CPU cores). Ok(()) }

// Single-thread runtime for I/O-only workloads. #[tokio::main(flavor = "current_thread")] async fn main() -> anyhow::Result<()> { Ok(()) }

Task Spawning

use tokio::task;

// Spawn a non-blocking async task. let handle = task::spawn(async move { fetch_data(url).await }); let result = handle.await?; // propagate JoinError

// CPU-bound work must go to the blocking thread pool — never block the async runtime. let result = task::spawn_blocking(|| { expensive_cpu_computation() }).await?;

Channels

use tokio::sync::{mpsc, oneshot, broadcast, watch};

// mpsc: producer → consumer pipeline. let (tx, mut rx) = mpsc::channel::<Bytes>(1024);

// oneshot: request / response pattern. let (resp_tx, resp_rx) = oneshot::channel::<Result<Record>>();

// broadcast: fan-out to multiple subscribers. let (tx, _rx) = broadcast::channel::<Event>(16);

// watch: latest-value semantics (config reload, health state). let (tx, rx) = watch::channel(Config::default());

select! and Cancellation

use tokio::select; use tokio_util::sync::CancellationToken;

async fn worker(token: CancellationToken) { select! { _ = token.cancelled() => { // Structured cancellation — always handle shutdown path. } result = do_work() => { // Normal completion. } } }

Structured Concurrency

use tokio::task::JoinSet;

async fn fetch_all(urls: Vec<String>) -> Vec<anyhow::Result<Bytes>> { let mut set = JoinSet::new(); for url in urls { set.spawn(fetch(url)); } let mut results = Vec::new(); while let Some(res) = set.join_next().await { results.push(res.expect("task panicked")); } results }

Async Anti-Patterns

  • std::thread::sleep inside async context — use tokio::time::sleep .

  • Holding MutexGuard across .await — use tokio::sync::Mutex instead.

  • Blocking I/O (file read, DNS) on async thread — use spawn_blocking or tokio::fs .

  • Unbounded channels — always set a capacity bound.

  1. Common Crates

serde — Serialization

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct User { pub id: u64, pub display_name: String, #[serde(skip_serializing_if = "Option::is_none")] pub email: Option<String>, }

axum — HTTP Servers

use axum::{extract::{Path, State}, routing::get, Json, Router};

async fn get_user( State(db): State<DbPool>, Path(id): Path<u64>, ) -> Result<Json<User>, AppError> { let user = db.find_user(id).await?; Ok(Json(user)) }

let app = Router::new() .route("/users/:id", get(get_user)) .with_state(db_pool);

sqlx — Async Database

use sqlx::PgPool;

async fn insert_user(pool: &PgPool, name: &str) -> sqlx::Result<i64> { let row = sqlx::query!("INSERT INTO users (name) VALUES ($1) RETURNING id", name) .fetch_one(pool) .await?; Ok(row.id) }

reqwest — HTTP Client

use reqwest::Client;

async fn fetch_json<T: for<'de> serde::Deserialize<'de>>( client: &Client, url: &str, ) -> anyhow::Result<T> { Ok(client.get(url).send().await?.error_for_status()?.json().await?) }

rayon — Data Parallelism

use rayon::prelude::*;

fn parallel_sum(data: &[f64]) -> f64 { data.par_iter().sum() }

clap — CLI

use clap::Parser;

#[derive(Parser, Debug)] #[command(version, about)] struct Args { #[arg(short, long, default_value = "8080")] port: u16,

#[arg(short, long, env = "CONFIG_PATH")]
config: std::path::PathBuf,

}

  1. Performance Optimization

Zero-Cost Abstractions

  • Prefer iterators over manual index loops — they compile to identical machine code.

  • Use #[inline] for hot, small functions that cross crate boundaries.

  • Prefer stack allocation; move to heap (Box , Vec , Arc ) only when necessary.

SIMD

// Use std::simd (nightly) or portable-simd crate for explicit vectorization. // Profile first — LLVM auto-vectorizes most iterator chains. use std::simd::{f32x8, SimdFloat};

fn dot_product_simd(a: &[f32], b: &[f32]) -> f32 { a.chunks_exact(8) .zip(b.chunks_exact(8)) .map(|(a_chunk, b_chunk)| { let va = f32x8::from_slice(a_chunk); let vb = f32x8::from_slice(b_chunk); (va * vb).reduce_sum() }) .sum() }

Profiling

flamegraph (install: cargo install flamegraph)

cargo flamegraph --bin my-app

perf stat for CPU counters (Linux)

perf stat cargo run --release

heaptrack for heap allocation analysis (Linux)

heaptrack cargo run --release

criterion for micro-benchmarks

cargo bench

Allocation Awareness

// Preallocate when size is known. let mut v = Vec::with_capacity(expected_len);

// String building: use write! into a pre-allocated String. use std::fmt::Write; let mut s = String::with_capacity(256); write!(s, "id={}", id)?;

// Avoid format!() in hot paths — prefer direct write!() or push_str().

  1. Testing

Unit Tests

#[cfg(test)] mod tests { use super::*;

#[test]
fn parse_valid_config() {
    let cfg = Config::from_str("[server]\nport = 9000").unwrap();
    assert_eq!(cfg.port, 9000);
}

#[test]
fn parse_invalid_port_returns_error() {
    let result = Config::from_str("[server]\nport = -1");
    assert!(result.is_err());
}

}

Integration Tests

// tests/integration/server.rs — tests the full public API. #[tokio::test] async fn health_endpoint_returns_200() { let addr = spawn_test_server().await; let resp = reqwest::get(format!("http://{addr}/health")).await.unwrap(); assert_eq!(resp.status(), 200); }

Property-Based Testing (proptest)

use proptest::prelude::*;

proptest! { #[test] fn encode_decode_roundtrip(data: Vec<u8>) { let encoded = encode(&data); prop_assert_eq!(decode(&encoded).unwrap(), data); } }

Async Tests

#[tokio::test] async fn fetch_returns_data() { let mock = MockServer::start().await; Mock::given(method("GET")).respond_with(ResponseTemplate::new(200)).mount(&mock).await; let result = fetch(&mock.uri()).await; assert!(result.is_ok()); }

Test Conventions

  • Test one behavior per test function.

  • Use descriptive names: <function><scenario><expected> .

  • Mock only external I/O boundaries (HTTP, filesystem, database).

  • Run cargo test -- --nocapture for diagnostic output during development.

  • Run cargo nextest run for faster parallel test execution in CI.

  1. Unsafe Rust

When to Use

  • FFI boundaries (calling C functions, exporting to C).

  • Low-level memory mapping or SIMD intrinsics when safe abstractions cannot reach.

  • Performance-critical code where the safe alternative has measurable overhead (proved by profiling).

Guidelines

/// # Safety /// /// - ptr must be non-null and properly aligned for T. /// - The memory at ptr must be valid for len elements of type T. /// - The caller must ensure no other mutable references to the memory exist. pub unsafe fn from_raw_parts<T>(ptr: *const T, len: usize) -> &'static [T] { std::slice::from_raw_parts(ptr, len) }

  • Every unsafe block must have a // SAFETY: comment explaining why it is sound.

  • Minimize the scope of unsafe — wrap in a safe abstraction immediately.

  • Prefer unsafe fn over unsafe block inside a safe fn when the entire function is unsafe.

  • Use Miri (cargo +nightly miri test ) to detect undefined behavior in unsafe code.

  1. FFI and Interop

Exporting to C

#[no_mangle] pub extern "C" fn my_add(a: i32, b: i32) -> i32 { a + b }

Calling C from Rust

extern "C" { fn strlen(s: *const std::os::raw::c_char) -> usize; }

fn rust_strlen(s: &str) -> usize { let cstr = std::ffi::CString::new(s).expect("no null bytes"); // SAFETY: cstr is a valid, null-terminated C string. unsafe { strlen(cstr.as_ptr()) } }

cbindgen / bindgen

  • Use cbindgen to generate C headers from Rust public API.

  • Use bindgen to generate Rust bindings from C headers.

  • Always run through CI to detect API drift.

  1. Build System

Cargo Workspaces

Cargo.toml (workspace root)

[workspace] members = ["crates/core", "crates/server", "crates/cli"] resolver = "2"

[workspace.dependencies] tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] }

crates/core/Cargo.toml

[dependencies] tokio.workspace = true serde.workspace = true

Feature Flags

[features] default = ["std"] std = [] async = ["dep:tokio"] metrics = ["dep:prometheus"]

[dependencies] tokio = { version = "1", optional = true } prometheus = { version = "0.13", optional = true }

#[cfg(feature = "metrics")] fn record_latency(ms: u64) { /* ... */ }

#[cfg(not(feature = "metrics"))] fn record_latency(_ms: u64) {}

Build Scripts

// build.rs — runs before crate compilation. fn main() { // Rerun when proto files change. println!("cargo:rerun-if-changed=proto/"); // Link a system library. println!("cargo:rustc-link-lib=ssl"); }

Useful Cargo Commands

cargo build --release # optimized build cargo clippy -- -D warnings # lint (treat warnings as errors) cargo fmt --check # format check (CI) cargo doc --no-deps --open # generate and open docs cargo audit # check dependencies for CVEs cargo deny check # license + advisory checks cargo expand # show macro expansion

  1. Rust 1.75+ Features

Async fn in Traits (Stable 1.75)

// No longer requires #[async_trait] crate. trait Fetcher { async fn fetch(&self, url: &str) -> anyhow::Result<Bytes>; }

RPITIT — Return-Position Impl Trait in Traits (Stable 1.75)

trait Source { fn items(&self) -> impl Iterator<Item = &str>; }

let-else (Stable 1.65)

let Ok(value) = parse_value(raw) else { return Err(ConfigError::InvalidValue); };

std::io::ErrorKind — richer variants (ongoing)

use std::io::ErrorKind; match err.kind() { ErrorKind::NotFound => { /* ... / } ErrorKind::PermissionDenied => { / ... / } _ => { / ... */ } }

  1. Code Quality Checklist

Before marking Rust work complete:

  • cargo clippy -- -D warnings passes with zero warnings.

  • cargo fmt --check produces no changes.

  • cargo test (or cargo nextest run ) passes — all tests green.

  • All unsafe blocks have // SAFETY: comments.

  • Public API items have /// doc comments.

  • No .unwrap() in library code except tests.

  • Error types derive Debug

  • implement std::error::Error .
  • Async code does not block the executor thread.

  • Performance-critical paths benchmarked before and after changes.

Assigned Agents

This skill is used by:

  • developer — Rust feature implementation and bug fixes

  • code-reviewer — Rust-specific code review patterns

  • qa — Rust testing strategies (proptest, criterion, nextest)

Memory Protocol (MANDATORY)

Before starting: Read .claude/context/memory/learnings.md

After completing:

  • New Rust pattern discovered -> .claude/context/memory/learnings.md

  • Rust-specific issue or gotcha -> .claude/context/memory/issues.md

  • Architecture or crate selection decision -> .claude/context/memory/decisions.md

ASSUME INTERRUPTION: Your context may reset. If it's not in memory, it didn't happen.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

filesystem

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

slack-notifications

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

chrome-browser

No summary provided by upstream source.

Repository SourceNeeds Review