Why Casper forked Rust's rand crate

fanf21 pts0 comments

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

rand urandom random generator implementation crate

Related Articles