Rust walkthrough: adding drift to a project
Full step-by-step example, using the repo's own examples/minimal-rust — a real, working crate that intentionally trips every rule once, doubling as this walkthrough's running example and as a manual smoke check for the tool itself.
1. Prerequisites
- A Rust toolchain (
rustup). cargo-dylintanddylint-link, the dylint frameworkcrates/drift-lintis built on:
cargo install cargo-dylint dylint-link
Clone drift and look at the demo crate:
git clone https://github.com/FelixMiddelhoff/drift
cat drift/examples/minimal-rust/src/main.rs
2. First run: every unconditional rule at once
cd drift
cargo dylint --path crates/drift-lint -p minimal-rust
Five of the six rules — hashmap_iter, unseeded_rng, wallclock_read, unordered_parallelism, usize_in_hashed_state — need no configuration and fire immediately (real output from this exact command, not paraphrased):
warning: usize/isize field on a struct deriving Hash — width varies across platforms
--> examples\minimal-rust\src\main.rs:20:5
= help: use a fixed-width integer type (u32/u64/i32/i64) instead
warning: iterating a HashMap/HashSet — order is not guaranteed stable across peers
--> examples\minimal-rust\src\main.rs:26:25
= help: use a BTreeMap/BTreeSet, or sort the keys before iterating, if this feeds simulated state
warning: call to an OS-entropy-seeded RNG source
--> examples\minimal-rust\src\main.rs:31:19
= help: use an explicit, tracked seed (e.g. StdRng::seed_from_u64) fed by your simulation's deterministic seed
warning: wall-clock read — not guaranteed the same across peers
--> examples\minimal-rust\src\main.rs:33:18
= help: use your simulation's own deterministic tick counter if this feeds simulated state
warning: rayon parallel iteration — result order is scheduler-dependent
--> examples\minimal-rust\src\main.rs:38:23
= help: confirm the terminal reduction is order-independent (commutative), or suppress if already confirmed
Reading examples/minimal-rust/src/main.rs alongside the output: each warning points at exactly the line the demo crate built to trigger it — a #[derive(Hash)] struct Unit { id: usize }, a HashMap::iter(), rand::random::<u32>(), Instant::now(), and values.par_iter().sum().
3. Fixing a real finding
Take the hashmap_iter hit. Iterating a HashMap directly means two peers running the same logic can see entries in a different order — if that order ever feeds simulated state (accumulating in sequence, picking a "first" element, anything order-sensitive), that's a desync waiting to happen.
#![allow(unused)] fn main() { // Before — flagged: iteration order not guaranteed let units: HashMap<u32, u32> = HashMap::new(); for (_id, _unit) in units.iter() { /* ... */ } // After — deterministic, sorted by key first let mut entries: Vec<_> = units.iter().collect(); entries.sort_by_key(|(id, _)| **id); for (_id, _unit) in entries { /* ... */ } }
drift::hashmap_iter recognizes this collect-then-sort pattern and won't re-flag it — confirmed by the rule's own test fixture, not assumed.
4. Enabling float_outside_fixed_step
The sixth rule is opt-in — it does nothing until a dylint.toml at your workspace root configures tick_reachable_roots:
# dylint.toml
[drift-lint]
tick_reachable_roots = ["main"]
This repo's own root dylint.toml sets exactly this, which is why minimal-rust's tick() function shows up as a sixth warning in the same run above:
warning: non-associative float arithmetic reachable from simulation state
--> examples\minimal-rust\src\main.rs:46:26
46 | let _next_position = 1.0_f32 + delta * 9.8; // drift::float_outside_fixed_step
= help: confirm this runs under a fixed-step/fixed-point discipline, or list the enclosing function in dylint.toml's fixed_step_functions
Real, surprising behavior worth knowing before you configure this: setting tick_reachable_roots at all scopes every rule except usize_in_hashed_state to reachability from those roots, not just float_outside_fixed_step. If you point tick_reachable_roots at a narrow function, the other five rules can silently stop firing on code that isn't reachable from it — this repo's own demo roots at main specifically so every rule's own demo call site stays reachable in one pass. Pick a root that's genuinely upstream of everything you want scanned, not just your physics tick.
tick_reachable_roots = ["simulation::tick"]
fixed_step_functions = ["physics::integrate"] # exempt a verified fixed-step integrator
5. Suppressing a specific finding
#![allow(unused)] fn main() { #[allow(drift_hashmap_iter)] fn known_safe() { // ... } }
Every rule has its own attribute name (drift_unseeded_rng, drift_wallclock_read, drift_unordered_parallelism, drift_usize_in_hashed_state, drift_float_outside_fixed_step) — same pattern as any other rustc lint.
6. Wiring into CI
jobs:
drift-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo install cargo-dylint dylint-link
- run: cargo dylint --path crates/drift-lint --workspace
cargo dylint exits non-zero when there are findings — no extra plumbing needed to fail the job. Commit your dylint.toml (if you use tick_reachable_roots) alongside your own project so it evolves with your simulation code, same as the Unreal walkthrough's config.toml.
Real-world validation
Every rule above has also been run against veloren/veloren's common crate group — a real, shipped open-source multiplayer voxel RPG, not a toy — with genuine findings: real rand::random() calls inside combat-state code, a real Instant::now() field in the simulation's own State struct, real HashMap iteration in an ECS aura system, and a usize field caught through a type alias (pub type Species = usize;), confirming type resolution goes through rustc's own type information, not just textual matching. See each rule's own entry in the rule catalog for the exact file:line results.
Known limitations
See the rule catalog for the full, honest list per rule. The short version: reachability is a direct, intra-crate call graph only — dyn Trait/function-pointer call targets aren't resolvable via a plain HIR walk, so they don't add an edge. This is a deliberate under-approximation (a missed rule firing is judged less damaging to trust in the tool than a wrong one) — see the rule catalog for why.