Search by

skunkwerkx / hyperuuid

buvinghausen

RFC 9562 UUID v4/v5/v7 generation, with a version 8 SQL Server order — a PHP extension over a Rust core, installed with PIE.

Package info

github.com/SkunkWerkx/HyperUuid

Language:Rust

Type:php-ext

Ext name:ext-hyperuuid

pkg:composer/skunkwerkx/hyperuuid

Statistics

Installs: 71

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.1.0 2026-11-01 07:24 UTC

README

CI crates.io NuGet Maven Central PyPI Go Reference Swift Package RubyGems npm CRAN Packagist C/C++ License: MIT

One RFC 9562-compliant UUID engine, written once in Rust, called in-process — no bridge process, no per-language reimplementation — from C#, Java, Go, Swift, Ruby, PHP, Python, TypeScript, R, C and C++.

What it gives every one of those languages, from the same compiled code:

  • The SQL Server order. A v7 converted to the byte order a uniqueidentifier column sorts by (an RFC 9562 version 8), so an id minted on a client, before any insert, lands in a clustered index in creation order. SQL Server ordering
  • v7 ordering, with its limits written down. One process-wide stream, strictly increasing across threads, single calls and batches, with every bound on clock steps, capacity and what the counter leaks stated. v7 ordering, precisely
  • Byte-identical v5. The same namespace and name give the same 16 bytes in every language, checked against Python's own uuid.uuid5. A conformance corpus, derived by an oracle that shares no code with the core, pins v5, v7's fields and the SQL Server layout, and every binding's suite replays it.

Every other polyglot ID library either reimplements the same generation logic per language (drift risk: nine codebases that can each get the bit-twiddling subtly wrong in different ways) or ships a server/sidecar process to generate IDs centrally (a network round-trip for something that should cost nanoseconds). HyperUuid does neither: a single Rust core is compiled once and reached from inside each language's own process — over a plain C ABI (P/Invoke, FFM, Fiddle, and .Call into a vendored static library for R), linked directly into the language VM as a native extension (PyO3 for CPython, Magnus for CRuby, ext-php-rs for PHP, napi-rs for Node and Bun), or, for Go, translated from its WebAssembly build into Go source the Go compiler builds like any other package — sharing the same address space, the same generation logic, the same test vectors, on every platform. No runtime bridge, no serialization layer, no embedded interpreter.

Speed comes with it, measured rather than asserted: in every language measured on linux-x64, generating a UUID through HyperUuid is as fast as the platform's own facility or faster, and usually several times faster — 16x faster than Foundation.UUID(), 7.9x faster than Guid.NewGuid(), 5.5x faster than SecureRandom.uuid, 3.8x faster than CPython 3.14's own uuid.uuid7(), and in PHP ahead of a naive inline random_bytes v4 that validates nothing. Go, TypeScript and R have so far been measured only on an Apple M6 Mac. Go is 4.7x faster than google/uuid's v4 there and 5.8x faster than its v7, with no boundary to cross at all, and loses its batch to the cgo binding it replaced (why). TypeScript and R lose calls there: TypeScript its v4 to crypto.randomUUID() on Node and Bun and its v7 to the uuid package on Bun (on Node it is 2.3x faster), and R its time-based ids, to the uuid package's v1, while its v4 is 12.7x faster than that package's (their tables).

Quick start

// C# — dotnet add package HyperUuid (nuget.org)
using HyperUuid;

var id = UuidGenerator.NewV7();                     // time-sortable, RFC 9562 §6.2
var ts = UuidGenerator.V7Timestamp(id);              // recover the embedded timestamp
var batch = UuidGenerator.NewV7Batch(1000);          // 1000 IDs, one native call
// Go — go get github.com/SkunkWerkx/HyperUuid/go@latest
import hyperuuid "github.com/SkunkWerkx/HyperUuid/go"

id, err := hyperuuid.NewV7()
ts, err := hyperuuid.V7Timestamp(id)
# Ruby — gem "hyperuuid" (rubygems.org)
require "hyperuuid"

id = HyperUuid.new_v7
id.timestamp

Every binding follows the same shape — new_v4/new_v5/new_v7, a v7 batch variant, v7 timestamp extraction, the SQL Server order conversions, the RFC's Nil/Max constants, and a probe that says whether the native core loaded and which version it is (UuidGenerator.IsAvailable/NativeVersion in C#, and the same pair in each language's idiom), so a consumer with a fallback gates on the probe rather than catching a load failure. R has only the version half, hyperuuid_native_version(): its package does not load at all without its compiled code. PHP's is PHP's own extension_loaded('hyperuuid') beside HyperUuid::nativeVersion(): without the extension its classes do not exist. See each language's own README (linked in the table below) for its exact idiom and install instructions.

