HashXxh64
HashXxh64 is Basalt’s implementation of XXH64, Yann Collet’s xxHash. Reach for it when the value has to mean something outside your program: a Zstandard frame’s checksum, a hash another tool computed, a digest of a file too large to hold in memory.
// The Content_Checksum of a zstd frame is XXH64 of the decoded bytes at seed 0.const u32 frame_checksum = static_cast<u32>(HashXxh64(decoded));The name is the contract. HashXxh64 is XXH64 — the algorithm zstd, .NET and a decade of file formats use — and its output for a given (bytes, seed) pair is frozen for the life of the framework. It is never upgraded, only ever joined by new stable hashes under their own names.
When this is the right member
Section titled “When this is the right member”Basalt’s stable hash collection has several 64-bit members, all frozen. They differ in what the value is good for:
| You need | Use |
|---|---|
| To reproduce a value zstd, .NET or another tool computed | HashXxh64 — no choice to make; the format made it |
| To hash something you never hold whole — a large file, a socket | HashXxh64, the only member with a streaming form |
| A fast 64-bit identity for content you do hold, owing nothing to a format | HashRapidV3 / HashRapidV3Nano, faster on bulk |
| A key for an in-memory table | HashFast — never a stable member |
HashXxh64 is not the fastest member and is not meant to be. It is chosen for what its value is.
The one-shot, seeded and unseeded
Section titled “The one-shot, seeded and unseeded”const u64 digest = HashXxh64(bytes); // seed 0 — what zstd and .NET's seedless ctor useconst u64 keyed = HashXxh64(bytes, 0x1234ull); // the seed a format of your own choseHashXxh64(bytes) is exactly HashXxh64(bytes, 0), and the frozen test corpus pins both, so the short spelling is interoperable with zstd by construction. The seed alters the four accumulator lanes’ starting values and nothing else — every 64-bit seed is legal, and the reference states no constraint on them.
Streaming: for input you never hold whole
Section titled “Streaming: for input you never hold whole”Three functions, a POD state, and four rules.
HashXxh64State state = HashXxh64Initialize(0);
u8 chunk[64 * 1024];usize read = 0;while ((read = ReadNextChunk(source, chunk, sizeof(chunk))) != 0){ HashXxh64Update(state, Str8(chunk, read));}
const u64 digest = HashXxh64Final(state);The same shape works over FileRead from basalt/core/platform/File.h: open the file, loop FileRead(file, offset, Span<u8>{ chunk, sizeof(chunk) }) while it returns a non-zero count, feed each count’s worth of bytes to HashXxh64Update, and finalise once at the end. Nothing about the chunk sizes matters — which is the first rule below.
| Rule | What it means for you |
|---|---|
| Concatenation | Update(a) then Update(b) is exactly Update(a ++ b), at any split. Chunk however your I/O chunks; empty updates are no-ops. |
| One-shot equivalence | The streamed digest equals HashXxh64(whole, seed), always. |
Final does not consume |
It takes the state by const& and finalises a copy. Digest a prefix, keep updating, digest again. |
| Copy is snapshot | HashXxh64State is plain data: assigning it forks the stream. There is no Reset — assign a fresh HashXxh64Initialize(seed). |
A state is 72 bytes of plain data with nothing to free: put it on the stack, in an arena, or inside your own struct, and copy it with =. One state belongs to one thread; any number of them may run in parallel.
What the promise rests on
Section titled “What the promise rests on”- The algorithm is published, not local. XXH64 by Yann Collet, specified bit for bit in the project’s own
doc/xxhash_spec.md. You can compile the reference yourself and reproduce every value Basalt ever emitted. - 252 frozen reference vectors pin it in the build. Every length 0–70 at two different seeds, long inputs running the stripe loop many times, every byte alignment, degenerate contents. The table is a public contract and is never regenerated: a change to any output bit fails the build.
- xxHash’s own nine published sanity values are asserted separately, from a reimplementation of the reference’s test-input generator — so the anchor holds even if Basalt’s own corpus were wrong.
- The output is a function of the bytes alone. Not of the buffer’s address, its alignment, the compiler, the optimisation level, or the machine’s byte order. The reads are defined little-endian, so x86-64 and ARM64 agree by construction.
One honest footnote, since a promise you cannot check is worth less than one you can: xxHash never wrote an explicit “this output is frozen” sentence for XXH64 — the only such wording upstream is attached to XXH3. XXH64’s stability there rests on the published specification and on a decade of formats that froze it for their own reasons. Basalt’s guarantee is its own vector table, which does not depend on upstream’s wording at all.
What it does not promise
Section titled “What it does not promise”There is deliberately no canonical big-endian byte form and no HashXxh64(u64) overload: how you serialise a value, and in which byte order you write the digest, are part of your file format, not of a hash function.
API summary
Section titled “API summary”Parameters, assertions and the full contract are documented in basalt/core/hash/HashXxh64.h.
| Signature | Purpose |
|---|---|
HashXxh64(String8 bytes) |
The frozen XXH64 hash at seed 0. What zstd’s frame checksum is. |
HashXxh64(String8 bytes, u64 seed) |
The same under a caller-chosen seed. HashXxh64(b) == HashXxh64(b, 0). |
HashXxh64Initialize(u64 seed) |
A streaming state that has consumed no bytes. |
HashXxh64Update(HashXxh64State&, String8 bytes) |
Appends bytes. Any chunking; empty appends are no-ops. |
HashXxh64Final(const HashXxh64State&) |
The digest so far. Does not consume the state. |