Random in Rust with stdt

          __      .___ __   
  _______/  |_  __| _//  |_ 
 /  ___/\   __\/ __ |\   __\
 \___ \  |  | / /_/ | |  |  
/____  > |__| \____ | |__|  
     \/ 

stdt on GitHub |

stdt on crates.io

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_iter allocates 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.

stdt on GitHub |

stdt on crates.io


You'll only receive email when they publish something new.

More from GSLF
All posts