--- name: rust description: >- Use this skill when writing, building, linting, testing, or releasing Rust code — including workspace lint configuration, clippy/fmt gates, error handling with Result/thiserror, unsafe policy, async patterns, and cargo workspace/submodule layout. --- # Rust — Rules & Preferences Extends [`../coding-dev.md`](../coding-dev.md). Everything there applies; this adds Rust specifics. --- ## 1. The Gate Nothing is "done" until all of these are clean: ```bash cargo fmt --all --check cargo clippy --workspace --all-targets --all-features cargo test --workspace cargo doc --workspace --no-deps cargo build --release ``` > [!IMPORTANT] > **`--workspace` is not optional.** Without it only the root package is checked and every > member crate's warnings stay invisible. This has hidden real warnings for weeks at a time. > `--all-targets` additionally covers tests, benches, and examples — which are also code. - `cargo fmt` before every commit. **No hand-formatting**, ever. - `cargo doc` matters: a broken intra-doc link is a reference to something that no longer exists, which is worth knowing about. - Run the project's build script (`./scripts/build.sh`) if it has one — it wires these together plus size checks and a smoke run. --- ## 2. Lints — Pedantic On, Configured In One Place ```toml # Cargo.toml (workspace root) [workspace.lints.clippy] pedantic = { level = "warn", priority = -1 } too_many_lines = "warn" unwrap_used = "warn" [workspace.lints.rust] unsafe_code = "forbid" ``` ```toml # every member crate [lints] workspace = true ``` > [!CAUTION] > **Lint config lives in `[workspace.lints]` only.** A package that repeats the configuration > carries a second copy that drifts — which is how a binary once kept reading its own stale > allows while the workspace's were being actively edited. - **Never `#[allow]` on a whim.** If a lint is genuinely wrong for the codebase, allow it **workspace-wide** with a comment explaining why, so the decision is visible. - **Re-test every `#[allow]` periodically.** If removing one produces no warning, delete it. - `too_many_lines` is the mechanical enforcement of the 100-line function limit. A function that trips it wants extracting, not an allow. --- ## 3. Errors & Panics - **`Result` and `Option` everywhere.** `thiserror` for library error enums; `anyhow` is acceptable at the application/binary boundary only. - **No `unwrap()` or `expect()` outside tests and `main`.** Anything that can be absent at runtime — a device, a file, a network peer, an env var — must be handled. Hardware and network resources appear and disappear while the program runs. - No `panic!` as flow control. A library never panics on input it was designed to receive. - Never `?`-swallow context — attach it (`.context(...)` / a typed variant) so the error says where it came from. --- ## 4. `unsafe` **Forbidden by default**, enforced in `Cargo.toml` with `unsafe_code = "forbid"`. If an FFI binding genuinely requires it, it goes behind a **safe wrapper in exactly one module**, with a `// SAFETY:` comment on every block stating the invariant being upheld. `unsafe` scattered through call sites is not acceptable. --- ## 5. Dependencies & Workspace Layout Follow the global ordering (reuse → fork → write). In Rust specifically: - Search crates.io and check maintenance status before adding. Prefer pure-Rust crates over ones requiring a C toolchain when the difference is not material — a C dependency turns a one-command build into a container requirement. - **Pin precisely when it matters.** If you need an API that only exists on a git revision, pin the revision and **write down why** — a future reader will otherwise "helpfully" bump it to the crates.io release that lacks the API. - **Reusable logic goes in its own crate**, published separately, with the application as a consumer. The workspace layout that follows from this: ``` crates/-core/ # all reusable logic, no UI, no framework deps crates/-proto/ # wire types & protocol parsing, dependency-light src/ # or crates/-ui — presentation only ``` - **Submodule hygiene**: commit and push inside the submodule crate first, then commit the updated SHA pointer in the parent. Match each repo's own commit-trailer and identity conventions — a public library repo and a private app repo often differ deliberately. --- ## 6. Async - Tokio runtime with `futures` streams; `async_trait` where trait methods must be async. - **Model new flows as streams or async methods**, matching what the codebase already does. - **Never block in async context.** Use `spawn_blocking` for genuinely blocking work. - Subscribe to event streams; never poll on a timer (see the global events-over-polling rule). - Don't short-circuit a hand-written `Stream` impl that manually pumps backends — it usually encodes ordering guarantees that are not obvious from the signature. --- ## 7. Feature Flags & `cfg` - Feature flags declared in the root `Cargo.toml`. - **Gate at module level, not per function** — per-function `cfg` leaves a trail of empty stubs and makes the real platform differences hard to see. - Adding a platform backend: declare the feature, create the gated module, and **log which backend was selected at startup**. Silent backend selection is very hard to debug remotely. - Use `build.rs`-exported configs for conditions that aren't plain features. --- ## 8. Style Details - `unsafe`-free, `unwrap`-free, and formatted — see above. - **No `pub` on anything that doesn't need to cross a module boundary.** Default to private and widen deliberately. - Max ~5 parameters; past that, pass a struct. - Prefer `f32` for UI/geometry when the framework's native unit is `f32` — mixing in `f64` produces a stream of casts and rounding inconsistencies. - Derive rather than hand-implement (`Debug`, `Clone`, `Default`, `PartialEq`) unless the hand implementation is doing something a derive cannot. - `#[non_exhaustive]` on public enums/structs that are likely to grow. --- ## 9. Testing - Unit tests inline in `#[cfg(test)] mod tests`; integration tests in `tests/`. - `cargo test --workspace` — plus `--release` when timing or optimization matters to the behavior under test. - **Regression test every fixed bug.** - A dummy/mock backend that exercises the full pipeline without real hardware is worth building early — it is the only way most device code gets tested at all. - Never point test output at real hardware. Route test signal to a virtual/null endpoint and **verify the route resolved** — most tools silently fall back to the system default when the requested target does not exist, which sends your test straight at the user. --- ## 10. Building in Containers When the crate needs system headers the host does not have (audio, graphics, webkit), build inside the project's designated container. Chain `cd` inside the same command string — entering a container resets the working directory: ```bash distrobox enter -- bash -c "cd /path/to/project && cargo build --release" ``` Prefer the project's `scripts/build.sh` over a hand-typed invocation; it also handles job throttling so a release build does not saturate the machine. **Split so that as little as possible needs the container**: a `-core` crate with no GUI dependencies builds and tests directly on the host, which is seconds instead of minutes. --- ## 11. Related - [`../coding-dev.md`](../coding-dev.md) — the general rules this extends. - [`c-cpp.md`](./c-cpp.md) — when bindgen/FFI crosses into a C library.