Repository navigation
Releases: SkunkWerkx/HyperUuid
Release list
v0.8.0 - Inspection, SQL Server reads, and Android
0.8.0 adds UUID inspection from the core: version, variant and an RFC check, also on values read straight back from SQL Server. It also guarantees that a v7 batch is in strictly increasing order, and brings Android to C#, Go and Swift. Most of it was asked for by Norse Architecture, the first consumer. The full list is in CHANGELOG.md.
Highlights
-
Version and variant inspection (#29). Every binding now has
Version,VariantandIsRfc, in its own spelling. They come from new C exportsuuid_version,uuid_variantanduuid_is_rfc; in Rust they areUuid::variant()andUuid::is_rfc(version), beside the existingUuid::version(). UseIsRfc(id, 7)before trusting a value's v7 fields, for example in a wrap constructor or a deserializer. It checks the RFC variant as well as the version nibble, which a platform's own nibble-onlyVersionproperty doesn't. Swift had no version or variant accessor before. -
Reads straight from SQL Server byte order (#30). A new
Layout(Rfc9562,SqlServer) is accepted byVersion,IsRfc,V6UnixMillis,V7UnixMillis,V6Timestamp,V7TimestampandGetTimestamp. A value read back from auniqueidentifiercolumn is checked and dated in one native call, without converting it back first. In SQL Server order only v6 and v7 exist, and the core checks the variant bits as well as the version nibble, so it never confuses the two. -
A v7 batch is always strictly increasing (#32). The counter wraps every 2^26 values. Before this release, a batch that straddled the wrap had its second half sort before its first.
- Crossing the wrap: the UUIDs from the wrap on now carry the supplied timestamp plus one millisecond, which RFC 9562 §6.2 allows on counter overflow.
- Size limit: a batch takes at most
MaxV7BatchUUIDs (67,108,864). A larger one is refused before anything is allocated. - One batch, not the whole stream: the next batch or call in the same millisecond can sort before the end of the previous batch, at most once per 2^26 UUIDs.
v7 ordering, precisely has all the details.
-
Android for C#, Go and Swift. Each binding gets the core for arm64 and x86_64, built with the NDK and aligned for the 16 KB memory pages that Android 15 and Google Play require.
Language How C# A .NET MAUI app now gets the core on every platform MAUI targets with nothing but the PackageReference. On CoreCLR, the app loadsruntimes/android-{rid}/native/libhyperuuid.sofrom its APK. A Native AOT publish links the core in fromstaticlibs/android-{rid}/libhyperuuid.a.Go Under GOOS=android, cgo linksgo/staticlib/android_{arm64,amd64}, with the NDK's clang asCC.Swift The Swift SDK for Android (Swift 6.3+, API 28+) links the artifact bundle's *-unknown-linux-androidvariants.On every PR, CI runs the C# app (CoreCLR and Native AOT) and the full Go and Swift suites in a 16 KB-page Android emulator. The arm64 APKs are built and checked, but not run.
-
A shared conformance corpus (#31).
corpus/*.jsonpins v5 derivation, the v6 and v7 field layouts, both SQL Server conversions, timestamp reads and inspection. Every binding's suite replays it. The vectors are generated bycorpus/oracle.py, which shares no code with the core, and CI regenerates every vector on each run to catch a hand-edited one.
Fixes
- A batch too large to address is no longer reported as a random-source failure. C#, Go, Java, PHP, Ruby's Fiddle backend and Swift treated that error code as an RNG error. It is now an argument error, as Python and Ruby's Magnus backend already reported it. It can only happen on a 32-bit target.
- Ruby: the Magnus extension survives a compacting garbage collection. Raising
RandomSourceErrororTimestampOutOfRangeErrorafter a compaction could segfault. Both are now pinned when the extension loads.
Upgrade notes
Most of this release is new API. These changes can affect existing code:
- PHP:
Uuid::variant()returns aUuidVariantenum instead of an int.$id->variant()->value === 0b10replaces the old comparison, which is now always false. - Ruby:
Uuid#variantreturns a Symbol (:ncs,:rfc9562,:microsoft,:future) instead of an Integer.variant == :rfc9562replacesvariant == 0b10, which is now always false. - Swift:
UuidGenerator.ErrorgainsbatchTooLarge(count:)andbatchNotAddressable(count:). Aswitchover it with nodefaultneeds both cases. The second replaces a crash on a v6 count pastUInt32.max. - All bindings: a v7 batch larger than
MaxV7Batchnow fails with an argument error. - All bindings except Python:
GetTimestampnow requires an RFC 9562 v6 or v7. A value with a 6 or 7 version nibble but another variant returnsNone/nullinstead of a meaningless timestamp. Python already behaved this way.
Verifying
Every native library, static archive and wasm32-wasip1 archive in this release was built and attested by CI run 37873606161, at commit a623c0c. That includes the new Android archives:
gh attestation verify go/staticlib/android_arm64/libhyperuuid.a \
--repo SkunkWerkx/HyperUuid --signer-repo SkunkWerkx/.githubThe binaries were built before the tag existed. The tagged commit is the one that commits them, and its parent is the commit they were built from. So to pin the source, check against that commit rather than the tag: add --source-digest a623c0cce76569b0cca31a26b09b232319d396d8.
The gems, hyperuuid-wasm included, are attested by this repo's own release.yml, so --repo SkunkWerkx/HyperUuid alone verifies them. The Python wheels verify the same way as the native libraries, or with --owner SkunkWerkx.
v0.7.0 - iOS and Mac Catalyst, and a faster core on Linux
Notes
0.7.0 brings HyperUuid to iPhone, iPad and Mac Catalyst apps from C#, Swift and Go. On Linux, single UUIDs and batches now get their randomness from a user-space ChaCha20 generator instead of a system call each time, which makes them much faster. The full list is in CHANGELOG.md.
Highlights
-
iOS and Mac Catalyst. These platforms can't load a native library, so the core is linked into the app.
Language How C# A .NET iOS, MAUI or Mac Catalyst app needs only the PackageReference. The package carries the core forios-arm64,iossimulator-arm64,maccatalyst-arm64andmaccatalyst-x64(about 7 KB each), and the SDK links it. This works under Mono AOT, the interpreter and Native AOT.Swift The package builds for iOS, the iOS simulator and Mac Catalyst on arm64. On those platforms it links the core from a new HyperUuidCoreApple.xcframework, which Xcode can link. Linux, Windows and WebAssembly builds see the same package as before.Go cgo builds for an iOS device, the iOS simulator on Apple silicon ( -tags iossimulator), and Mac Catalyst on both architectures (maccatalyst, which gomobile sets) each link their own archive fromgo/staticlib/.On every PR, CI tests all three on a Mac against archives built from that commit. The suites run in an iOS simulator and as a Mac Catalyst process, and an iOS device build is linked and checked for the core's symbols.
-
Faster single UUIDs on Linux.
new_v4,new_v6andnew_v7used to callgetrandomfor every UUID. They now take randomness from a pool of buffered ChaCha20 generators, built like the kernel's own vDSO generator: each key is erased as soon as the next one is made, and every pool slot is reseeded fromgetrandom.- Speedups on linux-x64:
new_v4went from 38 to 23 ns on glibc and from 228 to 24 ns on musl, andnew_v6is 1.3-1.4x faster. - Safe across
fork: the pool's memory is markedMADV_WIPEONFORK, so a forked child never repeats its parent's bytes. - Safe across VM snapshots: no key is used for more than one second of wall-clock time. That covers cloned VMs, Lambda SnapStart, and CRaC or CRIU checkpoints.
- Fallback: where neither guarantee can be trusted, the core keeps plain
getrandom. Other platforms are unchanged.
- Speedups on linux-x64:
-
Batches are about 2.5x faster on Linux, and 3.5x on musl. A large v6 or v7 batch makes one 32-byte
getrandomcall and expands it with a SIMD ChaCha20 keystream. Nothing outlives the call. A 1000-UUID v7 batch went from 10.6 to 4.4 µs on glibc. -
new_v5is about 13% faster (54.7 to 47.3 ns). A new test checks it against theuuidcrate for every name length up to 300 bytes. -
Every benchmark table re-measured. Single calls against each platform's own generator:
- C#: 7.5-10x faster than
Guid.NewGuid(). - Java: 2.8-6.2x faster than
UUID.randomUUID(). - Swift: 11-16x faster than
Foundation.UUID(). - Ruby: 5.5x faster than
SecureRandom.uuidfor v4. - Batches: a 1000-UUID byte fill now costs 3.6-4.9 µs in every binding, down from 9-12 µs.
- C#: 7.5-10x faster than
Fixes
- Go on Android and iOS no longer links another platform's archive. Go treats
GOOS=androidaslinuxandGOOS=iosasdarwin, so those builds used to get the Linux or macOS archive at link time. iOS now gets its own archive. Android and the iOS simulator on Intel, which have no archive, now stop with the same named compile error as any other unsupported build.
Upgrade notes
Drop-in for every binding.
- Go: builds for Android and the Intel iOS simulator now fail at compile time, as other unsupported platforms already did. They never had a working core.
- Linux kernels before 4.14 (no
MADV_WIPEONFORK) keep callinggetrandomfor single UUIDs, as in 0.6.x. - C#: iOS and Mac Catalyst builds need the .NET
ios/maccatalystworkloads and the matching Xcode, as any .NET iOS app does.
Verifying
Every native library, static archive and wasm32-wasip1 archive in this release is built by CI and carries a build-provenance attestation. That includes the new iOS and Mac Catalyst archives:
gh attestation verify go/staticlib/ios_arm64/libhyperuuid.a \
--repo SkunkWerkx/HyperUuid --signer-repo SkunkWerkx/.githubThe gems are attested by this repo's own release.yml, so --repo SkunkWerkx/HyperUuid alone verifies them. The Python wheels verify the same way, or with --owner SkunkWerkx.
v0.6.1 - Ruby in the browser
0.6.1 adds Ruby to the browser languages: HyperUuid now runs in a web page from C#, Rust, Python, Go, Swift and Ruby. It also fixes two install and typing papercuts from 0.6.0. The full list is in CHANGELOG.md.
Highlights
- Ruby in the browser: the new hyperuuid-wasm gem. ruby.wasm links extensions into the interpreter when rbwasm build makes it. So in the Gemfile you hand to rbwasm build, list hyperuuid-wasm instead of hyperuuid:
gem "hyperuuid-wasm"
gem "js"
- Same extension: you get the same Magnus extension as the platform gems, prebuilt for wasm32-wasip1, so HyperUuid::BACKEND is :native.
- No Rust toolchain: the gem carries one archive for Ruby 3.4 and one for 4.0, and --ruby-version picks.
- Shares an interpreter with HyperCast: hypercast-wasm links into the same interpreter.
- Tested in CI: CI builds each archive from the commit, attests it, and runs the gem linked into a fresh interpreter under Node and in headless Chrome.
See Ruby in the browser.
This was listed under "Proven, not shipped" in 0.6.0. @matsadler fixed both Magnus bugs that held it back (magnus#186, #187) and shipped them in Magnus 0.8.3 and 0.9.2 the day after they were filed. That's what made this release possible. Thank you, Mat!
Fixes
- Ruby: the platform gems no longer depend on fiddle. They never load it, but 0.6.0's still declared it, so Bundler on Alpine with Ruby 3.4 compiled fiddle from source for nothing, or failed without build-base and libffi-dev. Only the universal gem declares it now, and it loads Fiddle only when the Fiddle backend first runs.
- Python: a non-UUID where a uuid.UUID belongs (new_v5's namespace, the timestamp readers, the SQL-order conversions) now raises TypeError as the README promises, not AttributeError.
Upgrade notes
Drop-in for every binding. The Ruby extension now builds on Magnus 0.9.2 (from 0.8.2), with no API change. Python's change only turns an AttributeError into a TypeError.
Verifying
Every native library, static archive and wasm32-wasip1 archive in this release is built by CI and carries a build-provenance attestation:
gh attestation verify go/staticlib/linux_amd64/libhyperuuid.a
--repo SkunkWerkx/HyperUuid --signer-repo SkunkWerkx/.github
The gems, hyperuuid-wasm included, are attested by this repo's own release.yml, so --repo SkunkWerkx/HyperUuid alone verifies them. The Python wheels verify the same way, or with --owner SkunkWerkx.
v0.6.0 - One way in, and in the browser
0.6.0 gives every binding one way to reach the core, puts HyperUuid in the browser for C#, Rust, Python, Go and Swift, and makes every package smaller. The full list is in CHANGELOG.md.
Highlights
-
In the browser, proven in a browser. Every route below runs in headless Chrome in CI, on every PR.
Language How C# Blazor WebAssembly (.NET 11+): a plain PackageReference, as beforeRust wasm32-unknown-unknownwith getrandom'swasm_jsfeaturePython (new) Pyodide: await micropip.install("hyperuuid")finds a newpyemscriptenwheel on PyPI (~73 KB). The whole test suite runs inside Pyodide in CI.Go (new) TinyGo 0.42+ with -target=wasm, the core linked inSwift (new) the wasm32-wasip1build, run in a page through a WASI shim -
Go links the core, and that's the only way. cgo statically links a ~20 KB archive on Linux, macOS and now Windows, using the same MSVC archive C# links under Native AOT.
- Removed: purego, the loading cgo backend, the wasmtime backend, and the nine embedded libraries a build used to extract to a temp file.
- Dependencies:
go.modneeds onlygoogle/uuid.
-
Swift links the core on every platform. macOS and Windows join Linux and WebAssembly: no loader, no resource bundle, and nothing to deploy beside your executable.
-
Ruby runs Magnus on Alpine.
- New musl platform gems carry Magnus extensions for Ruby 3.4 and 4.0.
- Fiddle is now only the universal gem's last resort, for Ruby 3.3, a Ruby newer than this release, Intel Macs and unbuilt platforms.
- Platform gems no longer carry Fiddle libraries.
-
Smaller everywhere.
- Every shipped library is stripped: linux-x64 is 16 KB, with identical machine code.
- The Ruby
x86_64-linux-gnugem is 339 KB, from 459 KB. - Go's module and the Composer package drop the embedded libraries.
- A Blazor app no longer copies a 3 MB archive into its output.
Upgrade notes
Go (breaking: the reason this is 0.6.0).
- Needs cgo and a C compiler where it's built:
- Linux: gcc or clang (
build-baseon Alpine) - macOS: the Xcode command-line tools
- Windows: MinGW-w64 gcc (llvm-mingw on arm64)
- Linux: gcc or clang (
- Unsupported builds fail at compile time, with an error that names the fix. That covers
CGO_ENABLED=0, stock Go'sGOOS=js/wasip1, and any other platform. - Drop the old build tags
-tags hyperuuid_dynamicand-tags hyperuuid_wasm. - API unchanged:
Available()is alwaystrue, andErrNativeUnavailableis deprecated.
Swift.
- Nothing to deploy any more on macOS and Windows: remove the step that copied
HyperUuid_HyperUuid.bundle/.resourcesbeside your executable. NativeLibraryErrorhas no cases now, so code matching.openFailed/.symbolNotFoundno longer compiles.- Unsupported platforms fail at compile time.
Ruby.
- Alpine moves from Fiddle to Magnus automatically.
- The glibc platform gems are renamed
x86_64-linux-gnu/aarch64-linux-gnu. RubyGems and Bundler resolve them by themselves; only an explicit--platform x86_64-linuxor a pinned gem file name needs updating. - Intel Macs stay on Fiddle via the universal gem.
Python and Ruby.
- The opt-in wasmtime backend is gone, along with the
[wasm]extra and the Gemfile group. HYPERUUID_WASMis now ignored. The native backend always loads.
Rust, C#, Java and PHP are drop-in. Rust now declares rust-version = "1.85".
Fixes
- Python on Pyodide: about a quarter of no-argument
new_v6()/new_v7()calls would have stamped the previous millisecond, because of how Emscripten reports the clock. Fixed before the wheel shipped. - C#: a Blazor WebAssembly app no longer gets the 3 MB
libhyperuuid.ainbin/and the publish output. - Rust: the
no-panicproof from a consumer's crate is documented: it needslto = truein your release profile.
Proven, not shipped
- PHP and Ruby in the browser both work.
- PHP: the
ext-php-rsextension runs as a WordPress Playground side module, andphp/README.mdhas the full recipe. - Ruby: ruby.wasm with Magnus works too, but waits on two Magnus fixes.
- Both are documented with what it would take to ship them.
- PHP: the
- Upstream issues filed on the way:
Verifying
Every native library and static archive in this release is built by CI and carries a build-provenance attestation:
gh attestation verify go/staticlib/linux_amd64/libhyperuuid.a \
--repo SkunkWerkx/HyperUuid --signer-repo SkunkWerkx/.githubThe Python wheels, the Pyodide wheel included, verify the same way, or with --owner SkunkWerkx.
v0.5.0 - Proven panic-free, and a twentieth the size
0.5.0 proves the Rust core cannot panic, fixes the panics that proof turned up, and cuts the native library every binding loads to a twentieth of its size. The full list is in CHANGELOG.md.
Highlights
-
The public API is proven panic-free at link time. Every generation, parsing and conversion function and every C export carries
#[no_panic]under the newno-panicfeature, and CI links a release binary that calls each one on Linux and Windows, so a panic path the optimizer cannot remove fails the build. The feature is off by default and changes no code a consumer runs. -
The native libraries are a twentieth the size. The shared library is now built
#![no_std], and link-time optimization finally reaches it. Same 13 exports over the same code.0.4.0 0.5.0 linux-x64 426,856 bytes 19,048 bytes win-x64 124,928 bytes 17,920 bytes The Linux libraries now depend on libc alone, so the musl builds no longer need
libgcc_s. The Python, Ruby and PHP extensions keep std, so a panic in one still surfaces as a host exception. -
Python wheels are tested before they ship. All eight are built, installed on their own platform and called into on every CI run, and the release publishes those exact files after checking their count, version and provenance. Through 0.4.0 they were built at the tag, and 0.4.0's osx-x64 wheel failed there.
-
The Composer package is smaller. It no longer carries the other bindings: 2.6 MB downloaded (was 3.5), 6.1 MB unpacked (was 8.5).
Upgrade notes
Rust is the one breaking change. NewV6Error and NewV7Error gained a BufferTooSmall variant and are now #[non_exhaustive], so a match on either one outside the crate needs a wildcard arm. That is why this is 0.5.0.
Every other package is a drop-in. The C ABI only gained return code 3 from uuid_new_v6_batch/uuid_new_v7_batch (a count * 16 that overflows usize). Only a 32-bit target can reach it, and no binding can, because each allocates its buffer before the call.
Building from source: [lib] now declares only the rlib. Build the shared library with cargo cdylib and the wasm32-wasip1 module with cargo wasm-module; a plain cargo build no longer produces a shared library.
Fixes
All in the Rust crate unless noted.
default-features = falsebuilds on every target. It failed with "#[panic_handler]function required" on any target that can produce a cdylib, including the developer's own machine andwasm32-unknown-unknown. CI now builds a real consumer for both.Uuid::from_strno longer panics on a stray hyphen. A 36-character string with an extra hyphen in the last group read one byte past the end (00000000-0000-0000-0000--00000000000). It is nowParseUuidError.- A short buffer passed to
new_v6_batch/new_v7_batchreturnsBufferTooSmallinstead of panicking, and leaves the buffer untouched. v7::now_v7no longer panics on a clock set before 1970. It returnsTimestampOutOfRange, as it now also does for a clock so far ahead that its milliseconds overflow au64, which used to be silently truncated.Timestamp::to_unix_millissaturates instead of overflowing. In a release build it used to wrap into a valid-looking timestamp, sonew_v6_at/new_v7_atminted a UUID for the wrong time.- Ruby: the native extension falls back to
RuntimeErrorinstead of panicking on an uninitialized exception cache (unreachable in practice).
Verifying
Every native library and static archive in this release is built by CI and carries a build-provenance attestation:
gh attestation verify go/native/linux-x64/libhyperuuid.so \
--repo SkunkWerkx/HyperUuid --signer-repo SkunkWerkx/.githubThe Python wheels are now signed by the forge, so they verify the same way (or with --owner SkunkWerkx). Wheels up to 0.4.0 verify with --repo alone.
v0.4.0 - Alpine, a linked-in core, and every benchmark re-measured
0.4.0 widens where HyperUuid runs, removes the shared library from the builds that never needed one, and replaces the published benchmarks with a fresh set measured on x86-64. The full list is in CHANGELOG.md.
Highlights
- Alpine works.
linux-musl-x64andlinux-musl-arm64are built, attested and shipped in every package (asmusllinux_1_2wheels for Python). Through 0.3.0 Alpine got the glibc library, which does not load under musl. - The core links in where it can.
- Swift on Linux links a static library through SwiftPM, which adds the static Linux SDK (musl) and WebAssembly to that binding. Nothing has to be deployed beside the executable.
- Go cgo builds on Linux and macOS link the core instead of embedding and
dlopening it. The binary starts without touching the filesystem and runs from a read-only orscratchimage. - C# Native AOT publishes link the core into the executable.
- A load probe in every binding. The core reports its own version (
hyperuuid_version), and each binding fronts it with a check that never throws:UuidGenerator.IsAvailable/NativeVersionin C#, and the same pair in each language's idiom. - Java in a GraalVM Native Image is fast. FFM calls were being interpreted there: 6.4 µs per
newV7(long)against 36 ns on the JVM. They are now compiled: 78 ns. Nothing changes on the JVM, and a consumer'snative-imagebuild needs no configuration. - Python and Ruby calls cost about half.
- Python:
new_v4()540 → 243 ns,new_v7(ms)726 → 354 ns,v7_timestamp466 → 252 ns. Wheels are unchanged (still oneabi3wheel per platform). - Ruby:
new_v4317 → 234 ns,new_v7512 → 284 ns on the Magnus backend.
- Python:
- New in Python:
v6_unix_millisandv7_unix_millisreturn the embedded timestamp as anint, with nodatetimebuilt (161 ns). The package is now typed (py.typed).
Benchmarks, re-measured
Every table was re-measured on linux-x64 (Intel Core i9-11900H) with each binding's own harness. The previous figures came from an arm64 WSL2 machine whose slow clock read inflated several ratios, so some headline numbers are smaller than in 0.3.0. Each README names the machine and runtime behind its tables.
| Binding | Generation vs. the platform's own call |
|---|---|
| Swift | 9.5–13x faster than Foundation.UUID() |
| C# | 5.7–8.1x faster than Guid.NewGuid() |
| Ruby | 2.1–4.9x faster than SecureRandom.uuid |
| Python | 3.0–4.2x faster than stdlib uuid |
| Java | 2.4–4.5x faster than UUID.randomUUID() |
| Rust | 2x faster than the uuid crate on v5/v7, level on v4/v6 |
| PHP | level to 1.2x faster than a naive inline v4 |
| Go | slower per call than google/uuid; use the batch doors |
Upgrade notes
Runtime floors. Every floor that had reached end of life is raised. A consumer on an older runtime keeps resolving 0.3.0.
| Floor | |
|---|---|
| Python | 3.11 |
| Ruby | 3.3 |
| PHP | 8.2 |
| Java | JDK 25 |
| Swift | 6.2 |
Behaviour changes.
- Ruby, PHP:
Uuid.parseaccepts the 8-4-4-4-12 form only. Bare and misplaced-hyphen strings no longer parse. - Python: a
datetimepassed tonew_v6/new_v7is truncated to its millisecond. It used to be rounded and could stamp a UUID up to half a millisecond late. - Every binding: caller errors (out-of-range timestamps and counts, wrong types, null names) are now the same exception on every backend and name the mistake. Ruby's are
HyperUuid::TimestampOutOfRangeErrorandRandomSourceError; theRuntime::names remain as aliases. - Go: batch functions return
ErrNegativeCountfor a negative count.-tags hyperuuid_dynamickeeps the previous loading cgo backend. - C#: set
HyperUuidStaticLink=falseto keep the shared library under Native AOT. Blazor WebAssembly requires .NET 11; an older browser project gets warningHYPERUUID001and no native link. - Java: the optional GraalWasm engine moves to 25.4.4.1.1. It must be the same release as the GraalVM JDK you run on.
- Unknown architectures are no longer treated as x64. Go and PHP report an unsupported platform, Swift refuses to compile, and Java falls back to its wasm backend.
- Intel macOS (
osx-x64) still ships in every package. There is no precompiledx86_64-darwingem; Ruby there runs on the Fiddle backend.
Fixes
- An empty v5 name crossed the C ABI as a null pointer from C#, Go, Ruby (Fiddle) and Swift, which the core then built a slice from. Fixed in the core; every binding pins the empty-name vector.
- Java: a batch count past
Integer.MAX_VALUE / 16wrote past a Java array. It is nowIllegalArgumentException. - Python:
new_v6_batch/new_v7_batchnarrowedcountto 32 bits, so2**32 + 1returned one UUID. Out of range is nowValueError. - Swift:
newV6(_: Date)/newV7(_: Date)trapped on a date before 1970, and a missing resource bundle crashed the process. Both are thrown errors now. - Go: a core missing a symbol panicked on the purego backend.
- Ruby: on Alpine, 0.3.0 loaded the glibc library and the first call raised
Fiddle::DLError. - C#: a Blazor WebAssembly app could not use HyperUuid and HyperCast together (duplicate symbol at link time), failed in the browser on .NET 11, and got no native link when it reached the package through a class library.
Verifying
Every native library and static archive in this release is built by CI and carries a build-provenance attestation:
gh attestation verify go/native/linux-x64/libhyperuuid.so \
--repo SkunkWerkx/HyperUuid --signer-repo SkunkWerkx/.githubv0.3.0 - A wasm backend in Java, Ruby, Python and Go
The core built as a wasm32-wasip1 module, hyperuuid.wasm, ships beside the native libraries in the jar, the gems and the wheels, and is committed under go/native/. Each binding runs it behind its existing backend switch, with the engine an optional dependency the consumer adds only if they want this path:
- Java — GraalWasm.
-Dhyperuuid.backend=wasm, or automatic when the jar has no native build for the platform.org.graalvm.polyglot:wasmiscompileOnlyand never reaches the POM;UuidGenerator.backend()reports which path won. - Ruby — the wasmtime gem.
HYPERUUID_WASM=1, or automatic when no native library exists for the platform.HyperUuid::BACKENDreports:wasm;spec/wasm_backend_spec.rbpins the outputs byte-for-byte against Fiddle. - Python — wasmtime-py via
pip install hyperuuid[wasm].HYPERUUID_WASM=1, or automatic when the PyO3 extension fails to import.hyperuuid.BACKENDreports"wasm"or"native". - Go — wasmtime-go behind
-tags hyperuuid_wasm. Opt-in only, never selected automatically; the tag compilo throughout, so no win-arm64 build.
Measured on one box, through each shipped binding:
| Binding | new_v7 wasm |
new_v7 native |
1000-UUID byte fill wasm | native |
|---|---|---|---|---|
| Java (GraalVM JIT) | 420 ns | 64 ns | ||
| Java (Native Image) | 181 ns | — | — | — |
| Ruby | 867 ns | ~450 ns | 40.6 µs | 24 µs |
| Python | 6.2 µs | 850 ns | 41 µs | 18. |
| Go | 3.1 µs | 142 ns | 41 µs | 17.6 µs |
On a stock JDK with no GraalVM JIT the modulwV7lands at 3.1 µs. Every wasm call isserialized under a lock, because neither a GraalWasmContextnor a wasmtimeStore` is safe for concurrent use; the
native backends stay lock-free.
One detail was load-bearing rather than tidy: the module exports wasi-libc's malloc/free through two linker flags in rust/.cargo/config.toml. A wasm host cannot hand this library a pointer into its own memory, so every embedder has to ask the guest for a buffer — and a host-picked offset past the data segments collided with dlmalloc's first allocation and corrupted a batch mid-buffer in the spike that led here. The export is what makes the guest's own
allocator the only one in play. Nothing abountract changes on any native target.
CI builds the module on every leg and runs the four suites a second time through it.
The carrier diet: Java, Go, Swift
Java: nothing is copied on the way across. Every door used to open an Arena.ofConfined() per call and copy every
input into it. All twelve downcalls are now l(true), so a caller's byte[]— a v5name, a batch destination, sixteen bytes to reorder in place — is pinned and handed to the native side directly. The single-UUID doors use one per-thread 16-byte in/out scratch, written and read as two big-endian longs.reachability-metadata.json` registers the option and the GraalVM Native Image smoke test passes on it.
| JMH | Before | After |
|---|---|---|
newV4 |
155 ns | 102 ns |
newV5 |
230 ns | 102 ns |
newV6 |
128 ns | 67 ns |
newV7 |
125 ns | 77 ns |
| each, allocation | 112 B/op | 32 B/op |
fillV7(byte[]) |
0 B/op |
Go: the UUID crosses by value. Every single-UUID door handed the core &out[0] of a Go local, and any Go pointer passed to a cgo call escapes to the heap. The C shims now keep the sixteen bytes on their own stack and return them as a struct, and take a UUID argument the same way, so no Go pointer crosses except a caller's own slice. NewV4/NewV6At/NewV7At go 1 → 0 allocs, NewV5String 3 → 1 (the one left is Go's own []byte(name)), V6/V7UnixMillis 75 → 58 ns, 0 allocs. Per-call time on the generators barely moves, because entropy, not the
crossing, is what those doors cost — the REAm is corrected accordingly. purego isunchanged.
Swift: zero mallocs per call. Every doore out-value and another per input, and theString v5 form copied the name into an Array. uuid_t is sixteen RFC-ordered bytes already, so it is now the scratch and the result; the v5 name crosses via withUTF8, the batch object doors fill their result array in place
through the existing fill path, and the librce rather than a 13-field struct copied percall. newV4 1 → 0 mallocs, newV5 3 → 0, newV7Batch(1000) 86 → 17 µs and 1002 → 1 malloc. newV5(namespace:name:) gains an UnsafeRawBufferPointer primitive that the String and [UInt8] forms now wrap.
The wasm module is attested like every native library
hyperuuid.wasm carries the same build-prov native builds, signed by the reusableworkflow in SkunkWerkx/.github, and stage-native-binaries.yml refuses to commit it under go/native/ unless that attestation verifies:
gh attestation verify hyperuuid.wasm \
--repo SkunkWerkx/HyperUuid --signer-repo SkunkWerkx/.githubEvery README now has a Verifying provenance section with the exact command and flags for its artifact.
Ruby: the gemspec declares no wasmtime
The engine is a Gemfile group for this repo's own suite, not a development dependency of the gem, so gem install hyperuuid and bundle install against the gem pull in nothing new.
Upgrade note
Drop-in for every binding. Nothing is removed or renamed. The wasm backends are opt-in and change nothing until asked for: no new runtime dependency in any package — Java's GraalWasm is compileOnly, Ruby's wasmtime a Gemfile group for the suite, Python's an extra, Go's behind a ge: wasmtime-go now appears in go.mod, soit enters a consumer's module graph without entering their binary. The only addition under rust/ is the .cargo/config.toml that exports malloc/free on wasm32-wasip1, which applies to builds run from that directory and to nothing a consumer compiles.
v0.2.1 - Maven Central gets 0.2.x, and the crate gets its signature
A release-machinery fix. No binding's API changes — everything 0.2.0 added is unchanged and still there. What changes is that two things 0.2.0 couldn't finish are now finished.
0.2.0 was a partial release:
| 0.2.0 | 0.2.1 | |
|---|---|---|
| crates.io, RubyGems, PyPI, NuGet | published | published |
| Maven Central | never published | published |
| crates.io provenance | unsigned | signed |
If you use the Java binding, or if you verify provenance, this is the release you want. Everyone else can take it or leave it.
Java: 0.2.0 never reached Maven Central
Sixteen @param tags were missing from the destination-buffer and raw-byte SQL-order methods 0.2.0 introduced, and javadoc -Xwerror correctly refused to build the docs jar — which stopped the Maven publish after crates.io, RubyGems, PyPI and NuGet had already published.
0.2.0 is absent from Maven Central and will stay absent. The version number is spent on five other registries, so it cannot be published now. Java consumers should go straight from 0.1.1 to 0.2.1, which carries everything 0.2.0 added — the fillV6/fillV7 overloads over UUID[] and byte[], and the in-place byte[] SQL-order transforms.
The gate that caught this had existed all along. It simply never ran anywhere it could catch anything: C# and Rust get their doc enforcement from compilation CI already performs (CS1591 with warnings-as-errors, #![deny(missing_docs)]), but Java's -Xwerror only fired during the Maven publish. javadoc now runs on every pull request, on the same leg that already compiles Java.
The crate is signed again
On 0.2.0 the attestation step ran after cargo publish and could not find the packaged .crate, so the crate uploaded and the signing failed. A crates.io publish is irreversible — yank hides a version, it never deletes one, and the number can never be reused — so the 0.2.0 crate has no provenance attestation and cannot be given one. Even re-tagging wouldn't help: the commit sha rides inside .cargo_vcs_info.json, so a different commit produces different bytes.
Its integrity is still checkable — cargo verifies every download against the checksum crates.io's index records — but its provenance is not.
The pipeline now packages, attests, and only then publishes, so the same failure would stop the release while it is still reversible. Attesting the packaged file rather than the uploaded one is sound because a .crate is byte-identical wherever it is produced from the same commit; that was measured against both v0.1.1 and v0.2.0's published checksums, not assumed.
gh attestation verify hyperuuid-0.2.1.crate \
--repo SkunkWerkx/HyperUuid --signer-repo SkunkWerkx/.githubcrates.io publishing is tokenless
Publishing now uses crates.io Trusted Publishing over OIDC instead of a stored API token — short-lived credentials minted per run and revoked when the job ends. Consumer-invisible, and recorded only because it changes what a compromise of this repository's secrets could reach.
Worth knowing if you're setting up your own: a Trusted Publisher configuration can only be created after the crate already exists, and crates.io has no equivalent of PyPI's pending publishers. A brand-new crate needs one manual token publish first.
The rest of 0.2.0 was already sound
Before cutting this release, every other 0.2.0 artifact was verified by downloading it from its registry and running gh attestation verify — 20 artifacts, all passing:
- PyPI 6/6 wheels · RubyGems 7/7 gems, including
aarch64-mingw-ucrt· NuGet the published.nupkg - Packagist / Swift / Go — 6 native libraries, which covers all 18 committed paths: they are only 6 distinct git blobs, one per RID, shared across the three trees by design
So the gap really was just the crate and Java.
Upgrading
Drop-in from 0.2.0 for every binding, and the first available 0.2.x for Java. Same API, same behaviour, same native core — only the release plumbing changed.
Full changelog: v0.2.0...v0.2.1
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
v0.1.1 - Now genuinely no_std and allocation-free
The Rust core is now genuinely no_std and allocation-free — the no-std category it has
published since v0.1.0 is compiler-enforced instead of asserted, and it no longer links alloc
either. The other seven bindings keep the same API and behavior; they pick up packaging and
documentation fixes and are rebuilt against the new core.
hyperuuid (crates.io)
The crate is #![no_std] under default-features = false
v0.1.0 shipped categories = ["data-structures", "no-std"] while the library still required
std in four places — a claim that was live and false on crates.io. It's now true and the
compiler holds it:
[dependencies]
hyperuuid = { version = "0.1.1", default-features = false }std is a new default-on feature rather than an unconditional #![no_std], because this crate
also builds the cdylib every other binding in this repo dlopens, and a linked artifact needs a
#[panic_handler] that only std supplies. The shared library builds exactly as before.
What moved off std:
NewV6Error/NewV7Errorimplementcore::error::Error(the same trait —std::error::Error
is a re-export of it, so nothing changes for existing callers).getrandom's std-only error impl now rides thestdfeature instead of being pinned on
unconditionally, so it's no longer forced into every consumer's graph.v7::now_v7is compiled out withoutstd, joining thewasm32gate it already had. It is the
only API that reads a system clock; everything else in the crate is timestamp-in, bytes-out.v7's monotonic counter replacedstd::sync::OnceLockwith a lock-free seed fold over
core::sync::atomic. The seed is added rather than stored, so it commutes with concurrent
increments instead of clobbering one — the RFC 9562 §6.2 ordering guarantee is unchanged.
No allocator required either
new_v6_batch/new_v7_batch were the crate's only allocation: a count-sized scratch buffer for
their single getrandom call. That buffer is gone. The batch's entropy is now drawn into the front
of the caller's own output buffer and moved out to each item's octets as the batch is written
backwards, so there is no scratch space at all — not on the heap, and not a fixed stack frame
either, which would have been a poor thing to charge a microcontroller for.
The crate no longer contains extern crate alloc, so it now also carries the
no-std::no-alloc category. Benchmarked against the previous implementation on the same machine:
v7 batch slightly faster (one less alloc/free), v6 within noise. The published batch speedups
(2.5x v6, 3.6x v7 over an equivalent loop) still hold.
Upgrading
For essentially everyone this is a drop-in patch release: default features are on, and the public
API is byte-for-byte identical.
One exception. If your Cargo.toml already said default-features = false, that line was
inert in v0.1.0 — the crate had no default features, so you got a full std build regardless. On
v0.1.1 it now means what it says, and v7::now_v7 will disappear. Either drop the line, or take
the no_std path deliberately, which additionally needs:
- an entropy backend — off std there's no OS for
getrandomto read from, so build with
RUSTFLAGS='--cfg getrandom_backend="custom"'and export a__getrandom_v03_customsymbol; - your own timestamps — pass them to
v7::new_v7/v6::new_v6in place ofnow_v7.
Also worth knowing: the batch functions draw their entropy before assembling any UUID, so on
NewV6Error::Random/NewV7Error::Random no UUID has been written, but the front of out may hold
partial bytes from the failed draw. Previously out was left untouched on that path. Treat the
buffer as clobbered rather than intact when a batch call returns Err.
C#, Java, Go, Python, Ruby, PHP, Swift
No API change and no behavior change: each binding calls the same C ABI, which is untouched — all
12 exported symbols are identical. They're rebuilt against the new core and republished so the
version stays coordinated across all eight registries.
Two things did change for these packages:
- LICENSE and README now ship inside the package on NuGet, Maven, PyPI and RubyGems, rather
than the license only being named in metadata. Nothing to do on your side; it's there if your
build or compliance tooling looks for the actual text. - Documentation corrections — references to the retired ctypes backend are gone, each
binding's README is now about that binding rather than the repo at large, and all of them carry
CI and registry badges.
Install
Same eight packages, same coordinated version — see the
v0.1.0 notes for the full install
table. As before, Go's module needs its own go/v0.1.1 tag pushed alongside this one, since a
subdirectory module can't be resolved from the bare tag.
Verification
Everything above was measured or compiled, not argued:
cargo check --no-default-features --target thumbv7em-none-eabi— clean against a real
bare-metal target. Added to CI as acheck-no-stdjob, because every other cargo invocation in
the pipeline compiles the std configuration and would never notice ause std::creeping back.cargo build --no-default-featureson the host now fails with two errors, not three: "no global
memory allocator found" is gone, which is the direct proofallocis unlinked.tests/allocation_free.rswraps a counting#[global_allocator]around v4/v5/v6/v7 and both
batch functions, asserting zero allocations for all of them — the batch exception it used to
assert is gone.- New
v7_batch_trailing_entropy_is_distinct_per_item(and its v6 counterpart) pin the in-place
entropy placement. Reversing the backwards walk fails them immediately; every other v7 batch test
still passes, because the monotonic counter keeps values ordered even when the entropy is wrecked. - New
tests/v7_counter_race.rsruns 8 threads x 5,000 same-millisecond calls in a fresh process,
covering the counter's first concurrent calls while the seed is still landing. - 54 unit tests + 2 integration tests green; Go, C# (39), Java (38), Swift (37), PHP (46), Ruby (49,
run twice for both the Magnus and Fiddle backends) and Python (46) all green against a freshly
built native.