Files
skills-public/code-languages/rust.md
T
BluscreamandClaude Opus 5 58525056a1 docs: use <repo>/<rel path> links throughout, document the convention
All cross-skill links now spell out the repository: skills-public/foo.md,
skills-private/bar.md. These resolve from the shared parent directory, so one
spelling works from either repo and from code-languages/ subdirectories —
replacing the previous mix of ./, ../ and absolute file:// URLs. Link labels
normalised to match. Convention added to the README conventions list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:44:23 +02:00

8.0 KiB

name, description
name description
rust 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. Everything there applies; this adds Rust specifics.


1. The Gate

Nothing is "done" until all of these are clean:

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

# 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"
# 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 — but when a lint is genuinely wrong for a piece of code, allow it at the narrowest scope that covers it (the expression, the function, the module) with a comment saying why:
    // The FFI callback signature is fixed by the C API; we cannot take fewer arguments.
    #[allow(clippy::too_many_arguments)]
    extern "C" fn on_event(/* … */) { }
    
  • A workspace-wide allow in [workspace.lints] is for the rare case where the lint is wrong for the entire codebase — not a shortcut for one stubborn call site. It silences the rule in code nobody has written yet.
  • 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/<name>-core/     # all reusable logic, no UI, no framework deps
    crates/<name>-proto/    # wire types & protocol parsing, dependency-light
    src/                    # or crates/<name>-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:

distrobox enter <container> -- 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.


  • coding-dev.md — the general rules this extends.
  • c-cpp.md — when bindgen/FFI crosses into a C library.