Casper's Blog – Why I forked rand
Casper's Blog
Thoughts on code, systems, and craft.
GitHub
Why I forked rand
July 27, 2026 — by Casper — rust
Rust's rand crate is the default choice for randomness in the Rust ecosystem.<br>My experience with rand has frustrating papercuts: many everyday operations are hard to discover!
That led me to fork and build urandom:<br>a randomness crate with a smaller public and implementation surface, organized around a specific user experience.
To quote:
“Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.” ― Antoine de Saint-Exupéry
One place to look
With rand, useful operations come from several traits:
use rand::RngExt;<br>use rand::seq::{IndexedRandom, SliceRandom};
let mut rng = rand::rng();
let roll: u32 = rng.random_range(1..=6);<br>let color = ["red", "green", "blue"].choose(&mut rng);
let mut numbers: Vec = (1..=10).collect();<br>numbers.shuffle(&mut rng);
Rand 0.10 also provides root-level helpers such as rand::random_range for one-off calls.<br>Once you keep an RNG handle, or need sequence operations such as<br>choosing and shuffling, the method API still comes from several traits.
The prelude shortens the imports, but you still need to know to which types the extension methods apply.<br>IDE completion does not help much when the method may belong to the RNG, the slice, the iterator or elsewhere.<br>Discovering the right method often requires already knowing which trait provides it on which type.
urandom puts its high-level consumer API on one Random wrapper struct:
let mut rand = urandom::new();<br>// rand: urandom::Random
let roll: u32 = rand.uniform(1..=6);<br>let color = rand.choose(&["red", "green", "blue"]);
let mut numbers: Vec = (1..=10).collect();<br>rand.shuffle(&mut numbers);
If you have a Random, editor completion shows you random, uniform, chance, choose, shuffle, sample, and more.<br>These are inherent methods, so there is no high-level extension trait to find or import.
A sealed Rng
rand treats its low-level RNG traits as public extension points. urandom does not:<br>its Rng trait is sealed, and the supported generators are selected and implemented inside the crate.
You cannot plug an arbitrary generator into Random.
A new generator requires a change to urandom.
I kept coming back to a core question: what material benefit does customizing the generator provide?
If the goal is a better algorithm, Xoshiro256 and ChaCha are established<br>default choices for their roles today, and recommendations change slowly.<br>If that changes, urandom can adopt a better choice in a future major release.
If the goal is compatibility with another project, programming language, legacy<br>algorithm, specialized hardware, or simulation-specific generator, matching its<br>generator alone is not enough. Uniform sampling, shuffling, and other<br>algorithms must match too. A dedicated implementation of that complete contract<br>is a better fit.
Sealing Rng lets me shape it around my crate instead. I can add exactly the<br>primitives urandom needs without designing and documenting an implementation<br>contract for unknown generators and their special cases. This lets the generator<br>and algorithms specialize for each other, enabling niche optimizations not<br>available to rand.
For most applications, choosing the entropy is more useful than implementing a<br>new PRNG. The concrete generators expose their native from_seed constructors:
fn from_my_entropy(seed: [u32; 8]) -> urandom::Random {<br>urandom::rng::ChaCha12Rng::from_seed(seed)
This is narrower than accepting any RNG implementation, but it preserves the<br>extension point I expect advanced users to need.
Beating rand without a better algorithm
My fork is not based on novel random-number algorithms.<br>On 64-bit systems, urandom::new() and rand::rngs::SmallRng use the same<br>Xoshiro256 family for non-cryptographic use. urandom::csprng() and<br>rand::rngs::StdRng use ChaCha12 for cryptographic use. Rand's convenient<br>rand::rng() handle is backed by ChaCha12.
rand's generator interface exposes integer words and byte filling.<br>A distribution that wants an f64 starts by asking for a full u64.
urandom::Rng also has specialized next_f32 and next_f64:
pub trait Rng: Sealed {<br>fn next_u32(&mut self) -> u32;<br>fn next_u64(&mut self) -> u64;
fn next_f32(&mut self) -> f32;<br>fn next_f64(&mut self) -> f64;
// byte filling omitted
Random floats need fewer random bits than a full word. A generator can therefore override these methods with a cheaper output function.
The Xoshiro family of algorithms support exactly that.<br>The implementation retains Xoshiro256++ for u64, while u32 and floats use the faster Xoshiro256+.<br>Both share the same state advance, but Xoshiro256+ has a cheaper output function whose upper bits are designed for this use case.
On my system, comparing the current urandom 1.0 implementation with rand 0.10.2,<br>the end-to-end Xoshiro f64 benchmark measured about 31% higher throughput (24% faster) in this...