What it is
mod-rand is a tiered randomness library for Rust. Three concrete
random sources, each chosen to be excellent for one threat model;
every operation you'd reach for — bounded integer ranges, byte
buffers, hex tokens, random or process-unique strings, shuffles,
samples, and weighted choice — exposed at the appropriate tier.
Zero runtime dependencies. MSRV 1.75. API locked under SemVer
for the entire 1.x line.
use Xoshiro256;
use ;
// Tier 1 — fast deterministic PRNG (xoshiro256**). Reproducible from a seed.
let mut rng = seed_from_u64;
let n: u64 = rng.next_u64;
let pct: u32 = rng.gen_range_u32;
let token: String = rng.gen_alphanumeric;
// Tier 2 — process-unique values. Tempdir names, request IDs.
let id: String = unique_name;
// Tier 3 — OS-backed cryptographic random. Tokens, keys, session IDs.
let secret: String = random_hex?;
# Ok::
What it covers
Every category of randomness operation that's in scope, at every tier where it makes sense.
| Operation | Tier 1 (PRNG) | Tier 2 (process-unique) | Tier 3 (crypto) |
|---|---|---|---|
Raw u32 / u64 draws |
next_u32 / next_u64 |
unique_u64 |
random_u32 / random_u64 |
Fill a &mut [u8] |
fill_bytes |
— | fill_bytes |
| Bounded integer ranges (every width) | gen_range_* |
range_* |
random_range_* |
| 128-bit integer ranges | gen_range_u128 / i128 |
range_u128 / i128 |
random_range_u128 / i128 |
Uniform f64 / f32 |
gen_f64 / gen_f32 / next_f64 |
— | — |
| Bernoulli trial | gen_bool(p) |
— | — |
| Hex strings | gen_hex(len) |
unique_hex(len) / random_hex_string(len) |
random_hex(bytes) / random_hex_string(len) |
| Alphanumeric strings | gen_alphanumeric |
unique_name / random_alphanumeric |
random_alphanumeric |
| Arbitrary-charset strings | gen_string |
random_string |
random_string |
| Base32 / base58 / base64 / URL-safe | via charsets::* |
via charsets::* |
via charsets::* |
Shuffle a &mut [T] |
shuffle |
— | shuffle |
Sample k without replacement |
sample |
— | — |
| Weighted choice | weighted_choice / weighted_index |
— | — |
Stream splitting (2^128 / 2^192) |
jump / long_jump |
— | — |
Tier 1 is no_std-compatible for everything that doesn't require
allocation. Tier 2 and Tier 3 require std (default feature).
The three tiers
Pick by threat model, not by speed.
| Tier | Algorithm | Use case | Crypto-safe |
|---|---|---|---|
| 1 | xoshiro256** (splitmix64-seeded) | Simulation, fixtures, shuffling, non-security strings | No |
| 2 | PID + nanos + atomic counter + Stafford-mix-13 | Tempdir names, request IDs, log correlation IDs | No |
| 3 | OS syscall (getrandom / BCryptGenRandom / getentropy) |
Tokens, keys, session IDs, password salts | Yes |
| Question | Tier |
|---|---|
| Is the value reproducible from a seed? | 1 |
| Must two calls inside one process always differ? | 2 |
| Could an attacker benefit from predicting it? | 3 |
If the answer to the third question is "yes" — even maybe — use Tier 3.
Bounded ranges — every integer width
Every tier exposes gen_range_* / range_* / random_range_*
methods using Rust's range syntax. .. is half-open, ..= is
inclusive; the caller's choice is the contract. All bounded methods
use Daniel Lemire's "Nearly Divisionless" rejection sampling, so
output is uniformly distributed — no modulo bias.
Widths covered: u8, u16, u32, u64, u128, usize, and the
signed counterparts.
use Xoshiro256;
use ;
let mut rng = seed_from_u64;
let byte: u8 = rng.gen_range_inclusive_u8;
let pct: u32 = rng.gen_range_u32;
let big: u128 = rng.gen_range_u128;
let die: u32 = rng.gen_range_inclusive_u32; // classic d6
let id = range_inclusive_u32;
let secret = random_range_inclusive_u64?;
# Ok::
The 128-bit ranges generalise Lemire's algorithm to a 256-bit
intermediate via two u64 draws; the full-width inclusive
0..=u128::MAX is special-cased to a raw 128-bit reinterpret.
String generation
Custom charset, or a pick from the standard ones in
mod_rand::charsets.
use Xoshiro256;
use ;
let mut rng = seed_from_u64;
// Tier 1 — deterministic from the seed.
let id = rng.gen_alphanumeric;
let hex = rng.gen_hex;
let pin = rng.gen_numeric;
// Custom charset — pick from charsets:: or supply your own &[u8].
let url = rng.gen_string;
let coin = rng.gen_string;
// Tier 2 — `unique_*` guarantees per-call distinctness;
// `random_*` guarantees a uniform distribution. Both ship.
let name = unique_name; // distinct across calls
let token = random_alphanumeric; // uniform distribution
// Tier 3 — cryptographic.
let key = random_alphanumeric?;
# Ok::
Charset constants in mod_rand::charsets: ALPHANUMERIC, ALPHA,
ALPHA_LOWER, ALPHA_UPPER, NUMERIC, HEX_LOWER, HEX_UPPER,
URL_SAFE (RFC 4648 §5), BASE58 (Bitcoin alphabet), BASE64
(RFC 4648 §4). Custom charsets are accepted as &[u8]; non-ASCII
charsets are rejected (panic on Tier 1 / Tier 2; InvalidInput on
Tier 3) so the returned String is always valid UTF-8.
Collection operations
Fisher-Yates shuffle, sample-without-replacement, and weighted choice. Available on Tier 1 (deterministic, reproducible) and Tier 3 shuffle (cryptographic).
use Xoshiro256;
use tier3;
let mut rng = seed_from_u64;
// Shuffle in place. Uniform over all n! permutations.
let mut hand: = .collect;
rng.shuffle;
// Sample k references without replacement, in source-slice order.
let pool: = vec!;
let chosen: = rng.sample;
// Weighted choice over a probability vector.
let items = ;
let weights = ;
let drop = rng.weighted_choice.unwrap;
// Cryptographic shuffle — every permutation drawn from the OS CSPRNG.
let mut deck: = .collect;
shuffle?;
# Ok::
Performance
Measured on a Ryzen 9 9950X3D, Windows 11 (cargo bench):
Tier 1 — xoshiro256** (target ~1 ns/u64)
| op | time |
|---|---|
next_u64 |
~0.6 ns |
next_f64 / gen_f64 |
~0.7 ns |
gen_f32 |
~0.7 ns |
gen_bool(0.5) |
~0.8 ns |
fill_bytes(32) |
~2.0 ns |
gen_range_u64(0..100) |
~0.8 ns |
gen_range_inclusive_u32(1..=6) (d6) |
~0.9 ns |
gen_range_u8(0..100) |
~0.8 ns |
gen_range_u128(0..u128::MAX/2) |
~2.2 ns |
gen_range_inclusive_u128(0..=u128::MAX) |
~1.1 ns |
gen_range_u64(0..⅔·u64::MAX) worst-case |
~5.9 ns |
gen_alphanumeric(16) |
~58 ns |
gen_alphanumeric(64) |
~115 ns |
gen_hex(32) |
~63 ns |
shuffle(10) |
~10 ns |
shuffle(100) |
~104 ns |
shuffle(1000) |
~953 ns |
shuffle(10000) |
~9.5 µs |
sample(k=10, n=1000) |
~905 ns |
weighted_index(10 weights) |
~19 ns |
weighted_index(100 weights) |
~172 ns |
weighted_index(1000 weights) |
~1.1 µs |
Tier 2 — process-unique (dominated by SystemTime::now())
| op | time |
|---|---|
unique_u64 |
~21 ns |
unique_name(8) |
~42 ns |
unique_hex(16) |
~47 ns |
range_u64(0..100) |
~23 ns |
range_u128(0..u128::MAX/2) |
~45 ns |
random_alphanumeric(16) |
~420 ns |
Tier 3 — OS CSPRNG (Windows / BCryptGenRandom)
| op | time |
|---|---|
random_u64 |
~35 ns |
fill_bytes(32) |
~53 ns |
fill_bytes(1024) |
~224 ns |
random_hex(16) |
~97 ns |
random_range_u64(0..100) |
~35 ns |
random_range_u128(0..u128::MAX/2) |
~48 ns |
random_alphanumeric(16) |
~628 ns |
random_alphanumeric(64) |
~2.4 µs |
shuffle(52) (deck of cards) |
~1.9 µs |
shuffle(1000) |
~38 µs |
Tier 1 beats its 1 ns/u64 target. Linux / macOS Tier 3 numbers are kernel-dependent; expect 100–500 ns per call on commodity hardware.
Why this library exists
- Zero dependencies. No
rand, nogetrandomcrate, nolibc. Juststd. Tier 1 works inno_std. - Explicit threat model. You pick the tier; you know what guarantees you're getting.
- Lower MSRV than the alternatives.
1.75. Many random crates in the ecosystem require1.85+. - Determinism contract on Tier 1. Locked byte-for-byte under
SemVer for the entire
1.xline — seedocs/STABILITY.md. - One library, three tiers, every operation. Numbers, bytes,
strings, tokens, shuffle, sample, weighted choice. You don't need
rand+rand_distr+fastrand+ a hand-rolled crypto random.
How it compares
Honest, side-by-side. The full discussion (including non-strengths)
is in docs/COMPARISON.md.
mod-rand |
rand + rand_distr |
fastrand |
nanorand |
|
|---|---|---|---|---|
| Fast PRNG | ✓ Tier 1 | ✓ many | ✓ | ✓ many |
| OS cryptographic random | ✓ Tier 3 | ✓ via getrandom |
✗ | ✓ ChaCha20 (userspace) |
| Process-unique stream | ✓ Tier 2 | partial (build yourself) | ✗ | ✗ |
| 128-bit integer ranges | ✓ | ✓ | ✗ | ✗ |
| String generation in API | ✓ | via Alphanumeric |
✗ | ✗ |
| Shuffle / sample / weighted | ✓ | ✓ | shuffle | shuffle |
| Named distributions | ✗ | ✓ (rand_distr) |
✗ | ✗ |
Generic Rng trait |
✗ | ✓ | ✗ | partial |
| Runtime crate dependencies | 0 | several (getrandom, etc.) |
0+ | 0+ |
| MSRV (at time of writing) | 1.75 | higher | 1.61 | 1.65 |
Use mod-rand when its tier model fits and you want zero deps + a
locked API. Reach for rand if you need named distributions or
generic-trait ecosystem interop.
Feature flags
[]
= { = "1.0", = false } # tier1 only, no_std
= { = "1.0", = ["tier2"] } # + tier2
= "1.0" # all three tiers (default)
| Feature | Effect |
|---|---|
std |
Required by tier2, tier3, and string generation on tier1 |
tier2 |
Enables mod_rand::tier2 |
tier3 |
Enables mod_rand::tier3 |
Default features: ["std", "tier2", "tier3"].
Documentation
- docs/API.md — hand-written API reference, tier by tier.
- docs/STABILITY.md — SemVer policy, determinism contract, deprecation rules.
- docs/COMPARISON.md — side-by-side with
rand,fastrand,nanorand. - docs/API-FREEZE-AUDIT.md — full manifest
of every
1.0public symbol. - CHANGELOG.md — release history.
Minimum supported Rust version
1.75 — pinned in Cargo.toml, verified by CI on every change. An
MSRV bump is a minor-version bump minimum.
License
Apache-2.0. See LICENSE.