vrd 0.0.10

A lightweight, no_std-friendly random number generator backed by Xoshiro256++ with optional Mersenne Twister support.
Documentation

Install

cargo add vrd

Or in Cargo.toml:

[dependencies]
vrd = "0.0.10"

Requires Rust 1.70.0 or later. Builds for macOS, Linux, Windows, no_std embedded targets (Cortex-M, thumbv7em-none-eabihf), and wasm32-unknown-unknown — all validated in CI.


Highlights

  • Xoshiro256++ default — 32-byte state, 2^256 - 1 period, high statistical quality, faster than MT19937 in practice.
  • Mersenne Twister opt-in — keep MT19937 for legacy reproducibility. Random::new_mersenne_twister() (entropy-seeded, requires alloc + std) or Random::new_mersenne_twister_with_seed(u32) (deterministic, alloc only).
  • no_std ready — pure-core build with no allocator: Random::from_seed([u8; 32]) gives you a working RNG on any embedded target.
  • Unbiased bounded samplingint, uint, random_range, bounded use Lemire's nearly-divisionless method, not modulo.
  • Bit-precise floatsfloat() carries 24 mantissa bits (the f32 maximum); double() / f64() carry 53 (the f64 maximum). Always [0.0, 1.0).
  • Distributionsuniform(low, high), normal, exponential, poisson (std-free, via libm).
  • Convenience helpersiter_u32 / iter_u64 / iter_bytes iterator adapters, uuid_v4_bytes (no_std) and uuid_v4 (alloc), hex_token, base64_token. fastrand and oorandom don't ship these; they spare callers reaching for a second crate.
  • rand 0.10 traits — implements TryRng (and the auto-derived Rng) plus SeedableRng, so vrd plugs into the wider rand ecosystem.

Feature flags

Flag Default? What it does
std yes Entropy seeding via rand::rng(); std::error::Error impls.
alloc via std Random::bytes, Random::string, Random::sample, Random::uuid_v4, Random::hex_token, Random::base64_token, the heap-stored Mersenne Twister backend.
serde no Serialize / Deserialize derives for the public types.

Disable defaults to ship into no_std:

vrd = { version = "0.0.10", default-features = false }            # core only
vrd = { version = "0.0.10", default-features = false, features = ["alloc"] }  # core + alloc

Quickstart

use vrd::Random;

fn main() {
    let mut rng = Random::new();              // entropy-seeded Xoshiro256++

    println!("u32:        {}", rng.rand());
    println!("u64:        {}", rng.u64());
    println!("[1, 100]:   {}", rng.int(1, 100));
    println!("[0.0, 1.0): {}", rng.float());
}

Deterministic sequences

use vrd::Random;

let mut rng = Random::from_u64_seed(0xCAFE_BABE);
let a = rng.rand();
let b = rng.rand();
// Re-seed with the same value to reproduce.

no_std embedded usage

use vrd::Random;

// Allocation-free; works on any target — including Cortex-M.
let mut rng = Random::from_seed([0x42u8; 32]);
let n = rng.rand();

Mersenne Twister (legacy reproducibility)

use vrd::Random;

let mut mt = Random::new_mersenne_twister();   // alloc + std
let v = mt.rand();

Migrating from earlier 0.0.x

The 0.0.10 release modernizes the architecture. Breaking changes:

  • Random now defaults to Xoshiro256++, not Mersenne Twister. Use Random::new_mersenne_twister() if you need MT.
  • The generic fill() method is gone — use Random::try_fill_bytes(&mut [u8]) from the rand_core::TryRng trait, or build types from rand() / u64().
  • int, uint, random_range are now unbiased — outputs are uniformly distributed even when the requested range doesn't divide 2^32 cleanly. Outputs differ from prior versions for the same seed.
  • MersenneTwisterError lost its IoError and SerializationError variants — direct serde_json / serde_yml / toml helpers were removed. Use serde directly with the serde feature for that.
  • VrdError::GeneralError now carries &'static str instead of Stringno_std-friendly.
  • The logging feature and create_log_entry helper are gone — vrd is no longer a log-formatting library.

See CHANGELOG.md for the full diff.


Development

cargo build                                                          # default features (std)
cargo build --no-default-features                                    # pure no_std
cargo build --no-default-features --features alloc                   # no_std + alloc
cargo test --all-features                                            # all tests
cargo clippy --all-targets --all-features -- -D warnings             # lint clean
cargo bench                                                          # comparative criterion benches
cargo check --target thumbv7em-none-eabihf --no-default-features     # Cortex-M smoke check
cargo check --target wasm32-unknown-unknown --no-default-features    # WebAssembly smoke check
cargo run --example all                                              # run every demo in examples/

Squeezing more performance

