Skip to content

Rust Style and Best Practices

This page documents paiOS-specific Rust preferences on top of the official Rust style guide and Development Standards. Use it when making tradeoffs the style guide doesn’t mandate.

We prefer stack allocation when it doesn’t hurt maintainability, especially for embedded and performance-sensitive paths.

  • Stack: Fixed size at compile time, no allocator, cache-friendly. Use for concrete types and when the full type is known at compile time.
  • Heap (Box, Vec, etc.): Needed for dynamic size, shared ownership, or when you must pick an implementation at runtime. Use when stack isn’t practical.

On resource-constrained or embedded targets, prefer stack and fixed-capacity types (e.g. heapless) where possible; avoid unnecessary heap.

When the concrete type is known at compile time (e.g. composition root wired with #[cfg(feature = "...")]), prefer generics so implementations can live on the stack:

// Preferred when the composition root knows concrete types at compile time
pub struct Orchestrator<V, A, I, Api, P> {
vision: V,
audio: A,
inference: I,
api: Api,
peripherals: P,
}
impl<V, A, I, Api, P> Orchestrator<V, A, I, Api, P>
where
V: VisionInterface,
A: AudioInterface,
I: InferenceInterface,
Api: APIInterface,
P: PeripheralsInterface,
{
pub fn new(vision: V, audio: A, inference: I, api: Api, peripherals: P) -> Self {
Self { vision, audio, inference, api, peripherals }
}
}

Use Box<dyn Trait> when you must choose the implementation at runtime (e.g. from config or user input), or when threading many generic parameters would make the API unwieldy and the allocation cost is acceptable.

Situation Prefer Reason
Composition root, one concrete type per build (e.g. feature flags) Generics (stack) No heap, one monomorphized type per profile.
Runtime selection of implementation Box<dyn Trait> Single type, flexible.
Many dependencies, type list explodes Box<dyn Trait> Keep APIs and call sites manageable.

Library crates use crate-local thiserror enums for typed, documented errors. The binary (pai-engine) uses anyhow for app-level propagation with context.

// Illustrative example: the real implementation may differ.
// Library crate: typed error with thiserror
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("failed to read {path}: {source}")]
Io { path: PathBuf, #[source] source: std::io::Error },
#[error("failed to parse {format} config {path}: {message}")]
Parse { path: PathBuf, format: &'static str, message: String },
}
// Binary: anyhow for propagation and context
fn load() -> anyhow::Result<Config> {
let raw = std::fs::read_to_string("cfg.toml").context("read config")?;
toml::from_str(&raw).context("parse config")
}

Rules enforced via [workspace.lints]:

  • No bare unwrap() or expect() in library production code (clippy::unwrap_used, clippy::expect_used are denied). Use ?, unwrap_or_else, or add a #[allow] with a reason comment for genuinely infallible paths.
  • No panic! or todo! in production code (clippy::panic, clippy::todo are denied). Test modules use #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] at their top.
  • No unsafe without a #[allow(unsafe_code)] annotation and a safety comment (unsafe_code is denied workspace-wide).
  • Run cargo fmt from the repo root (settings in rustfmt.toml: max_width = 100, edition = "2021").
  • Aim for zero clippy warnings: cd engine && cargo clippy --all-targets --all-features -- -D warnings.
  • The workspace enforces a deny-list via [workspace.lints.clippy] (panic policy, correctness, suspicious); see engine/Cargo.toml.
  • See Development Standards for DoD and required checks.
  • Public and notable internal items: Use /// or //! doc comments. See Documentation Guide for Starlight sync.
  • For terminal snippets in rustdoc, prefer bash frame="none" where possible (see project Cursor rules).