Random in Rust with stdt
April 18, 2026•651 words
__ .___ __
_______/ |_ __| _// |_
/ ___/\ __\/ __ |\ __\
\___ \ | | / /_/ | | |
/____ > |__| \____ | |__|
\/
The stdt Rust library (v0.0.5) now provides minimal, non-cryptographic PRNG utilities for generating integers, floating-point values, and random selections. Seeds are derived from the current time in nanoseconds combined with a hashed thread ID. Inputs are mixed with a SplitMix-inspired multiply-rotate scheme, and the core PRNG is xorshift64*, chosen for speed and a tiny state. Not suitable for cryptographic use, but small state, zero dependencies, no unsafe, and predictable costs per call..
The seeding path combines now_ns() and a hashed thread_id() via the mixer function. The mixer performs a multiply–xor–rotate–multiply sequence using constants aligned with SplitMix-style diffusion.
fn mixer(h: &mut u64, x: u64) {
*h ^= x.wrapping_mul(0x9E37_79B9_7F4A_7C15);
*h = (*h).rotate_left(27).wrapping_mul(0x94D0_49BB_1331_11EB);
}
The PRNG core itself is the standard xorshift64*. This gives a period of 264 − 1 for nonzero state, and adequate bit-mixing for non-adversarial use.
fn prng(state: &mut u64) -> u64 {
let mut x = *state;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
*state = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
Integer generation handles signed, inclusive intervals correctly by mapping i128 to a monotone u128 line through a sign-bit XOR. The decision to force the state away from zero prevents the well-known xorshift zero-state lockup.
fn generator_u128(thread_id: u64, ts_ns: u128) -> u128 {
let mut h: u64 = 0x9E37_79B9_7F4A_7C15;
mixer(&mut h, thread_id);
mixer(&mut h, (ts_ns >> 64) as u64);
mixer(&mut h, ts_ns as u64);
let mut state = if h == 0 { 1 } else { h };
// Extend to 16 byte
let mut bytes = [0u8; 16];
bytes[..8].copy_from_slice(&prng(&mut state).to_be_bytes());
bytes[8..].copy_from_slice(&prng(&mut state).to_be_bytes());
u128::from_be_bytes(bytes)
}
Integer sampling uses start + (seed % width) and maps back with a sign mask. Floating sampling honors f64 precision by extracting the top 53 bits from the u128 seed, forming a unit value in [0, 1) as mant, and then scaling: start + (end - start) * unit. This gives the best possible granularity a f64 can represent.
The selection helpers are straightforward: choose samples an index over [0, n−1] and returns a reference, while choose_iter materializes the iterable into a Vec and selects, which is constant time on a Vec and keeps the implementation simple for arbitrary IntoIterator.
The design avoids global state or synchronization; seeding is per call, and the PRNG body is a handful of ALU ops. There are weaknesses, but they are not material to the stated scope.
- First, the seeding relies on time and thread identity, so calls occurring on the same thread within the same effective timestamp can collide; many systems do not deliver true nanosecond granularity, so this kind of duplicates are possible.
- Second, integer sampling uses a modulo reduction
seed % width`which introduces small bias unless the width divides the domain; this is negligible for casual utilities but should be acknowledged. - Third, the u128 seed is built from two successive outputs of a single 64-bit state; it does not uniformly cover 2128, which is fine here but worth noting.
- Fourth, the seeding path is predictably derivable to an attacker (time plus hashed thread id via DefaultHasher), consistent with the explicit disclaimer “Not cryptographically secure.”
- Fifth,
choose_iterallocates a vector; for very large or streaming sources, a one-pass reservoir sampler would be more memory-friendly.
If requirements grow beyond “minimal, non-crypto,” consider:
- a thread-local PRNG state seeded once per thread to remove timestamp/hash overhead on hot paths and eliminate same-timestamp collisions;
- replacing modulo mapping with a multiply-high technique to reduce bias without rejection; and
- switching choose_iter to reservoir sampling to avoid allocation while still returning exactly one item when available.