The default release profile (opt-level = 3, lto = true, codegen-units = 1) gets vrd to ~1.1 ns per u32 on Apple Silicon. Two extra knobs are available to downstream consumers who want every cycle:

Native CPU targeting — enables AArch64 NEON or x86 AVX/AVX-512 codegen for whichever host you're running on:

# .cargo/config.toml in your binary crate
[build]
rustflags = ["-C", "target-cpu=native"]

target-cpu=native is not baked into vrd's release profile because it would break cargo install for users on machines that download crates as binaries. Set it in the consuming crate.

Profile-Guided Optimization (PGO) — typically yields 5–15% on hot loops:

# 1. Instrumented build that emits .profraw counters
RUSTFLAGS="-Cprofile-generate=/tmp/pgo" cargo build --release
# 2. Run a representative workload to populate the profile
./target/release/your-app
# 3. Merge into a single .profdata
$(rustc --print sysroot)/lib/rustlib/*/bin/llvm-profdata merge -o /tmp/pgo/merged.profdata /tmp/pgo
# 4. Rebuild with the profile applied
RUSTFLAGS="-Cprofile-use=/tmp/pgo/merged.profdata" cargo build --release

See CONTRIBUTING.md for setup, signed commits, and PR guidelines.


How vrd compares

vrd rand 0.10 fastrand 2.x oorandom 11.x
Default backend Xoshiro256++ ChaCha12 / SmallRng Wyrand PCG family
MT19937 backend ✓ (built-in) external (rand_mt)
Pure no_std core partial
Cortex-M + WASM CI gated
Unbiased bounded sampling (Lemire)
Bit-precise floats (24-bit f32 / 53-bit f64) partial
Built-in uuid_v4 / uuid_v4_bytes needs uuid
Built-in hex_token / base64_token needs hex + base64
Output stability commitment (patch versions) explicitly none
rand 0.10 traits (TryRng, SeedableRng) (native)
CSPRNG path planned (#90) ✓ (OsRng, ChaCha20Rng)
Distribution catalogue 4 (built-in) 20+ via rand_distr

Reach for vrd when you want a single small crate that gives you fast non-cryptographic randomness, MT19937 for legacy reproducibility, UUIDs, and URL-safe tokens — across std, no_std + alloc, embedded (Cortex-M), and WebAssembly — without building a CSPRNG into your binary.

Reach for rand + rand_distr when you need cryptographically secure randomness today, or the full statistical-distribution catalogue.

What you don't have to depend on

Pulling vrd in instead of rand + companion crates typically lets you drop these from your dependency tree:

  • uuid — covered by Random::uuid_v4 / uuid_v4_bytes
  • hex or data-encoding — covered by Random::hex_token
  • base64 — covered by Random::base64_token
  • rand_distr — if uniform / normal / exponential / poisson cover your needs

Fewer transitive crates, less compiled code, fewer audit boundaries to track.


FAQ

Which methods will I use most often?

use vrd::Random;

let mut rng = Random::new();                  // entropy-seeded Xoshiro256++

let n: u32 = rng.rand();                      // any u32
let n: u64 = rng.u64();                       // any u64
let n      = rng.int(1, 100);                 // i32 in [1, 100], uniform
let n      = rng.uint(1, 100);                // u32 in [1, 100], uniform
let f      = rng.double();                    // f64 in [0.0, 1.0)
let b      = rng.bool(0.5);                   // 50/50 coin
let pick   = rng.choose(&[10, 20, 30]);       // Option<&T>

#[cfg(feature = "alloc")]
let buf    = rng.bytes(32);                   // Vec<u8>, 32 random bytes

Every public method is documented at docs.rs/vrd with a worked example.

How do I migrate from rand?

vrd implements the rand 0.10 traits, so most idioms translate directly:

rand 0.10 vrd equivalent
let mut rng = rand::rng(); let mut rng = Random::new();
rng.random::<u32>() rng.rand()
rng.random_range(0..n) rng.uint(0, n - 1)
rng.fill_bytes(&mut buf) rng.try_fill_bytes(&mut buf).unwrap()
slice.choose(&mut rng) rng.choose(slice)
slice.shuffle(&mut rng) (alloc) rng.shuffle(slice) (alloc)
rand::rngs::StdRng::seed_from_u64(s) Random::from_u64_seed(s)

Or pass a Random directly to any crate that takes a rand_core::TryRng, Rng, or SeedableRng — vrd implements all three.

How do I generate non-security tokens (correlation IDs, log markers, debug fixtures)?

use vrd::Random;
let mut rng = Random::new();

let trace_id = rng.uuid_v4_bytes();           // [u8; 16], no_std
# #[cfg(feature = "alloc")]
# {
let trace_id = rng.uuid_v4();                 // RFC 4122 hyphenated, alloc
let log_id   = rng.hex_token(16);             // 32 lowercase hex chars
let csrf_id  = rng.base64_token(15);          // 20 URL-safe base64 chars (no padding)
# }

For security-sensitive tokens (API keys, session IDs, password-reset links, CSRF tokens), vrd is not the right tool. Use rand::rngs::OsRng or the getrandom crate — both produce CSPRNG-grade output backed by the OS entropy source.

Do I need one RNG per thread?

Yes. Random (and Xoshiro256PlusPlus, and MersenneTwister) hold mutable state and are not designed for concurrent access. The standard pattern is one RNG per thread, seeded distinctly:

use vrd::Random;

# let thread_id: u64 = 0;
let mut rng = Random::from_u64_seed(thread_id);   // distinct per thread
let _ = rng.rand();

For parallel deterministic streams that don't drift, a forking Random::split() API is tracked in #92.

Can I save and restore RNG state?

Yes — enable the serde feature.

vrd = { version = "0.0.10", features = ["serde"] }
use vrd::Random;

let mut rng = Random::from_u64_seed(42);
let snap = serde_json::to_string(&rng).unwrap();

let mut restored: Random = serde_json::from_str(&snap).unwrap();
assert_eq!(rng.rand(), restored.rand());      // identical state, identical output

Random, Xoshiro256PlusPlus, MersenneTwisterParams, and MersenneTwisterConfig all derive Serialize / Deserialize under the serde feature.

Is vrd cryptographically secure?

No. Random is a non-cryptographic PRNG built on Xoshiro256++. For credentials, secrets, session IDs, or anything that an attacker would benefit from predicting, use a CSPRNG such as rand::rngs::OsRng or the getrandom crate. A built-in ChaCha20-based CSPRNG backend is tracked in #90.

Does vrd work without std?

Yes. With default-features = false, vrd compiles for pure no_std targets — Cortex-M is gated in CI on every PR. The alloc feature unlocks Vec/String/Box-backed APIs (bytes, string, sample, shuffle, uuid_v4, hex_token, base64_token, the Mersenne Twister backend). Without alloc, Random::from_seed([u8; 32]) and Random::from_u64_seed(u64) give you a fully-functional Xoshiro256++ on bare metal.

Does vrd work in WebAssembly?

Yes. wasm32-unknown-unknown is gated in CI under both --no-default-features and --features alloc. Default WebAssembly has no entropy source, so seed manually with Random::from_seed([u8; 32]) or Random::from_u64_seed(u64) rather than Random::new(). If you want OS-level entropy in the browser, enable getrandom's js feature in your binary crate — that's downstream's choice, not vrd's.

Why ship Mersenne Twister at all if Xoshiro is the default?

Reproducibility against existing MT-generated test vectors. Numerical-simulation pipelines, scientific software, and tooling that emits "random-looking" reference data often pin MT19937 because that's what NumPy / older rand / SciPy / MATLAB historically used. Reach for Random::new_mersenne_twister() (or new_mersenne_twister_with_seed(u32) for alloc-only) only when you need bit-for-bit MT19937 output.

Can I get the same sequence on two machines?

Yes — use Random::from_seed([u8; 32]) or Random::from_u64_seed(u64). Both are deterministic and allocation-free. The output is byte-identical across architectures (x86, ARM, RISC-V, WebAssembly) — only floating-point operations downstream of the RNG (your code's arithmetic) may differ across targets.

Is the output stable across vrd versions?

For a given seed and method, vrd commits to bit-stable output across patch releases. Algorithm changes (e.g., a faster normal() sampling method) bump at least the minor version and are flagged in the CHANGELOG's Migration section, naming the affected methods. Once vrd reaches 1.0, this stability commitment will extend to minor releases as well. The rand crate explicitly does not guarantee either. If you have golden-file tests, fuzzing corpora, or reproducible-research workflows depending on a stable RNG sequence, that's a meaningful difference.

How big is the RNG state?

  • Xoshiro256PlusPlus: 32 bytes (four u64 words). Stored inline.
  • MersenneTwister: ~2.5 KB (624 × u32 + index). Heap-stored when wrapped in Random to keep the enum discriminant small.
  • Random: a tagged enum holding either Xoshiro256PlusPlus inline or Box<MersenneTwister>; sized for the Xoshiro variant. The wrapper-vs-direct dispatch overhead is zero — the inliner elides the match completely (verified in cargo bench).

How fast is it?

cargo bench runs head-to-head against fastrand 2.x and rand::rng() on u32, u64, byte fills, bounded sampling, and distribution sampling. On Apple Silicon, Xoshiro vrd produces a u32 in ~1.1 ns; the wrapper adds zero overhead vs the raw Xoshiro256PlusPlus. Run them locally — absolute numbers are workload- and platform-dependent.


License

Dual-licensed under Apache 2.0 or MIT, at your option.