Repository navigation
v0.2.0 - Raw bytes everywhere, and Windows gets the fast Ruby path
The native call was never the bottleneck. Every binding already made one call into the Rust core per batch — what cost real time was the thousand objects each language constructed around it. This release exposes the raw bytes directly in all eight packages, and the win scales exactly with how expensive that language's object construction is: 73x in PHP, 35x in Python, 11x in Ruby, and near-nothing in Go and Swift, whose UUID types already are 16 RFC-ordered bytes and never paid the cost.
Alongside that: Ruby's compiled Magnus extension now ships for both Windows architectures, so the slow Fiddle fallback is no longer the only option anywhere mainstream — and every registry's package now carries build provenance, not just NuGet's.
Raw bytes, across all eight bindings
Generating 1000 v7 UUIDs, each binding's existing batch method versus its new byte form:
| Binding | Existing batch | New byte form | Gain |
|---|---|---|---|
PHP — newV7BatchBytes |
2147 µs | 29.3 µs | 73x |
Python — fill_v7 |
650 µs | 18.5 µs | 35x |
Ruby — new_v7_batch_bytes |
400 µs | 35 µs | 11x |
Go — FillV7BytesAt |
23.0 µs | 17.6 µs | 1.3x |
C# — FillV7(Span<byte>) |
21.95 µs | 18.20 µs | 1.2x |
The interesting part is the right-hand column, not the left: PHP, Python, C# and Go all converge on roughly 18 µs. That's the native core doing the actual work, and it was always available — the differences between languages were the wrappers, not the engine.
Which is also why the gains are so lopsided. Go and Swift hand the native core the caller's own slice and it writes the whole batch in place, no per-element conversion, because uuid.UUID is [16]byte and Foundation's UUID wraps uuid_t. C# and Java can't: System.Guid is mixed-endian in memory and java.util.UUID is two longs, so their array forms still rebuild each element and only the byte forms remove real work. Both say so in their own docs.
New surface, per binding:
- C# —
Span<byte>overloads forFillV6/V7andV6/V7To/FromSqlOrder, plus a non-throwingTry*twin for every fallible operation (TryNewV4/V6/V7,TryFillV6/V7) so aResult<T>-shaped gateway no longer needs atry/catchper call - Go —
FillV6/V7,FillV6/V7Bytes(each with anAtvariant),V6/V7To/FromSqlOrderBytes - Swift —
fillV6/fillV7overUnsafeMutableRawBufferPointerandinout [UUID],v6/v7To/FromSqlOrder(bytes:), and a newError.bufferNotWholeUUIDs - Java —
fillV6/fillV7overUUID[]andbyte[],v6/v7To/FromSqlOrder(byte[]) - Python —
fill_v6/fill_v7into a caller'sbytearray - Ruby —
new_v6_batch_bytes/new_v7_batch_bytes - PHP —
newV6BatchBytes/newV7BatchBytes
Go's fill is fully allocation-free: 17,629 ns, 0 B, 0 allocs per 1000, against 138,646 ns and 1000 allocations for individual calls.
Ruby: Windows gets the fast path
Windows was where the Fiddle fallback cost the most, and it's now the compiled Magnus extension on both architectures:
new_v4 |
new_v7 |
|
|---|---|---|
| win-x64 | 406 ns vs Fiddle's 2407 ns (5.9x) | 595 ns vs 2759 ns (4.6x) |
| win-arm64 | 416 ns vs 2299 ns (5.5x) | 621 ns vs 2474 ns (4.0x) |
Windows-on-ARM had been the one mainstream platform still on the fallback. It isn't now — verified on real Windows-on-ARM hardware, both Ruby ABIs, both backends, including the cross-backend agreement specs that pin Magnus and Fiddle to identical output.
Platform gems are also fat now. A Magnus extension is bound to a single Ruby minor — there's no abi3 equivalent to collapse that axis the way PyO3 does for the Python wheels — so each platform gem carries one compiled extension per supported Ruby under lib/hyperuuid/<minor>/ and picks at require time. Ruby 3.4 and 4.0 today.
Nothing to configure. gem install hyperuuid resolves the right one, and anything outside that grid (Ruby 3.2/3.3, musl, anything exotic) still gets the universal zero-compile Fiddle gem automatically.
C#: Guid.Timestamp
using HyperUuid;
Guid id = UuidGenerator.NewV7();
DateTimeOffset? created = id.Timestamp; // the creation time
DateTimeOffset? none = Guid.NewGuid().Timestamp; // null — a v4 carries no timeA C# 14 extension block, which is the only form that can express a property — the classic this Guid form is limited to methods, and reading a timestamp out of bits the value already holds is a projection, not an action. It re-spells UuidGenerator.GetTimestamp rather than reimplementing it, with a test pinning the two to identical results on every version.
Works on any Guid, including one from Guid.CreateVersion7() — both write the same RFC 9562 layout.
Every package is signed now
At v0.1.1 only the .nupkg carried build provenance. The gem, the wheel, the crate and the jars all shipped unsigned even though the native binaries inside them were signed. That's closed, and the gates that were missing on the way in are in place:
# A package — signed by release.yml, which lives in this repo.
gh attestation verify hyperuuid-0.2.0-x64-mingw-ucrt.gem --repo SkunkWerkx/HyperUuid
# A native library, or a Magnus extension — signed by the shared forge workflow,
# so the signer's own repo has to be named too.
gh attestation verify libhyperuuid.so \
--repo SkunkWerkx/HyperUuid --signer-repo SkunkWerkx/.github--repo X asserts two things at once: that the artifact came from X, and that the workflow which signed it lives in X. The first half always holds; the second depends on where the signing step physically is. Omit --signer-repo on a forge-signed artifact and it fails with a bare verifying with issuer "sigstore.dev" that reads like a bad signature but is only an identity mismatch.
The RubyGems job now verifies all ten native artifacts before packing rather than trusting them, attests pkg/*.gem before the push so a failure stops the release while it's still reversible, then re-fetches each gem from the CDN and records attested-vs-served digests in the job summary — turning "the registry stores an upload verbatim" into a per-release measurement instead of a belief.
This matters because the Rust build is deterministic locally but not bit-reproducible across machines, so rebuild-and-compare was never a verification path a consumer could actually use. A signed attestation is.
The file on disk is updated too, so --notes-file picks it up as-is.
Worth noting the .nupkg has a third wrinkle I deliberately left out of the release notes, since csharp/README.md already covers it properly: nuget.org injects its own .signature.p7s into the zip during validation, changing the SHA-256. So the published package verifies without --signer-repo (release.yml signed it post-push), but recovering the as-packed attestation needs zip -d HyperUuid.0.2.0.nupkg .signature.p7s first, and then --signer-repo. Two attestations, deliberately, because one file can't cover both states.
Under the hood
- Both Windows Magnus extensions build the
gnullvmRust target instead ofgnu— same mingw-w64/UCRT ABI, LLVM instead of GCC. The GCC target statically links libgcc; the LLVM one uses compiler-rt. Shipped extension: 1,612,742 → 342,016 bytes, 79% smaller. Both the load into RubyInstaller's GCC-built Ruby and unwinding across the boundary — magnus turns Rust panics into Ruby exceptions, and this swaps the unwinder — were tested on real hardware rather than reasoned about. lto = trueandcodegen-units = 1on the release profile. Applies to builds of this repo; a downstream crates.io consumer still gets their own workspace's profile.
Upgrading
Drop-in for every binding. No API removed, nothing deprecated, no behavior change to any existing call.
One caveat, and it inverts the usual advice. The new byte forms are only faster when bytes are the destination — a datformat, a bulk COPY. Filling and then constructing UUID objects yourself is slower than the existing batch call: inPython that path measures ~1210 µs against new_v7_batch's 650 µs, roughly twice as slow, because the extension's internal fast path beats anything callable from Python. Ruby and PHP are the same story — slicing the returned string yourself only relocates the identical allocations into your own code.
If you want objects, keep using the existing batch methods. They haven't changed.
Ruby platform gems now declare required_ruby_version >= 3.4, < 4.1, narrower than the gemspec's own >= 3.2. A platform gem is only correct on the ABIs actually inside it, and RubyGems declining it is the only guard that runs first — a wrong-ABI extension must never be installed at all. Ruby 3.2/3.3 consumers resolve the universal Fiddle gem instead, automatically and silently.
Full changelog: v0.1.1...v0.2.0