围绕运行语义、异步行为与特性版本下限,为 Node 与浏览器场景调试与编写 JavaScript。
文档
Rust
为 Rust 编译错误、运行时故障、async 陷阱与 Cargo 构建问题提供结构化排查清单。
它能做什么
覆盖 Rust 语言与工具链:所有权、生命周期、trait、async、unsafe、FFI 与 Cargo。将编译错误码(E0382、E0502、E0277、E0038 等)映射到借用检查器逃生梯的具体动作,并提供按需深入的主题参考文档。包含指针/容器选型指引,以及边界处的溢出检查、错误类型、特性统一等规则。可用于借用检查器拒绝代码、future 无法 Send、Cargo 特性冲突、CI 才失败的构建等场景。
什么时候用它
- 解决 E0382 移动后使用或 E0502 多个可变借用冲突
- 定位阻塞调用或非 Send 守护跨过 .await 导致的 async 运行时停滞
- 诊断 cargo tree -d 暴露的同一 crate 多版本并存
- 配置 musl 静态链接或 wasm 目标的交叉编译
技能文档
User preferences and memory live in ~/Clawic/data/rust/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/rust/ or ~/clawic/rust/), move it to ~/Clawic/data/rust/.
When To Use
- Writing or reviewing Rust: functions, traits, error types, async code, public crate APIs
- Fighting the compiler: moves, borrows, lifetimes, trait bounds,
dyncompatibility, type inference - Diagnosing runtime failures: panics, deadlocks,
BorrowMutError, memory growth, segfaults throughunsafeor FFI - Cargo work: features, workspaces, lockfiles, MSRV, slow builds, CI-only failures, duplicated dependencies
- Targets beyond the host: cross-compilation, static musl binaries,
no_stdfirmware, wasm - Not for C++ (
cpp) or language-agnostic concurrency theory (async-patterns) — this is Rust the language and its toolchain
Quick Reference
| Situation | Play |
|---|---|
| Compiler says a value moved and you still need it | Borrow instead of consuming, or copy the small part you need out first — clone is rung 5, not rung 1 (→ Borrow Checker Escape Ladder) |
| Two mutable borrows refused on code that looks fine | The checker is field-sensitive on direct field access and field-insensitive through a method call — destructure or split_at_mut → ownership.md |
| A returned reference "does not live long enough" | Nothing borrowed from a frame outlives it: return owned data, or an index → lifetimes.md |
missing lifetime specifier on a signature | Two input references and no elision rule applies — name which one the output borrows from → lifetimes.md |
| "trait bound not satisfied" on a type that clearly implements it | Missing use for the trait, or &T supplied where T was required — read the required by chain bottom-up → traits.md |
| "future cannot be sent between threads safely" | A !Send value (std MutexGuard, Rc, RefCell ref) is alive across an .await → async.md |
| Async program stalls with no error and no progress | A blocking call is occupying a runtime worker → async.md |
| Program deadlocks | One thread locks the same lock twice, or two threads take two locks in opposite order → concurrency.md |
already borrowed: BorrowMutError at runtime | Overlapping RefCell borrows — the check you moved from compile time to runtime → ownership.md |
String vs &str, a panic on a string slice, UTF-8, or path handling | Byte indices are not characters and paths are not UTF-8 → strings-and-text.md |
A match is unreadable, non-exhaustive, or the type allows a state you never want | Model it as an enum, then let exhaustiveness find the sites → pattern-matching.md |
| Nothing in the logs explains what the service was doing | No subscriber, or context in strings instead of span fields → observability.md |
| "Where does this type go, and what should be public" | Module tree, pub(crate), re-exports, lib vs bin split → modules-and-layout.md |
Error names two identical-looking types (expected Uri, found Uri) | Semver-incompatible duplicate of one crate in the graph: cargo tree -d → cargo.md |
| Builds locally, fails in CI | Toolchain, lockfile, or feature-unification drift → cargo.md |
| Build is slow | cargo build --timings first; linker and codegen settings before restructuring code → cargo.md |
| Program is slow | Profile a release build with symbols; allocation and clone before algorithms → performance.md |
| Segfault, corrupted data, or nondeterministic "works on my machine" | Only reachable through unsafe or FFI — run it under Miri → unsafe.md |
| Anything else | Read the whole rustc output bottom-up including note: and help:, then rustc --explain E0xxx → compiler-errors.md |
Depth on demand: compiler-errors.md error code → real cause → first move · ownership.md moves, borrows, interior mutability · lifetimes.md elision, variance, 'static, HRTB · traits.md coherence, generics vs dyn, associated types · pattern-matching.md match ergonomics, let else, exhaustiveness, Option/Result combinators, modeling states · errors.md Result design, ?, panics, backtraces · async.md runtimes, Send bounds, cancellation · concurrency.md threads, locks, channels, atomics · strings-and-text.md String/&str, UTF-8, formatting, Cow, paths · collections.md Vec, maps, sets, queues, iterators · performance.md profiling, allocation, build settings · observability.md tracing, spans, metrics, panic hooks · cargo.md features, workspaces, lockfiles, build times · modules-and-layout.md module tree, visibility, lib/bin/tests layout, when to split a crate · testing.md layout, doctests, property tests, Miri, fuzzing · unsafe.md UB rules, raw pointers, safety comments · ffi.md C interop in both directions · macros.md macro_rules! and proc macros · api-design.md public APIs and semver hazards · serde.md derive traps and format edge cases · embedded-no-std.md firmware and no_std · wasm.md browser and WASI targets · cross-compilation.md target triples, musl, linkers, containers.
Core Rules
- Fix a borrow error at the lowest rung that compiles. The ladder (full version below) runs shorten → split → take → copy → clone → interior mutability → arena →
unsafe. Every rung down trades a compile-time guarantee for a runtime one; reaching forRc>against a rung-1 problem is the most common finding in Rust review. - Borrowed in arguments, owned in returns. A parameter typed
Stringforces every caller to allocate or surrender ownership;&stracceptsString,&String, and literals through deref coercion. Escape hatch:impl Intowhen the function genuinely stores the value. - Overflow checks are ON in debug and OFF in release by default.
i32::MAX + 1panics undercargo testand wraps silently in the shipped binary. For arithmetic on unbounded or untrusted input usechecked_*(returnsOption),saturating_*(clamps), orwrapping_*(documents intent) — or setoverflow-checks = truein[profile.release]and measure that cost instead of assuming it. - Measure release builds only. Debug builds skip optimization and keep overflow and bounds instrumentation: arithmetic-heavy loops commonly run 10-50× slower, so a debug timing ranks nothing. Profile with
[profile.release] debug = true— full optimization plus symbols, so the profiler can name frames. - One error type per boundary: enum outward, dynamic inward. A library returning
anyhow::Errordenies callers the ability to match on the failure; a binary with 40 enum variants pays for matching nobody does. Default: athiserrorenum in library crates,anyhow::Resultwith.context()in binaries (errors.md). mainprints errors withDebug, notDisplay.fn main() -> Result<(), MyError>on failure emitsError:and exits 1 — theDisplaymessage you wrote never appears. Either print it yourself andstd::process::exit(1), or return an error type whoseDebugrenders the full report (anyhow::Errordoes).- Bind every guard to a real name.
let _ = mutex.lock();releases the lock on that same line —_is not a binding, so the guard drops immediately;let _guard = mutex.lock()?;holds it to end of scope. Same trap withRefCellborrows and every other RAII handle. - Features are additive and unified across the entire graph.
default-features = falsein your manifest is a request, not a guarantee: if any crate in the tree enablestokio/full, the whole build getsfull. Find the enabler withcargo tree -e features -ibefore arguing with the manifest. - Pin the toolchain and the lockfile wherever the build must reproduce.
rust-toolchain.tomlpluscargo build --lockedin CI. Without--locked, CI resolves newer semver-compatible dependencies than yourCargo.lockand the difference arrives as a compile error nobody can reproduce locally.
Compiler Error Codes
The code names the subsystem that refused, which names the file to open. rustc --explain E0382 prints the canonical explanation; the column below is the first move that is usually right.
| Code | What it actually means | First move |
|---|---|---|
| E0382 | Use after move — consumed by a for loop, a by-value method, or a closure capture | Borrow (&v, .iter()) or copy the needed field out |
| E0499 | Two &mut to the same place alive at once | Split the borrow or shorten the first one (rungs 1-2) |
| E0502 | A &mut and a & overlap | Bind the read to a let before the mutation |
| E0505 / E0506 | Move out of, or assign to, something still borrowed | End the borrow first; std::mem::take if you need the value now |
| E0507 | Move out of a reference or an index expression | .clone(), .take(), mem::replace, or match by reference |
| E0515 | Returning a reference to a local | Return owned data or an index — the frame dies at return |
| E0597 | Borrowed value dropped too early | Hoist the owner's let above the borrower's |
| E0716 | Temporary dropped while borrowed | Bind the temporary: let s = make(); let r = &s; |
| E0106 | Missing lifetime specifier | Elision cannot choose among multiple inputs — name it (lifetimes.md) |
| E0308 | Mismatched types | Compare the two full paths in the message: usually &T vs T, or two versions of one crate (cargo.md) |
| E0277 | Trait bound not satisfied | Missing use of the trait, or a bound to add — read required by bottom-up |
| E0038 | Trait is not dyn compatible (formerly "object safe") | Generic method, Self return, or associated const — use generics or split the trait (traits.md) |
| E0599 | No method found | Trait not in scope, or the receiver does not deref to the type that has the method |
| E0072 | Recursive type has infinite size | Box the recursive field |
| E0117 / E0119 | Orphan rule / conflicting impls | Newtype wrapper; either the trait or the type must be local (traits.md) |
| E0282 / E0283 | Type annotations needed / ambiguous | Turbofish (collect::>()) or a typed binding |
| E0596 | Cannot borrow as mutable | Missing mut on the binding, or a &self method that needs &mut self |
| Anything else | Every code is stable and documented | rustc --explain E0xxx, then compiler-errors.md for the symptoms that have no code |
Pointer And Container Choice
Pick the weakest tool that expresses the need — every row costs something the row above does not.
| Need | Use | Cost or trap |
|---|---|---|
| Single owner, size known | T | The default; most code never leaves this row |
| Read-only view in a parameter | &T, &str, &[T] | No allocation, caller keeps ownership (rule 2) |
| Heap indirection, recursion, or a large value moved often | Box | One allocation; Box also erases the type |
| Borrow when possible, allocate only on modification | Cow<'a, str> | Removes "clone just in case" from hot paths (strings-and-text.md) |
| Shared ownership, one thread | Rc | Not Send; reference cycles leak — break them with Weak |
| Shared ownership, multiple threads | Arc | Atomic refcount; Arc by itself is still read-only |
| Mutate shared state, one thread | Rc> | Borrow checking moves to runtime: BorrowMutError panics |
Mutate one Copy value, one thread | Cell | Get/set only, no borrows, therefore no panic |
| Mutate shared state, multiple threads | Arc> | Poisoning on panic; lock ordering is yours to enforce |
| Many readers, rare writer | RwLock | Writer starvation and re-entrant read deadlock; measure before preferring it to Mutex |
| Initialize once, then read forever | OnceLock (rust >=1.70), LazyLock (rust >=1.80) | Replaces lazy_static and most once_cell uses with no dependency |
| A counter or a flag | AtomicUsize, AtomicBool | Cheapest sharing available; memory ordering is a real decision (concurrency.md) |
| Anything else | Start at T and &T | Move down a row only when the compiler proves you must |
Borrow Checker Escape Ladder
Ordered by cost. Take the first rung that compiles.
- Shorten the borrow. NLL ends a borrow at its last use, not at end of scope:
let n = v.len(); v.push(n);compiles wherev.push(v.len())does not. - Split the borrow.
let Foo { a, b } = &mut foo;yields independent&mutto distinct fields;slice::split_at_mutdoes the same for slices. A&mut selfmethod borrows the whole struct — that asymmetry explains most "obviously fine" rejections. - Take the value out.
let buf = std::mem::take(&mut self.buf);hands you an owned value and leavesDefault::default()behind; put it back when done.mem::replacewhen there is no sensible default. - Copy the small thing. An index, an id, a length: copying a
usizecosts nothing and removes the borrow entirely. Index-based access is a legitimate design, not a defeat. - Clone. Honest, and unobservable in setup, config, and error paths. A clone in a hot loop is a rung-1 problem in disguise; profile rather than guess which one you have.
- Interior mutability.
CellforCopy,RefCellfor the rest,Mutexacross threads. You now own the invariant the compiler used to check, and violations arrive as runtime panics. - Handles instead of pointers.
Vecplusu32indices. This is the standard answer for graphs and trees with back-edges;Rc>is the version that gets rewritten a year later. unsafe. Only with a// SAFETY:comment naming the invariant, and Miri in CI (unsafe.md).
Output Gates
Before emitting Rust code, verify:
- Every
unwrap/expectoutside tests is justified in a comment, or replaced by? - Public parameters take
&str,&[T], orimpl AsRefwherever the function only reads them - Errors follow rule 5: enum at library boundaries,
#[from]conversions, noBoxin a published signature - No
stdguard,Rc, or blocking call is alive across an.await - Arithmetic on untrusted or unbounded values uses
checked_*orsaturating_*, never debug-only overflow checks - Public enums and structs that may grow carry
#[non_exhaustive](api-design.md) - Each new
unsafeblock carries a// SAFETY:comment naming the invariant it upholds - The code passes
cargo clippy -- -D warnings, not merelycargo build
Configuration
User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/rust/config.yaml.
| Variable | Type | Default | Effect |
|---|---|---|---|
| edition | 2015 | 2018 | 2021 | 2024 | 2024 | Selects syntax and semantics in emitted code (if let temporary scope, reserved keywords, unsafe attribute forms) and which cargo fix --edition advice applies |
| msrv | text (version, e.g. 1.75) | none (current stable) | Gates which stabilized APIs and syntax may appear; suppresses suggestions newer than the floor and drives rust-version in Cargo.toml |
| async_runtime | tokio | async-std | smol | embassy | none | tokio | Chooses the runtime API in every async example and the spawn, timer, and IO types used from async.md |
| error_style | thiserror | anyhow | std-enum | box-dyn | auto | auto (thiserror in libraries, anyhow in binaries) | Sets the error type generated at each boundary and the ? conversion strategy in errors.md |
| unsafe_policy | forbid | reviewed | allow | reviewed | forbid emits #![forbid(unsafe_code)] and refuses ladder rung 8; reviewed requires a SAFETY comment plus a Miri step; allow drops the Miri requirement |
| lint_level | default | pedantic | deny-warnings | default | Controls which clippy lints the Output Gates enforce and whether generated CI fails on warnings |
| explanation_depth | fix-only | standard | teaching | standard | fix-only emits the corrected code and one line of why; standard adds the rung or rule that applies; teaching walks the compiler message and the alternatives that were rejected |
Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied:
- Tooling:
cargo testvscargo nextest, criterion vs a custom harness,cargovsjust/makewrappers, linker choice (lld,mold) — affects the commands intesting.mdandperformance.md - Conventions: module layout (
mod.rsvsname.rs), builder vs plain constructors, re-export policy at the crate root, doc-comment strictness — affectsmodules-and-layout.mdandapi-design.md - Platform: default target triple, libc flavor (gnu vs musl), hosted vs
no_std, wasm target — affectscross-compilation.md,embedded-no-std.md,wasm.md - Dependencies: appetite for adding a crate vs writing it, vetted-crate list, license and audit regime (
cargo deny,cargo audit) — affects every "add a crate" recommendation - Registries and sources: crates.io vs a private or mirrored registry,
cargo vendorplus source replacement, air-gapped or offline builds, git vs path dependencies for internal crates — affects the manifest,.cargo/config.toml, and every install command incargo.md - Work order and gates:
cargo checkin the edit loop vs full builds, whether clippy runs pre-commit or only in CI, whethercargo semver-checksgates release PRs, test-before-fix discipline — affects the command sequences inperformance.md,testing.md, andapi-design.md - Output format: depth of explanation vs a direct fix (see
explanation_depth), whole file vs diff, comment density in emitted code, whether to show the rejected alternatives — affects the shape of every answer - Safety posture: how proactively to surface panic, overflow, and unwrap risks vs answering only what was asked — affects Output Gates verbosity
- Cadence: dependency update and audit frequency, MSRV bump policy — affects the maintenance advice in
cargo.md
Traps
| Trap | Why it fails | Do instead |
|---|---|---|
let _ = mutex.lock(); to hold a lock | _ is not a binding; the guard drops on that line and the lock is free immediately | let _guard = mutex.lock().unwrap(); (rule 7) |
Rc> for a tree with parent pointers | Cycles never free, and every access can panic with BorrowMutError | Arena: Vec plus index handles (rung 7) |
.clone() inside a loop to appease the borrow checker | Turns an O(1) borrow into a copy per iteration | Fix the scope first (rungs 1-3), then clone deliberately |
#[derive(Clone)] on every generic type | Derive adds T: Clone to every type parameter, and that bound becomes part of your public API | Hand-write the impl, bounding only what actually needs it |
String parameters in a public API | Every caller allocates just to call you | &str, or impl Into when you store it (rule 2) |
mem::transmute to change a lifetime | Erases the exact property the compiler was checking; UB the moment the data dies | Restructure, or a raw pointer plus a SAFETY comment (unsafe.md) |
async fn on functions that never .await | Colors the whole call graph and wraps sync work in a state machine | async only where an await point exists (async.md) |
Benchmarking with cargo run or cargo test | Debug build: the number measures instrumentation (rule 4) | --release, criterion, and black_box on the inputs |
cargo clean to fix a slow build | Deletes the incremental cache and guarantees the slowest possible next build | cargo build --timings and read the graph (cargo.md) |
.iter().collect::>() just to loop over it | Allocates a vector to walk it once | Iterate the iterator; collect only when the collection is kept |
unwrap() on Mutex::lock() everywhere by reflex | Right most of the time, but hides that poisoning means another thread panicked mid-invariant | Handle or document the poison case (concurrency.md) |
| Adding lifetime parameters until it compiles | Produces signatures nobody can call and hides the real design error | Return owned data first; add lifetimes once you can state what borrows from what (lifetimes.md) |
Where Experts Disagree
anyhowin libraries. Purists never allow it: callers cannot match on the failure. Pragmatists accept it in application-adjacent crates nobody programs against. The boundary both sides accept: if any consumer must branch on the failure kind, the enum is not optional.- Clone-first vs lifetime-first. One camp writes owned data everywhere and optimizes later; the other treats every clone as debt. The line the evidence supports: clones in setup, config, and error paths are noise, clones inside hot loops are the first thing a profiler finds — so the disagreement only bites where you have measured.
unsafeavoidance.#![forbid(unsafe_code)]teams vs teams accepting reviewedunsafefor measured wins. Neither side acceptsunsafewithout a SAFETY comment and Miri coverage — that is not a tradeoff, it is a defect.- Generics vs
dyn. Monomorphization is faster per call, slower to compile, and multiplies code size per instantiated type;dyncosts an indirect call and keeps one copy. Default to generics in leaf code anddynat plugin-shaped boundaries, or wherever compile time or binary size is the measured constraint (performance.md).
Related Skills
More Clawic skills, get them at https://clawic.com/skills/rust (install if the user confirms):
cpp— when the other side of the FFI boundary is C++, or when comparing ownership modelsgo— the other systems language on the same team; different concurrency tradeoffsasync-patterns— language-agnostic concurrency and cancellation theory behindasync.mderror-handling— cross-language error design behind rule 5
Feedback
- If useful, star it: https://clawic.com/skills/rust
- Latest version: https://clawic.com/skills/rust
Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/rust.
常见问题
- 遇到借用错误时,这个技能如何决定是用 clone 还是先重构?
- 它使用借用检查器逃生梯,从第 1 级(缩短借用)到第 7 级(unsafe)排序。如果问题是第 1 级就能解决却直接用 clone(位于第 5 级),会被标记为 Rust 代码审查中最常见的问题,因为每下降一级都意味着把编译期保证换成运行期保证。
- future 无法跨线程 Send 时能帮上忙吗?
- 可以——它会指出 std::MutexGuard、Rc、RefCell 引用等 !Send 类型活过了 .await 点,并指引到 async.md 查看运行时与 Send 约束的处理方式。
- 除依赖解析外,它还覆盖哪些 Cargo 构建问题?
- Cargo 部分覆盖特性与特性统一、lockfile 漂移、构建缓慢(cargo build --timings)、仅在 CI 失败的构建、MSRV 以及 workspace 配置,相关细节在 cargo.md 中。
相关技能
为 Kotlin 代码提供正确性与惯用法诊断:空安全、协程、Flow、Compose 状态、Java 互操作与构建问题。
解读 TypeScript 类型错误,设计 API、tsconfig 与 .d.ts 的类型方案。
把 Flutter 框架异常读成具体修复动作,再把 release 包真正跑到设备上。
按文档化的 Python 规则手册和固定检查清单排查、审阅与编写代码。
针对 Vue 3 响应式、组件、路由和性能问题,依据控制台报错与代码形态给出具体修复方案。