- all nine files: trailing "Related" sections removed, their unique links inlined where the topic actually arises (FFI boundary -> c-cpp, embedded frontend -> js-typescript, build scripts -> bash, etc.) - README conventions: rule 2 mandated a Related Skills section, which now contradicts practice — replaced with inline-linking and one-home-per-fact rules. The <repo>/<rel path> rule was appended after the wrong section last commit; moved into the list where it belongs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.9 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
--workspaceis 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-targetsadditionally covers tests, benches, and examples — which are also code.
cargo fmtbefore every commit. No hand-formatting, ever.cargo docmatters: 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_linesis the mechanical enforcement of the 100-line function limit. A function that trips it wants extracting, not an allow.
3. Errors & Panics
ResultandOptioneverywhere.thiserrorfor library error enums;anyhowis acceptable at the application/binary boundary only.- No
unwrap()orexpect()outside tests andmain. 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 (see c-cpp.md for the other side of that boundary), 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
futuresstreams;async_traitwhere 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_blockingfor 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
Streamimpl 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
cfgleaves 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
pubon 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
f32for UI/geometry when the framework's native unit isf32— mixing inf64produces 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 intests/. cargo test --workspace— plus--releasewhen 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.