Skip to content

Releases: SkunkWerkx/HyperUuid

v0.8.0 - Inspection, SQL Server reads, and Android

Choose a tag to compare

@buvinghausen buvinghausen released this 09 Oct 02:58

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, Variant and IsRfc, in its own spelling. They come from new C exports uuid_version, uuid_variant and uuid_is_rfc; in Rust they are Uuid::variant() and Uuid::is_rfc(version), beside the existing Uuid::version(). Use IsRfc(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-only Version property 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 by Version, IsRfc, V6UnixMillis, V7UnixMillis, V6Timestamp, V7Timestamp and GetTimestamp. A value read back from a uniqueidentifier column 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 MaxV7Batch UUIDs (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 loads runtimes/android-{rid}/native/libhyperuuid.so from its APK. A Native AOT publish links the core in from staticlibs/android-{rid}/libhyperuuid.a.
    Go Under GOOS=android, cgo links go/staticlib/android_{arm64,amd64}, with the NDK's clang as CC.
    Swift The Swift SDK for Android (Swift 6.3+, API 28+) links the artifact bundle's *-unknown-linux-android variants.

    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/*.json pins 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 by corpus/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 RandomSourceError or TimestampOutOfRangeError after 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 a UuidVariant enum instead of an int. $id->variant()->value === 0b10 replaces the old comparison, which is now always false.
  • Ruby: Uuid#variant returns a Symbol (:ncs, :rfc9562, :microsoft, :future) instead of an Integer. variant == :rfc9562 replaces variant == 0b10, which is now always false.
  • Swift: UuidGenerator.Error gains batchTooLarge(count:) and batchNotAddressable(count:). A switch over it with no default needs both cases. The second replaces a crash on a v6 count past UInt32.max.
  • All bindings: a v7 batch larger than MaxV7Batch now fails with an argument error.
  • All bindings except Python: GetTimestamp now requires an RFC 9562 v6 or v7. A value with a 6 or 7 version nibble but another variant returns None/null instead 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/.github

The 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

Choose a tag to compare

@buvinghausen buvinghausen released this 07 Oct 01:26

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 for ios-arm64, iossimulator-arm64, maccatalyst-arm64 and maccatalyst-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 from go/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_v6 and new_v7 used to call getrandom for 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 from getrandom.

    • Speedups on linux-x64: new_v4 went from 38 to 23 ns on glibc and from 228 to 24 ns on musl, and new_v6 is 1.3-1.4x faster.
    • Safe across fork: the pool's memory is marked MADV_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.
  • Batches are about 2.5x faster on Linux, and 3.5x on musl. A large v6 or v7 batch makes one 32-byte getrandom call 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_v5 is about 13% faster (54.7 to 47.3 ns). A new test checks it against the uuid crate 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.uuid for v4.
    • Batches: a 1000-UUID byte fill now costs 3.6-4.9 µs in every binding, down from 9-12 µs.

Fixes

  • Go on Android and iOS no longer links another platform's archive. Go treats GOOS=android as linux and GOOS=ios as darwin, 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 calling getrandom for single UUIDs, as in 0.6.x.
  • C#: iOS and Mac Catalyst builds need the .NET ios/maccatalyst workloads 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/.github

The 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

Choose a tag to compare

@buvinghausen buvinghausen released this 03 Oct 19:57

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

Choose a tag to compare

@buvinghausen buvinghausen released this 03 Oct 02:03
83e2fd5

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 before
    Rust wasm32-unknown-unknown with getrandom's wasm_js feature
    Python (new) Pyodide: await micropip.install("hyperuuid") finds a new pyemscripten wheel on PyPI (~73 KB). The whole test suite runs inside Pyodide in CI.
    Go (new) TinyGo 0.42+ with -target=wasm, the core linked in
    Swift (new) the wasm32-wasip1 build, 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.mod needs only google/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-gnu gem 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-base on Alpine)
    • macOS: the Xcode command-line tools
    • Windows: MinGW-w64 gcc (llvm-mingw on arm64)
  • Unsupported builds fail at compile time, with an error that names the fix. That covers CGO_ENABLED=0, stock Go's GOOS=js/wasip1, and any other platform.
  • Drop the old build tags -tags hyperuuid_dynamic and -tags hyperuuid_wasm.
  • API unchanged: Available() is always true, and ErrNativeUnavailable is deprecated.

Swift.

  • Nothing to deploy any more on macOS and Windows: remove the step that copied HyperUuid_HyperUuid.bundle/.resources beside your executable.
  • NativeLibraryError has no cases now, so code matching .openFailed/.symbolNotFound no 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-linux or 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_WASM is 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.a in bin/ and the publish output.
  • Rust: the no-panic proof from a consumer's crate is documented: it needs lto = true in your release profile.

Proven, not shipped

  • PHP and Ruby in the browser both work.
    • PHP: the ext-php-rs extension runs as a WordPress Playground side module, and php/README.md has 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.
  • 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/.github

The 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

Choose a tag to compare

@buvinghausen buvinghausen released this 02 Oct 18:46
63f0c72

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 new no-panic feature, 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 = false builds on every target. It failed with "#[panic_handler] function required" on any target that can produce a cdylib, including the developer's own machine and wasm32-unknown-unknown. CI now builds a real consumer for both.
  • Uuid::from_str no 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 now ParseUuidError.
  • A short buffer passed to new_v6_batch/new_v7_batch returns BufferTooSmall instead of panicking, and leaves the buffer untouched.
  • v7::now_v7 no longer panics on a clock set before 1970. It returns TimestampOutOfRange, as it now also does for a clock so far ahead that its milliseconds overflow a u64, which used to be silently truncated.
  • Timestamp::to_unix_millis saturates instead of overflowing. In a release build it used to wrap into a valid-looking timestamp, so new_v6_at/new_v7_at minted a UUID for the wrong time.
  • Ruby: the native extension falls back to RuntimeError instead 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/.github

The 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

Choose a tag to compare

@buvinghausen buvinghausen released this 02 Oct 03:49

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-x64 and linux-musl-arm64 are built, attested and shipped in every package (as musllinux_1_2 wheels 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 or scratch image.
    • 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/NativeVersion in 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's native-image build needs no configuration.
  • Python and Ruby calls cost about half.
    • Python: new_v4() 540 → 243 ns, new_v7(ms) 726 → 354 ns, v7_timestamp 466 → 252 ns. Wheels are unchanged (still one abi3 wheel per platform).
    • Ruby: new_v4 317 → 234 ns, new_v7 512 → 284 ns on the Magnus backend.
  • New in Python: v6_unix_millis and v7_unix_millis return the embedded timestamp as an int, with no datetime built (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.parse accepts the 8-4-4-4-12 form only. Bare and misplaced-hyphen strings no longer parse.
  • Python: a datetime passed to new_v6/new_v7 is 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::TimestampOutOfRangeError and RandomSourceError; the Runtime:: names remain as aliases.
  • Go: batch functions return ErrNegativeCount for a negative count. -tags hyperuuid_dynamic keeps the previous loading cgo backend.
  • C#: set HyperUuidStaticLink=false to keep the shared library under Native AOT. Blazor WebAssembly requires .NET 11; an older browser project gets warning HYPERUUID001 and 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 precompiled x86_64-darwin gem; 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 / 16 wrote past a Java array. It is now IllegalArgumentException.
  • Python: new_v6_batch/new_v7_batch narrowed count to 32 bits, so 2**32 + 1 returned one UUID. Out of range is now ValueError.
  • 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/.github

v0.3.0 - A wasm backend in Java, Ruby, Python and Go

Choose a tag to compare

@buvinghausen buvinghausen released this 04 Sep 00:47

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:wasm is compileOnly and 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::BACKEND reports :wasm; spec/wasm_backend_spec.rb pins 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.BACKEND reports "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/.github

Every 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

Choose a tag to compare

@buvinghausen buvinghausen released this 02 Sep 19:28

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

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

Choose a tag to compare

@buvinghausen buvinghausen released this 02 Sep 17:41

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 for FillV6/V7 and V6/V7To/FromSqlOrder, plus a non-throwing Try* twin for every fallible operation (TryNewV4/V6/V7, TryFillV6/V7) so a Result<T>-shaped gateway no longer needs a try/catch per call
  • Go — FillV6/V7, FillV6/V7Bytes (each with an At variant), V6/V7To/FromSqlOrderBytes
  • Swift — fillV6/fillV7 over UnsafeMutableRawBufferPointer and inout [UUID], v6/v7To/FromSqlOrder(bytes:), and a new Error.bufferNotWholeUUIDs
  • Java — fillV6/fillV7 over UUID[] and byte[], v6/v7To/FromSqlOrder(byte[])
  • Python — fill_v6/fill_v7 into a caller's bytearray
  • 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 time

A 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 gnullvm Rust target instead of gnu — 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 = true and codegen-units = 1 on 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

Choose a tag to compare

@buvinghausen buvinghausen released this 31 Aug 21:40

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/NewV7Error implement core::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 the std feature instead of being pinned on
    unconditionally, so it's no longer forced into every consumer's graph.
  • v7::now_v7 is compiled out without std, joining the wasm32 gate 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 replaced std::sync::OnceLock with 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 getrandom to read from, so build with
    RUSTFLAGS='--cfg getrandom_backend="custom"' and export a __getrandom_v03_custom symbol;
  • your own timestamps — pass them to v7::new_v7/v6::new_v6 in place of now_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 a check-no-std job, because every other cargo invocation in
    the pipeline compiles the std configuration and would never notice a use std:: creeping back.
  • cargo build --no-default-features on the host now fails with two errors, not three: "no global
    memory allocator found" is gone, which is the direct proof alloc is unlinked.
  • tests/allocation_free.rs wraps 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.rs runs 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.