SQL Server ordering

V7ToSqlOrder/v7ToSqlOrder/v7_to_sql_order (C#/Java/Go/Swift/Python/TypeScript; Ruby's #to_sql_order and PHP's ->toSqlOrder() are methods on the value, and R's is the vectorised uuid_to_sql_order()) converts an RFC 9562 version 7 UUID to the byte order SQL Server's uniqueidentifier needs on the wire to sort by creation order, and the V7FromSqlOrder/v7FromSqlOrder/v7_from_sql_order counterpart converts it back. System.Data.SqlTypes.SqlGuid comparison — and therefore T-SQL ORDER BY on a uniqueidentifier column — doesn't compare a GUID's 16 bytes left to right; it uses a fixed, non-sequential byte significance order. The converted value is an RFC 9562 version 8. RFC 9562 defines v7's layout in network byte order and sends any other byte order to version 8 (§6.11), so the conversion writes an 8 and the RFC variant where SQL Server's text form and a .NET Guid show them, and fills the other 122 bits in the comparator's order: the timestamp and counter (the two fields that determine creation order) in the most-significant bytes, then the trailing 48 random bits, which carry no ordering information, as one intact block in the least-significant ones. That is the permutation this project's own SequentialGuid uses for C#, with an 8 where SequentialGuid keeps the 7. It is computed once in the Rust core and exported over FFI so every binding gets them from the same verified source instead of a nine-times-reimplemented one. Verified against the real System.Data.SqlTypes.SqlGuid comparator (not a hand-rolled stand-in) in the C# test suite; every other binding verifies the same sort behavior against a comparator replicating SqlGuid's documented byte order.

When this actually matters: SQL Server already ships its own native answer to GUID-clustering fragmentation — NEWSEQUENTIALID() — but it only runs inside SQL Server, as a column default at insert time. It can't help a frontier device (a mobile client, an edge node, anything generating records before it ever talks to the database) that needs to mint its own final row ID before that insert happens. This feature is what closes that gap: generate a v7 UUID on the device, convert it to SQL order, and the exact same value that was minted on the frontier is what lands in the clustered uniqueidentifier column — no round trip to the server first just to obtain a key, and no swapping identities between a client-side temp ID and a server-assigned real one.

That value arrives at the server somewhat out of strict minting order — real sync/queue/wire latency between device and database means insert order isn't quite generation order — so it's not as perfectly gap-free as a value SQL Server assigned to itself the instant before insert. It's still a bounded, mostly-monotonic disorder window, not the fully uniform randomness of a v4 GUID spread across the entire 128-bit space — a materially smaller number of page splits than random insert order produces, even with realistic sync delay. And it solves a problem IDENTITY(1,1) can't solve at all, not just less efficiently: an IDENTITY value doesn't exist until after the row is physically inserted, so anything that needs to reference that row before then — most concretely, a many-to-many bridge/junction table's composite key, built at the same time as the rows it links — has to insert first and come back for the generated key. A client-generated v7 ID needs no such round trip; it's already known at the moment it's needed everywhere, the bridge table included.

Why version 8. The 8 keeps the value honest wherever it is read as a UUID: SQL Server's text, a .NET Guid and a driver's UUID type all show a version 8, a custom layout, so nothing reads a version 7 timestamp out of bytes that no longer hold one there. It costs nothing: a v7 and a v8 have the same 122 free bits, so every v7 converts and the conversion back is exact. The conversion refuses only a value that isn't a v7 going in, or doesn't carry the layout's 8 and variant coming back, with the binding's invalid-argument error.

That's also the load-bearing condition for this feature to matter at all — a random or naively-generated GUID only fragments a clustered index because SQL Server always maintains a clustered index's physical sort order on every insert; nothing has to be "turned on" for that to happen, but nothing here helps if the column isn't the clustered key in the first place. And plenty of real-world SQL Server schemas sidestep the whole problem by never clustering on the GUID at all — an IDENTITY/sequence integer as the clustered key, with the GUID kept as an ordinary non-clustered unique column — which remains a completely reasonable choice if there's no frontier-generated-ID requirement driving the decision.

Binding it outside .NET. SQL Server reads a UUID's text form mixed-endian and stores the first three groups byte-reversed. Every driver outside .NET that was tested sends its own UUID type the same way it sends text. So binding the converted value as a UUID, or as a string, stores bytes other than the ones to_sql_order produced, and the column does not sort by creation. Against SQL Server 2022 that held for mssql-jdbc 13.6, go-mssqldb 1.11 (by default), pyodbc 5.3, mssql-python 1.15, pymssql 2.4, tiberius 0.13, pdo_sqlsrv 5.13 and tiny_tds 3.4. Bind the 16 bytes as binary instead: SQL Server's binary-to-uniqueidentifier conversion keeps them as they are. Reading has the same trap in reverse, because those drivers hand back a UUID with its first three groups reversed. Select CAST(col AS binary(16)), or use the driver's raw-bytes getter, to get the bytes the SQL Server layout reads take. Each binding's README shows its own form. .NET is the exception: ADO.NET sends a Guid as its ToByteArray() bytes, so the Guid binds as it is.

Reading a value still in SQL order. A value read back from a uniqueidentifier column (as raw bytes outside .NET, per the paragraph above) can be validated and its timestamp read without permuting it back first. Version, IsRfc, V7UnixMillis, V7Timestamp and GetTimestamp each take a layout (UuidLayout.SqlServer and its spellings), and the core reads the permuted bytes in place: one native call per question instead of a permute and then a read. In SQL Server layout Version answers 7, the version a value was converted from. Bytes without the layout's 8 and variant read as version 0 with no timestamp. The layout is the caller's to know, not something a value can reveal: bytes in RFC order can happen to carry an 8 and the variant where the layout puts them (a random v4 does one time in 16), so keep the layout alongside the value, the way a column or a converter already does. For the same reason, a conversion handed raw bytes in the wrong order succeeds about one time in 16: converting a value already in SQL order to SQL order again, or converting a v7 still in RFC order back from it. C#'s Guid overloads refuse both every time, because a Guid puts its version nibble where the layout puts the 8: a SQL-ordered Guid reads as a version 8, and an RFC-ordered v7 shows its 7 there.

The layout is persisted data. A value written in SQL order sits in a clustered index, so moving one bit in the layout would silently reorder every existing row. Every property test (round trips, sort order) would still pass. The conformance corpus pins the exact bytes instead, in both directions, along with the refusals. An oracle derives them in bit strings from RFC 9562's field table, and they are cross-checked against a port of SequentialGuid's permutation, which derives them from .NET's mixed-endian Guid layout and differs only in the 8.

v7 ordering, precisely

Every v7 call reserves the next step of one process-wide stream. The stream is re-seeded at random whenever a timestamp moves it forward, so counters do not count UUIDs across milliseconds. Each millisecond holds 2^20 steps, a single call consumes one step and a batch of n consumes ⌈n/64⌉. These guarantees hold on targets with 64-bit atomics, for timestamps up to 2^44 − 2 (the year 2527). Let S be the newest timestamp among the stream reservations that have succeeded; failed and out-of-stream calls never change it.

  • Ordering. A call whose timestamp is within 10 s of S either fails with CounterExhausted or reserves from the stream, and its UUIDs then sort strictly after every UUID from a stream reservation that completed before it began, on any thread. Overlapping reservations, singles and batches alike, are ordered by reservation, which need not be completion order. A timestamp up to 11 s behind the stream is clamped onto the stream's millisecond, and a millisecond whose steps run out splits into the next, so a batch that crosses the split is still strictly increasing.
  • Embedded timestamp. It runs at most 11 s ahead of the supplied one: a clamped call carries the stream's millisecond, a split's tail carries the millisecond after it (at most 1000 ms ahead of the supplied timestamp), and an out-of-stream call carries its own timestamp exactly.
  • Backfill and future-dated calls. A timestamp more than 11 s behind the stream, or past 2^44 − 2, is minted at itself, outside the stream: it succeeds, leaves the stream alone and is ordered against nothing. One call far in the future therefore advances the stream, and until the clock catches up, real-time calls fall behind it and are minted out of stream, unordered among themselves. A clock that steps back by more than 11 s does the same.
  • Capacity and exhaustion. A millisecond entered by an advance holds at least 2^19 + 1 steps, and one entered by a split holds 2^20. Demand above that splits forward, and the stream may drift up to 1000 ms ahead of the supplied timestamp. A split that would take it further, or past 2^44 − 2, fails with CounterExhausted (return code 6 over the C ABI) before anything is reserved. It is a state error, not an argument error: it reserves nothing, and a call whose timestamp is past the stream's millisecond succeeds. No throughput floor is claimed.
  • What the counter leaks. Only consumed steps, and only for stream-generated values. Within one millisecond the step difference between two values is exactly the number of steps consumed between them, and the raw counters of adjacent singles differ by 1 to 127. Across a split, from step s_a at one millisecond to step s_b at the next, the rollover-adjusted difference (2^20 − 1 − s_a) + (s_b + 1) is exactly the steps consumed between them; splits happen only while the stream is overloaded and ahead of real time. Across a real advance the re-seed hides the count. A lone step value shows at least step − (2^19 − 1) steps consumed in its millisecond. An out-of-stream value draws its own seed and relates to nothing.
  • A batch takes at most 67,108,864 UUIDs (MaxV7Batch and its spellings). Every binding refuses a larger one with an argument error before allocating anything.
  • The timestamp stops at 9999-12-31T23:59:59.999Z (253,402,300,799,999 ms), in both directions. Minting one millisecond later is the out-of-range error, as a negative timestamp is, and the version-agnostic timestamp door treats a version 7 dated past that as it treats a version 6: no timestamp. The 48-bit field itself reaches 10889, and the per-version millisecond reader (V7UnixMillis and its spellings) reads a UUID another generator dated past 9999 as the field says. The cap is the last instant .NET's DateTime and Python's datetime can name, so every binding's time type states the whole range.
  • No 64-bit atomics. Targets without them (thumbv7em-none-eabi and the like) keep a never-reset 26-bit counter that wraps every 67,108,864 UUIDs. None of the guarantees above apply there: a batch that straddles the wrap is stamped from the wrap on a millisecond later, and individual calls either side of the wrap sort in reverse.

The timestamp is the caller's, not read from the clock, so a clock that goes backwards by less than 11 s is absorbed by the clamp, and one that goes back further is minted out of stream.

RFC 9562 coverage

Version Purpose RFC section
v4 Cryptographically random §5.4
v5 Deterministic, namespace + name, SHA-1 (cross-language interoperable — the same (namespace, name) pair produces the same UUID everywhere, including against Python's own uuid.uuid5) §5.5
v7 Time-ordered, 48-bit Unix-ms timestamp + a counter re-seeded each millisecond + random bits — strictly increasing within one process's stream, even under concurrent generation (see v7 ordering, precisely) §6.2
v8 Only as the SQL Server order of a v7: the same 122 bits in the order uniqueidentifier sorts by §5.8 / §6.11
Nil / Max The all-zero and all-one special values §5.9 / §5.10
Inspection Version, variant (NCS / RFC 9562 / Microsoft / Future), and a one-call "is this an RFC 9562 UUID of version N" guard, in RFC order or straight from SQL Server order §4.1 / §4.2

v1 (classic time-based, leaks a MAC-derived node ID), v3 (MD5 name-based) and v6 (v1's fields reordered to sort) are deliberately not implemented. RFC 9562 treats v3 as superseded by v5, and v6 exists for systems that already speak v1: "Systems that do not involve legacy UUIDv1 SHOULD use UUIDv7 (Section 5.7) instead" (§5.6). A v7 embeds a timestamp *Timestamp/*_timestamp can recover on every binding; a v1 or v6 minted elsewhere still reports its version, and carries no timestamp here.

Inspection is layout knowledge, so it lives in the core like everything else. Every binding exposes the same three doors through it, each in its own spelling: Version, Variant and IsRfc. The version-agnostic GetTimestamp applies the same guard: a 7 nibble under another variant is no RFC version, so it carries no timestamp. The guard is what a wrap constructor or a deserializer runs on every value: IsRfc(id, 7) is true only for the RFC variant with version nibble 7. A platform's own Guid.Version or UUID.version() reads the nibble alone, so a Microsoft-variant value with a 7 in it passes that check.

Why not your platform's built-in UUID call?

Most languages do already have one — Guid.NewGuid(), java.util.UUID.randomUUID(), uuid.uuid4(), SecureRandom.uuid. HyperUuid isn't arguing you should never use those. It's for the specific, common situation where you need more than plain v4 randomness gives you:

  1. Time-sortable IDs with real ordering guarantees. A v7 ID minted a microsecond after another one you generated on the same thread will sort after it — HyperUuid's process-wide v7 stream (RFC 9562 §6.2 Method 1) guarantees that even under concurrent generation. Most stdlib v4 generators have no time-ordering story at all, and even a stdlib that does offer v7 (C#'s Guid.CreateVersion7, added in .NET 9) doesn't implement the counter, so two IDs minted in the same millisecond sort randomly relative to each other — exactly the index-fragmentation problem v7 adoption is meant to solve in the first place.
  2. One generation engine across a polyglot system. If your API is C#, your batch jobs are Go, and your data pipeline is Python, plain per-language UUID libraries give you three independent implementations that all should agree bit-for-bit on RFC 9562 semantics but have no structural reason to. HyperUuid's v5 namespace UUIDs are verified in CI to match byte-for-byte with Python's own uuid.uuid5 — because it's the literal same Rust code minting them everywhere, not three ports of the same spec.
  3. Batch throughput, and a byte-level door under it. Need to backfill a million IDs? NewV7Batch/new_v7_batch/NewV7BatchAt (binding-dependent naming) shares one timestamp capture and one contiguous counter reservation across the whole batch instead of paying per-item overhead N times — 1.3-15x faster than the equivalent loop depending on binding (numbers below and in each binding's README). Underneath that, every binding also exposes a raw-bytes form that constructs no UUID objects at all, which beats its own batch by 41x in Ruby, 36x in Python and 16x in PHP (measured before PHP's extension) and lands every language measured on linux-x64 on the same floor, 3.6-4.9 µs per 1000 UUIDs (numbers below). Most stdlib UUID facilities have no batch API at all, let alone both. (Measured before the per-millisecond re-seed, core change, 2026-10-27.)
  4. It's not slower for the trouble — it's faster, mostly. Generation beats the platform's own call outright in every language measured on linux-x64, and in Go on the Apple M6 by 4.7-5.8x. TypeScript, measured so far only on that Mac, loses its v4 there on both runtimes and its v7 on Bun, and R's time-based ids lose to the uuid package's v1 (see the scoreboard).

The honest trade-off: this is one more native dependency to ship (a platform-specific libhyperuuid.so/.dylib/.dll, or the prebuilt extension Python, Ruby, PHP and TypeScript install; Go alone carries the core as source) versus a UUID call that's already sitting in your standard library. If you only need plain v4 randomness and don't care about cross-language consistency, the stdlib call is simpler and that's a completely reasonable choice.

The scoreboard

By language — real numbers, no adjustment for story, the receipts in docs/benchmarks.md and each binding's own README. What each of them cost to learn, binding by binding, with what we believed, what we measured and what we should have known: docs/LESSONS_LEARNED.md.

Language Generation vs. the platform's own call The platform's own call
Swift 11-16x faster Foundation.UUID()
C# 7.5-7.9x faster Guid.NewGuid()
Ruby 2.1-5.5x faster SecureRandom.uuid
Python 3.4-4.2x faster uuid.uuid4()-uuid7()
Java 2.8-5.1x faster UUID.randomUUID()
Rust 1.7-2.4x faster the uuid crate
PHP 1.2-1.4x faster a naive inline v4 (PHP core has no UUID call at all)
R 12.7-29x faster for v4; 1.2-1.3x slower for time-based ids, a v7 against a v1 the uuid package's UUIDgenerate()
TypeScript 1.2-6.4x slower per call, except v7 on Node: 2.3x faster crypto.randomUUID(), the uuid package's v7()
Go 4.7-5.8x faster; the batch 1.6x slower than the cgo binding it replaced google/uuid's NewRandom() and NewV7()

The Go, R and TypeScript rows were measured on an Apple M6 (Mac18,5) under macOS, every other row on linux-x64 on an Intel Core i9-11900H, so the two groups are not comparable with each other. The PHP row predates the extension: it measured 1.0's FFI package. The linux-x64 rows' v7 figures were measured before the per-millisecond re-seed (core change, 2026-10-27); the Apple M6 rows after it, and after the switch to CCRandomGenerateBytes on Apple platforms (2026-10-28).

Regardless of where your language lands above: if SQL Server is your RDBMS, SQL Server ordering below might be reason enough to reach for this on its own — the only practical way to mint a client-side ID on a frontier device and have it arrive already sorted for clustering, something NEWSEQUENTIALID() structurally can't do (server-side only) and something IDENTITY(1,1) can't do at all for a value — a many-to-many bridge table's composite key, most concretely — that needs to exist before the row does.

Go: no boundary at all

Go's binding crosses nothing. The Rust core is built for wasm32-wasip1, translated to one Go source file by wasm2go and committed (go/internal/core/core.go, regenerated and diffed in CI), so the Go compiler compiles the core like any other package and a call into it is a Go call. No cgo, no C compiler, no binary in git, CGO_ENABLED=0 is fine, and it builds for every GOOS and GOARCH Go has, the browser and WASI included. The core's one import, WASI's random_get, is served by the binding: from an AES-256-CTR keystream keyed from crypto/rand and rekeyed after 1 MiB or one second where Go's own AES is hardware-accelerated (the same CPU-feature rule Go's standard library applies, read through golang.org/x/sys/cpu), and from crypto/rand itself everywhere else.

Measured on the Apple M6 (Go 1.27.3), the shipped binding against google/uuid, which asks the operating system for every id:

Call hyperuuid google/uuid
v4 37.5 ns, 0 allocs NewRandom() 177 ns, 1 alloc 4.7x faster
v7 37.2 ns, 0 allocs (NewV7At, explicit timestamp) NewV7() 214 ns, 1 alloc 5.8x faster
v7 timestamp read-back V7Timestamp 7.0 ns id.Time() 1.4 ns 5.0x slower: prefer id.Time() in Go

The losses, beside them. The batch: 1000 v7s in one call cost 2.91 µs here, where the cgo binding this replaced did it in 1.84 µs on the same machine, because the translated core walks each item in bounds-checked Go rather than native Rust; it is still 13x faster than 1000 single calls. Linux arm64 (Docker on the same Mac): v4 level with google/uuid and v7 1.8x faster, both 1.3-1.4x behind the cgo binding's own single calls there; twelve goroutines minting v4s run at 9.3 ns a call aggregate, where the cgo binding's archive ran at 4.9 ns there. The browser and WASI: 333 ns a v4 under stock Go's js/wasm in Node and 154 ns under TinyGo on wasip1, the operating system's source each time since no wasm host has hardware AES to establish, and google/uuid is ahead on a v4 on both (2.6x under TinyGo) and level or behind on a v7; the batch is where the binding wins there. Every number, the memory the guest holds, the per-architecture randomness rule and what the sandbox buys: go/README.md.

Platform support

Every language, on every platform, proven for real: .github/workflows/ci.yml's core job builds the Rust core fresh on each of 5 real-hardware legs and the two musl RIDs (in real Alpine containers), and each language's test-<binding> job then runs that language's actual test suite on the same hosts against that leg's freshly-built native library (Go's against the committed translation of the same core; the musl suites on the language's own official *-alpine image) — not just that it compiles. A pull request runs the bindings it touched; a dispatch runs them all. Intel macOS has no leg of its own; see the osx-x64 note under the table.

Language linux-x64 linux-arm64 linux-musl-x64 linux-musl-arm64 osx-x64 osx-arm64 win-x64 win-arm64 Status
Rust (core) ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ crates.io
C# ✅ ✅ ✅ ✅ built ✅ ✅ ✅ NuGet
Java ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ Maven Central
Go ✅ ✅ ✅ ✅ built ✅ ✅ ✅ go get (git tag)
Swift ✅ ✅ ✅ ✅ built ✅ ✅ ✅ .package(url:) (git tag)
Ruby ✅ ✅ ✅ ✅ Fiddle, built ✅ ✅ ✅ RubyGems
PHP ✅ ✅ ✅ ✅ — ✅ ✅ — pie install (Packagist; zips on the GitHub Release)
Python ✅ ✅ ✅ ✅ built ✅ ✅ ✅ PyPI
TypeScript ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ npm
R source source source source source source source experimental CRAN
C, C++ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ GitHub Release (one archive per RID)

The cells that are not a plain ✅, and why each is deliberate:

  • osx-x64 (Intel macOS): built, and tested at the core, in TypeScript and in Java only. There is no Intel macOS CI leg. The library is cross-compiled on the Apple silicon runner, attested, and shipped in every package that carries one, and the Rust core's own suite runs on it there under Rosetta 2, which is why that row keeps its ✅. TypeScript's and Java's are the two binding suites that run on Intel macOS, TypeScript's on an x64 Node under Rosetta 2 against an addon cross-built there, Java's on x64 Temurin 21 and 25 under Rosetta 2 (the JNI path and the FFM path), which is why their rows have a ✅; every other one still runs on Apple silicon against the same source. Ruby there installs the universal gem and runs on Fiddle, since no precompiled gem is built for it. The leg took 37 minutes against 8 on Apple silicon, for hardware Apple stopped selling in 2023 on runners GitHub retires by Fall 2027.
  • PHP on osx-x64 and win-arm64: no zip. Intel macOS: Homebrew bottles no PHP for it any more, and no leg compiles a PHP to build against. Windows on ARM: PHP has never shipped a native Windows ARM64 build, so it runs as an x64 process there regardless of host CPU and takes the win-x64 zip — already exercised for real by the win-x64 leg.
  • TypeScript on Bun. The suite runs on Node on every leg and in both musl containers, and on Bun everywhere but win-arm64, where Bun is not yet verified, and osx-x64, whose cross-build leg runs Node alone.
  • C and C++: one archive per RID, plus Android, iOS, Mac Catalyst and WebAssembly. CI packs every archive from the run's own libraries with the release's packer, then on each RID's runner builds a C consumer and a C++23 one against it through CMake, and the C one through pkg-config, shared and static (osx-x64 under Rosetta 2, musl in Alpine). The iOS, iOS simulator and Mac Catalyst archives are static only and built against on a Mac: linked for a device, run in the simulator, and run as Mac Catalyst on both architectures. The android-arm64, android-x64 and wasi-wasm32 archives are packed but not run: no runner executes them, and their libraries are the ones C# and Swift already test there.
  • R: source. R ships a source package: CRAN compiles it for each platform it serves and builds its own Windows and macOS binaries, and a source install needs Rust 1.89 or newer. CI runs R CMD check --as-cran on linux-x64, linux-arm64, osx-arm64 and win-x64, on the current R and, on linux-x64, the previous minor too; the musl and osx-x64 cells are source builds no job here checks. On win-arm64 the build picks aarch64-pc-windows-gnullvm; R on Windows ARM64 is experimental, and that build is untested.

The musl libraries are built so that they depend on musl's libc and nothing else — the unwinder is linked statically — which is what lets them load on a bare alpine or python:alpine image with no libgcc installed. The glibc libraries need glibc 2.34 or newer. The TypeScript addon links Rust's std, so that floor is not its floor; its own is not stated here yet.

Runtime floors follow upstream support: a version that has reached end of life is not a floor. Today that is Go 1.26, Python 3.11, Ruby 3.3, PHP 8.3, Node 22.14 (the first release with Node-API 10) and .NET 10, and the musl job runs the Ruby, Python and TypeScript suites on those oldest versions as well as the newest (for Node, on 22, 24 and 26), and Go's once, on its floor, the one Go that job needs. PHP's extension is built per minor, so a release run builds and tests 8.3, 8.4 and 8.5 on every platform, and a pull request the newest everywhere and 8.3 on linux-x64. R follows CRAN's window instead: CI checks the current release and the previous minor, and the package declares R 4.2. C# covers Windows, Linux, macOS and Blazor WebAssembly on .NET 10; iOS, Mac Catalyst and Android need .NET 11. JDK 21 sits above that line, though 17 is still supported upstream (Temurin to 2027-10-31): Temurin builds no JDK 17 for Windows arm64 or musl arm64, two of the jar's eight platforms. Swift's floor is 6.2 for a different reason: it is the first release whose package manager can link a static library, which is how the binding reaches musl and WebAssembly at all. CI runs it on 6.2 as well as 6.4. The crate's own floor is Rust 1.89 (rust-version in its manifest, the floor of the current uuid 1.x), and CI checks the library, with and without std, on exactly that toolchain.

How each binding reaches the core (linked into the executable for Swift and C#'s Native AOT, compiled as Go source for Go), iOS, Mac Catalyst and Android, and how each one is published: docs/platforms.md.

WebAssembly

Seven of these languages compile into a WebAssembly app with the core inside it: Rust on wasm32-wasip1, C# in Blazor WebAssembly (.NET 10+), Python under Pyodide, Ruby through rbwasm build, Go under stock Go (GOOS=js, GOOS=wasip1) and TinyGo alike, since its core is Go source, Swift with swift.org's WebAssembly SDK, and C from the wasi-wasm32 archive. PHP's is proven and unshipped, Java's is blocked on its compilers, TypeScript's is the fallback package every install carries (tested in Chromium), and R's is not built. docs/platforms.md has the table, binding by binding, and why each gap is where it is.

Provenance

Every file a release ships verifies from the tag alone. One release attestation, signed by this repository's own release.yml at the tag, names every registry package (the .nupkg as packed and as nuget.org serves it, the gems, the wheels and the sdist, the crate, the Maven files, the ten npm tarballs and the R source tarball), Swift's two zips, the fifteen C archives and the 36 PHP extension zips on the GitHub Release, and its predicate is the release inventory: every file's path, size, SHA-256, the commit it was built from and the repository that signed its build provenance. One command, any file:

gh attestation verify <file> --repo SkunkWerkx/HyperUuid \
  --source-ref refs/tags/v<version> --predicate-type https://github.com/SkunkWerkx/.github/predicates/release/v1

Which identity signs each artifact's build provenance, and the gates on the way in: docs/provenance.md.

Key features

  • RFC 9562 compliant — correct version nibble and variant bits on every UUID, from every binding, because they all come from the same Rust core
  • One implementation, every call site — no per-language reimplementation to drift out of sync; v5's SHA-1 hashing, v7's counter stream and the SQL Server permutation are each written exactly once
  • Faster than the platform's own call — in every language measured on linux-x64, measured per binding with that ecosystem's own benchmark harness, and in Go on the Apple M6; TypeScript and R, measured so far only on that Mac, lose calls there (their tables)
  • Monotonically increasing v7 — a process-global stream (RFC 9562 §6.2 Method 1) guarantees strict ordering under concurrency, continued correctly across individual and batch calls, and a batch stays in order across a millisecond's end (precisely); when the stream cannot advance, the call fails with CounterExhausted
  • Batch generation — NewV7Batch/new_v7_batch amortizes timestamp capture, counter reservation, and the random-bytes fetch across the whole batch
  • Fork- and snapshot-aware randomness — the random bits come from the OS (getrandom, Windows' ProcessPrng, Apple's CCRandomGenerateBytes) or, on Linux and Android, a ChaCha20 keystream keyed from it. Linux buffers keystream in a mapping marked MADV_WIPEONFORK, so a forked child starts empty and keys its own (a kernel that cannot honour that flag skips the buffer), and no key outlives a second of wall-clock time, so a process restored from a snapshot (AWS Lambda SnapStart, CRaC, a cloned VM) throws away what it held, unless the snapshot was taken within a second of a key being drawn and restored onto a clock that has not moved on (the details). Tests on Linux and Apple fork a child and require it to draw different bytes
  • SQL Server byte ordering — V7ToSqlOrder/v7_to_sql_order, as an RFC 9562 version 8, computed once in the Rust core and exported to every binding, verified against the real System.Data.SqlTypes.SqlGuid comparator, and validated and read in place without converting back
  • Inspection — version, variant and a one-call RFC guard, in either layout, from the core
  • One conformance corpus — corpus/*.json pins v5 derivation, the v7 field layout, the SQL Server layout and its refusals, timestamp extraction and inspection. The Rust suite and every binding's suite replay it, and corpus/oracle.py re-derives every vector from code that shares nothing with the core
  • No runtime bridge — direct FFI (P/Invoke, FFM, JNI, Fiddle, R's .Call), the Rust core linked directly into the VM as a native extension (PyO3, Magnus, ext-php-rs, napi-rs), or, in Go, the core compiled as Go source, never a serialization protocol — with Ruby's zero-compile Fiddle fallback kept fully supported and test-verified against the Magnus fast path
  • Genuinely allocation-free where it counts — verified with a counting allocator in Rust and [MemoryDiagnoser] in C#, not just claimed
  • AOT-friendly — C# publishes cleanly under PublishAot, with the core linked into the executable rather than sitting beside it; Java's JVM binding survives a real GraalVM Native Image build into a standalone native binary, no JVM required to run it
  • CI-proven, not CI-claimed — 5 real-hardware platforms plus two musl RIDs in Alpine containers × 10 language/runtime targets (R is checked on four of the five hardware legs and not in Alpine; Intel macOS is cross-built and tested at the core, in TypeScript and in Java), each running that language's actual test suite against a freshly-built native library on every dispatch

Layout

rust/       the core: twelve uuid_* exports plus hyperuuid_version, allocation-free and no_std; a cdylib (and two for Android), fifteen static archives (four for iOS and Mac Catalyst, two for Android) and the Blazor WebAssembly one
corpus/     the conformance vectors every suite replays, and the independent oracle that derives them
csharp/     the .NET 10 and 11 binding: UuidGenerator over LibraryImport, AOT smoke test, Blazor wasm
java/       the JDK 21+ binding: JNI on 21–24 and FFM on 25+ from one multi-release jar,
            Native Image smoke test
java/jni/   the JNI library, a no_std Rust cdylib linking the core in
python/     the 3.11+ binding: PyO3 native extension (abi3 and free-threaded wheels, Pyodide in the browser)
swift/      the SwiftPM binding: the core linked in as a static library on every platform
go/         the Go binding: the core as Go source, translated by wasm2go from its wasm32-wasip1 build;
            stock Go and TinyGo alike, browser and WASI included
ruby/       the 3.3+ binding: Magnus extension + Fiddle fallback
php/        the 8.3+ binding's suite, benchmarks and stubs: the extension itself is rust/'s php feature (ext-php-rs), installed by PIE
typescript/ the Node 22.14+ and Bun binding: a napi-rs addon with the core linked in, one npm platform package per RID, and the WebAssembly fallback package loaded where none fits
r/          the R 4.2+ binding: .Call into a C shim over the core's static library, built from vendored crates
include/    hyperuuid.h, the C header, generated by cbindgen from rust/src/ffi.rs, and
            hyperuuid.hpp, the hand-written C++23 header over it
c/          the C archives' CMake and pkg-config files, and C and C++ smoke consumers
docs/       benchmarks, platforms and provenance in depth; the lessons learned, the roadmap, the CRAN
            checklist, and the design notes and spike receipts under superpowers/

Why "Hyper"

The SkunkWerkx Hyper* series — HyperUuid, HyperCast, HyperTabular — owes its founding attitude to Casey Muratori and his recent YouTube talks on what "premature optimization" actually meant. Knuth's line gets quoted as a license to never care; Muratori's point is that most slow software was never optimized badly — it was pessimized by default: allocations nobody needed, layers nobody asked for, work done and thrown away on every call. These libraries are that argument, practiced: allocation-free cores, no runtime bridge, no reflection, fast paths for the common shape — and every performance claim a measured receipt, because the other half of taking performance seriously is refusing to assert it.

Contributing

Pull requests and issues are welcome. .github/workflows/ci.yml builds the core on every host and runs each binding's suite when the pull request could have broken it (its own tree, the core or the workflows changed); pr-board is the one status to keep green before merging.

License

MIT