hyperuuid 0.6.0-aarch64-linux-musl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: df842f35933b133dacb0fff7e972a74a6a6a7bc23268bcf9d59316e83e13b0c7
4
+ data.tar.gz: 4068551ff41f1f8f220703158c1347db49d27543cf086976af5a7302189019a1
5
+ SHA512:
6
+ metadata.gz: 8f31b85bb00425be9a181ac9de8f5a2c68820d7b1b92457da7fe707a28d8e3c25ea9e6d6a3606c90965fbe0e6250d3b9e6e227f7544d2c080887b6de0a37dde2
7
+ data.tar.gz: a4b6958b623b7f81e8388243585900a40772f8922e282beac516920e3e5b6acf4a56b92571c0e3cb6205dfcde3920e5c140c5faee02a7d29448d1c3a6f4250fb
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Skunk Werkx
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,375 @@
1
+ # hyperuuid
2
+
3
+ [![CI](https://github.com/SkunkWerkx/HyperUuid/actions/workflows/ci.yml/badge.svg)](https://github.com/SkunkWerkx/HyperUuid/actions/workflows/ci.yml)
4
+ [![RubyGems](https://img.shields.io/gem/v/hyperuuid.svg)](https://rubygems.org/gems/hyperuuid)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/SkunkWerkx/HyperUuid/blob/master/LICENSE)
6
+
7
+ **Ruby's own stdlib stops at `SecureRandom.uuid` — random v4, full stop. No v5, no v6, no v7. This gem is the whole RFC, with zero gem dependency beyond `Fiddle` (which ships with every Ruby install) — and it's faster than `SecureRandom.uuid` too.**
8
+
9
+ RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation, with
10
+ two backends sharing one public surface. Ruby 3.3 is the floor. The fast path is a native
11
+ extension built with [Magnus](https://github.com/matsadler/magnus) — the Rust core linked
12
+ directly into the Ruby VM, auto-selected when loadable — which redefines the low-level
13
+ `Runtime` methods in place on require; everything above them (`Uuid`, the module doors and
14
+ their argument checks, batch slicing) is shared byte-for-byte between backends. The
15
+ last-resort fallback, in the universal gem only, calls the native `libhyperuuid` shared
16
+ library via [`Fiddle`](https://docs.ruby-lang.org/en/master/Fiddle.html) — dlopen/dlsym plus
17
+ a raw C-ABI call, no runtime bridge. `HyperUuid::BACKEND` reports which one is live;
18
+ `HYPERUUID_PURE=1` forces Fiddle for testing — see [Backends](#backends).
19
+
20
+ ```ruby
21
+ require "hyperuuid"
22
+
23
+ id = HyperUuid.new_v4
24
+ id2 = HyperUuid.new_v5(HyperUuid::Namespaces::DNS, "example.com")
25
+ id3 = HyperUuid.new_v6
26
+ id4 = HyperUuid.new_v7
27
+
28
+ id4.timestamp # recover the embedded UTC Time
29
+ id4.timestamp(raise_on_mismatch: false) # nil instead of raising if id4 isn't v6/v7
30
+ id4.to_sql_order # byte order SQL Server's uniqueidentifier needs to sort by creation order
31
+
32
+ # One native call, one random-bytes fetch, one counter reservation for the whole batch:
33
+ batch = HyperUuid.new_v7_batch(1000)
34
+ ```
35
+
36
+ ## The doors
37
+
38
+ | Door | Returns | Takes |
39
+ |---|---|---|
40
+ | `new_v4` | `Uuid` | — |
41
+ | `new_v5(namespace, name)` | `Uuid`, the same one for the same pair | a `Uuid` namespace (`Namespaces::DNS`/`URL`/`OID`/`X500`, or your own) and a `String` name — text is hashed as UTF-8, a binary String as its bytes, and it may be empty |
42
+ | `new_v6(time = nil)` `new_v7(time = nil)` | `Uuid` | nothing (now), a `Time`, or an Integer of Unix-epoch milliseconds |
43
+ | `new_v6_batch(count, time = nil)` `new_v7_batch(count, time = nil)` | `Array` of `Uuid` | a count, and the same time forms |
44
+ | `new_v6_batch_bytes(count, time = nil)` `new_v7_batch_bytes(count, time = nil)` | one binary `String`, 16 bytes per UUID | as the batch doors — see [Bulk generation into bytes](#bulk-generation-into-bytes) |
45
+ | `native_version` | the loaded core's `"major.minor.patch"` | — |
46
+ | `available?` | `true`/`false`, never raising | — |
47
+
48
+ Every generating door returns `HyperUuid::Uuid`, a minimal value object — this gem has no
49
+ runtime dependency on the `uuid` gem:
50
+
51
+ | On `Uuid` | |
52
+ |---|---|
53
+ | `Uuid.parse(text)` | the 8-4-4-4-12 hyphenated form `#to_s` produces, either case, and nothing else — an `ArgumentError` otherwise |
54
+ | `Uuid.new(bytes)` | wraps 16 raw RFC 9562-ordered bytes |
55
+ | `Uuid::NIL` `Uuid::MAX` | the RFC 9562 §5.9/§5.10 special values |
56
+ | `#bytes` `#to_s` `#version` `#variant` | the frozen binary String, the hyphenated text, the version nibble, the variant bits |
57
+ | `#timestamp(raise_on_mismatch: true)` | the UTC `Time` embedded in a version 6 or 7 UUID; `nil` instead of an `ArgumentError` for any other version when passed `false` |
58
+ | `#to_sql_order` `#from_sql_order` | to and from the byte order SQL Server's `uniqueidentifier` sorts by |
59
+ | `==` `eql?` `hash` `<=>` | value equality and byte-order comparison (`Comparable`) |
60
+
61
+ `#to_sql_order`/`#from_sql_order` convert a version 6 or 7 UUID to and from the byte order SQL
62
+ Server's `uniqueidentifier` needs on the wire to sort by creation order (`#to_sql_order`
63
+ dispatches on the UUID's own version, matching `#timestamp`'s convention) — computed once in
64
+ the native Rust core rather than reimplemented in Ruby, and verified there (and independently
65
+ against the real `System.Data.SqlTypes.SqlGuid` comparator in the C# binding's test suite).
66
+ Same-millisecond v6 UUIDs aren't guaranteed to sort correctly afterward — v6 has no counter,
67
+ so `clock_seq`/`node` (not the timestamp) decide ties, the same pre-existing RFC 9562 v6
68
+ limitation plain order already has. `#from_sql_order` figures out which version to invert by
69
+ checking a byte position that's provably collision-free between the two (see the method's own
70
+ doc comment).
71
+
72
+ ### Errors
73
+
74
+ Two exceptions are this gem's own, and both carry the same message on every backend:
75
+
76
+ - `HyperUuid::TimestampOutOfRangeError` — the time cannot be embedded: past version 7's
77
+ 48-bit millisecond field, past version 6's 60-bit one, or negative (a `Time` before the Unix
78
+ epoch included).
79
+ - `HyperUuid::RandomSourceError` — the operating system's random source failed.
80
+
81
+ Through 0.3.0 these were reachable only as `HyperUuid::Runtime::TimestampOutOfRangeError` and
82
+ `HyperUuid::Runtime::RandomSourceError`; those names remain, as aliases of the same classes.
83
+
84
+ Everything else is a caller bug and raises Ruby's own error for it, checked once in the shared
85
+ doors rather than left to whichever backend is live: a `TypeError` for a time that is not a
86
+ `Time`, an Integer or `nil`, for a count that is not an Integer, for a namespace that is not a
87
+ `Uuid` or a name that is not a `String`; an `ArgumentError` for a count outside
88
+ 0..2³² − 1.
89
+
90
+ ### Is the native core there?
91
+
92
+ `HyperUuid.available?` answers whether a backend actually loaded — probed once, cached, never
93
+ raising — for a consumer with a fallback of its own:
94
+
95
+ ```ruby
96
+ id = HyperUuid.available? ? HyperUuid.new_v7.to_s : SecureRandom.uuid
97
+ ```
98
+
99
+ `HyperUuid.native_version` reads the version out of the loaded core itself (its
100
+ `hyperuuid_version` export), so a mismatch against `HyperUuid::VERSION` can be named before
101
+ the first UUID is minted. `HyperUuid::BACKEND` says which backend was *selected*, which is not
102
+ the same question: it reads `:fiddle` on a platform with no library at all, because that is
103
+ the backend whose first call explains what is missing — a `LoadError` naming the path it
104
+ looked for, or `HyperUuid::NativePlatform::UnsupportedPlatformError` naming the platform.
105
+
106
+ ## Why not `SecureRandom.uuid`?
107
+
108
+ `SecureRandom.uuid` only ever gives you a random v4 UUID — Ruby's stdlib has no built-in v5, v6, or v7 at all. If you need more than that, the choice is really "which gem":
109
+
110
+ 1. **Full RFC 9562 coverage, one gem, zero extra dependency.** v4/v5/v6/v7 plus batch generation plus `Nil`/`Max`, and the only thing this gem adds to your `Gemfile.lock` beyond `Fiddle` — which is Ruby's own bundled FFI layer, not a third-party C extension to compile.
111
+ 2. **No native-extension compile step.** Third-party UUID gems that go beyond v4 are typically pure Ruby or wrap a C extension compiled at install time; this gem ships its fast path as a prebuilt platform-gem extension and its fallback as a `dlopen`ed prebuilt library — either way, the gem itself compiles nothing at install time ([Install](#install) has the one caveat, which is Fiddle's own).
112
+ 3. **Batch generation.** `new_v7_batch(1000)` shares one timestamp capture, one random-bytes fetch, and one counter reservation across the whole batch instead of paying per-item overhead a thousand times over.
113
+ 4. **Cross-language consistency.** The same Rust core mints v5 namespace UUIDs for Python, Go, C#, and every other binding in this repo — verified in CI to match Python's own `uuid.uuid5` byte-for-byte. If your system isn't Ruby-only, no Ruby-only gem can offer that.
114
+
115
+ The honest trade-off: this gem is native code, not pure Ruby — a precompiled extension in each platform gem, and on the universal gem a platform-specific `libhyperuuid.so`/`.dylib`/`.dll` loaded through Fiddle — so it runs only where one of those was built. If plain v4 randomness is all you need, `SecureRandom.uuid` is simpler and already in stdlib — that's a completely reasonable choice.
116
+
117
+ ## Bulk generation into bytes
118
+
119
+ `new_v6_batch_bytes` and `new_v7_batch_bytes` return the batch as one binary `String` of raw RFC 9562-ordered bytes — 16 per UUID — instead of an Array of `Uuid` objects:
120
+
121
+ ```ruby
122
+ bytes = HyperUuid.new_v7_batch_bytes(1000)
123
+ first = bytes[0, 16] # ready for a BINARY(16) bind parameter
124
+ ```
125
+
126
+ **About 20x faster than `new_v7_batch`** for a 1000-UUID batch (10.4 µs versus 210 µs). The native call is identical — the difference is that `new_v7_batch` then allocates a `Uuid` object and its byte Strings for every item on top of it. This hands back the bytes the native core already produced, untouched.
127
+
128
+ The catch, and it inverts the advice: **if you need `Uuid` objects, keep using `new_v7_batch`.** Slicing these bytes into objects yourself just relocates the identical allocations into your own code, and measures no better — sometimes worse. Reach for the byte form only when bytes are the destination: a bind parameter, a wire format, a bulk load.
129
+
130
+ Slice it with `bytes[i * 16, 16]` — which is exactly what `new_v7_batch` does internally.
131
+
132
+ ## Benchmarks
133
+
134
+ `ruby benchmark/uuid_benchmark.rb` reproduces every table here and the bytes-versus-objects
135
+ comparison above. Its first line names the backend, core version and Ruby it measured, so
136
+ run it again under `HYPERUUID_PURE=1` for the other backend.
137
+
138
+ Real numbers, `benchmark-ips` on Ruby 4.0.7, linux-x64 on an Intel Core i9-11900H (`ruby benchmark/uuid_benchmark.rb`) — not claimed, measured. With the Magnus backend (the default wherever the extension loads):
139
+
140
+ | Call | i/s | vs `SecureRandom.uuid` |
141
+ |---|---:|---:|
142
+ | `SecureRandom.uuid` | 872,365 | baseline |
143
+ | `HyperUuid.new_v4` | 4,278,000 | **4.9x faster** |
144
+ | `HyperUuid.new_v7` (explicit ms) | 3,517,000 | **4.0x faster** |
145
+ | `HyperUuid.new_v6` (explicit ms) | 3,487,000 | **4.0x faster** |
146
+ | `HyperUuid.new_v6` (current time) | 3,324,000 | **3.8x faster** |
147
+ | `HyperUuid.new_v7` (current time) | 3,320,000 | **3.8x faster** |
148
+ | `HyperUuid.new_v5` | 1,838,000 | 2.1x faster |
149
+
150
+ A `HyperUuid.new_v4` — real entropy, correct version/variant bits, minted by the shared Rust core — costs a fifth of what `SecureRandom.uuid` does, because the Magnus extension is an ordinary native method call with nothing marshalled around it.
151
+
152
+ The "current time" rows deserve a footnote, because they will not look like this everywhere. Here they land beside the explicit-ms rows: the only difference between the two is one `Process.clock_gettime(CLOCK_REALTIME)` wall-clock read, and on this machine that read is too cheap to see. On a machine with a slow clock it is the whole story — where a virtualized clock defeats the vDSO fast path the same read costs ~1µs, which puts both current-time rows at parity with `SecureRandom.uuid` while the explicit-ms rows stay well ahead of it. `SecureRandom.uuid` never reads a clock — random v4 is the only thing it does. If your clock is slow and you are minting many, read it once and pass the timestamp, or use the batch doors.
153
+
154
+ The Fiddle fallback (`HYPERUUID_PURE=1`, and any platform without a prebuilt extension) keeps its own diet — a reused thread-local scratch buffer instead of two GC-finalizer-registering mallocs per call, zero-copy `String` passes for read-only inputs, an unsynchronized fast path past the load mutex — landing at 1.25x slower than `SecureRandom.uuid` for v4 (1.26 µs against 1.01 µs in its own run) and 1.45x slower for v6/v7, with the same structural story as before: `Fiddle`'s interpreted marshalling is the floor, and the batch doors are how you amortize it.
155
+
156
+ Batch generation still amortizes per-call cost on both backends — one native call for the whole batch:
157
+
158
+ | Call | i/s (Magnus backend) |
159
+ |---|---:|
160
+ | `new_v6` × 1000 (individual) | 3,498 |
161
+ | `new_v6_batch(1000)` | 4,633 (**1.3x**) |
162
+ | `new_v7` × 1000 (individual) | 3,539 |
163
+ | `new_v7_batch(1000)` | 4,677 (**1.3x**) |
164
+
165
+ The multiplier is small on this backend for the best reason available: the individual calls are cheap, so there is little waste left to amortize, and what `new_v7_batch` spends its 214 µs on is building a thousand `Uuid` objects — the byte form above does the same native work in 10 µs. On the Fiddle backend, where each call costs 1.4 µs, the same batch is 6.7x the loop. If you need v5/v6/v7, need many at once, or need this Ruby service's IDs to agree byte-for-byte with a Go or Python service's, that's what this gem is for — and now it's the fast option too, not just the capable one.
166
+
167
+ ## Backends
168
+
169
+ | `HyperUuid::BACKEND` | What runs | Chosen when |
170
+ |---|---|---|
171
+ | `:native` | the core linked into a Magnus extension | a precompiled platform gem is installed — every one carries an extension for each Ruby it installs on; see [Install](#install) |
172
+ | `:fiddle` | `libhyperuuid` for this platform, `dlopen`ed through Fiddle | no extension loads: the universal gem, on a Ruby or platform no platform gem covers; also the answer when nothing loads at all |
173
+
174
+ Selection happens once, at `require`, in that order.
175
+
176
+ `HYPERUUID_PURE` forces `:fiddle`, even where an extension would load. It is a testing and
177
+ diagnostic switch — CI runs the whole suite through it, and it is how to rule an extension
178
+ problem in or out — not a setting a deployment needs. It is read for presence, not value: set
179
+ to anything at all, `0` and the empty string included, it forces Fiddle. A forced backend that
180
+ turns out to have nothing to load does not fall through to another one: the first call raises,
181
+ and `HyperUuid.available?` answers `false`. That is what happens inside a platform gem, which
182
+ carries no Fiddle library: the `LoadError` names the universal gem
183
+ (`gem install hyperuuid --platform ruby`, or Bundler's `force_ruby_platform`), where the
184
+ Fiddle backend lives.
185
+
186
+ **Threads.** Every backend is safe to call from any number of threads. The extension runs
187
+ under the GVL. Fiddle releases the GVL for the duration of each call, so that is the one
188
+ backend where Ruby threads run the core truly in parallel — each through its own scratch
189
+ buffer, with the core's v7 counter shared and atomic. `spec/hyperuuid_spec.rb`'s "concurrent
190
+ callers" examples run under both.
191
+
192
+ **Ractors.** Main Ractor only, on every backend: called from another Ractor the doors raise
193
+ `Ractor::UnsafeError`.
194
+
195
+ ## Verifying provenance
196
+
197
+ Every gem RubyGems.org serves — the universal fallback and each of the seven precompiled
198
+ platform gems — carries its own GitHub build-provenance attestation, signed directly by
199
+ this repo's own `release.yml` (the `rubygems-publish` job attests `ruby/pkg/*.gem` right
200
+ before the push), so plain `--repo` verifies any of them:
201
+
202
+ ```sh
203
+ gem fetch hyperuuid -v X.Y.Z --platform <platform> # or omit --platform for the universal gem
204
+ gh attestation verify hyperuuid-X.Y.Z-<platform>.gem --repo SkunkWerkx/HyperUuid
205
+ ```
206
+
207
+ That's the release's second layer of checking, not the only one: before any gem gets built,
208
+ the same job verifies every native binary it packs — the Fiddle libraries, the Magnus
209
+ extensions (one per Ruby ABI per platform) — against *their own*
210
+ attestations — those are signed from `SkunkWerkx/.github` by `hyper-build-native.yml`, so
211
+ that check needs `--signer-repo SkunkWerkx/.github` added — and refuses to proceed on an
212
+ unverified one. RubyGems.org has no unpublish and no
213
+ duplicate-version overwrite, so this all happens while a bad artifact is still reversible.
214
+ The release run's job summary then re-fetches every gem from the CDN and records
215
+ attested-vs-served digests, turning "rubygems.org stores an upload verbatim" into a
216
+ per-release measurement rather than an assumption — see
217
+ [csharp/README.md's provenance section](../csharp/README.md#native-binary-provenance) for
218
+ more on why `--signer-repo` is needed for some artifacts here and not others.
219
+
220
+ ## Install
221
+
222
+ ```sh
223
+ gem install hyperuuid
224
+ ```
225
+
226
+ Eight gems are published per release: seven precompiled Magnus platform gems that
227
+ `gem install` and `bundle` auto-select when they match — `x86_64-linux-gnu`,
228
+ `aarch64-linux-gnu`, `x86_64-linux-musl`, `aarch64-linux-musl`, `arm64-darwin`,
229
+ `x64-mingw-ucrt` and `aarch64-mingw-ucrt` — and one universal `ruby`-platform gem. A platform gem carries its
230
+ Magnus extensions and nothing else native: no Fiddle library at all. The universal gem is
231
+ the last resort, Fiddle with every platform's native library bundled, and it is what
232
+ RubyGems resolves for a Ruby the platform gems do not cover (3.3, or a Ruby newer than the
233
+ release, such as 4.1 before a release ships for it) and on a platform no platform gem is
234
+ built for — Intel macOS among them, which runs on Fiddle. No extra configuration needed either
235
+ way.
236
+
237
+ Selection has **two** axes here, unlike every other binding in this repo. A Magnus extension
238
+ is bound to one Ruby minor ABI — there's no `abi3` equivalent to collapse the version axis the
239
+ way [the Python binding's](../python/) wheels do — so each platform gem is a "fat" gem
240
+ carrying one compiled extension per supported Ruby, under `lib/hyperuuid/<minor>/`, and picks
241
+ one at `require` time:
242
+
243
+ | Ruby | Linux (glibc and musl), Apple silicon macOS, Windows — x64 and arm64 | Gem installed |
244
+ | --- | --- | --- |
245
+ | a newer Ruby than the release covers (4.1+) | Fiddle | universal |
246
+ | 4.0 (primary) | Magnus, `BACKEND == :native` | platform |
247
+ | 3.4 (until its EOL 2028-03-31) | Magnus, `BACKEND == :native` | platform |
248
+ | 3.3 (the floor, until its EOL 2027-03-31) | Fiddle | universal |
249
+
250
+ What stands behind each cell: CI runs the whole suite through each Magnus extension on every
251
+ push — Ruby 3.4 and 4.0 on all seven platform-gem platforms, the two musl ones inside each
252
+ Ruby's own Alpine image — and the Fiddle suite on Ruby 4.0 on every one of them, plus on Ruby
253
+ 3.3 inside Alpine. Intel macOS has no CI leg: its library is cross-built and tested at the
254
+ core, and Ruby there runs the universal gem's Fiddle backend over it. Anywhere else — a platform with no native build at
255
+ all — the universal gem installs but `HyperUuid.available?` answers `false`, and the first
256
+ call raises `HyperUuid::NativePlatform::UnsupportedPlatformError`.
257
+
258
+ The platform gems declare `required_ruby_version >= 3.4, < 4.1` precisely so RubyGems
259
+ *declines* them outside that range and resolves the universal gem instead — a wrong-ABI
260
+ extension must never be installed in the first place. On Windows it would at least fail to
261
+ load cleanly (the extension imports `<arch>-ucrt-ruby<minor>.dll` by name —
262
+ `x64-ucrt-ruby400.dll` on x64, `aarch64-ucrt-ruby400.dll` on ARM), but Linux extensions
263
+ don't link libruby at all, so one can load successfully against the wrong ABI and misbehave
264
+ later. When 3.4 goes EOL it simply leaves the matrix and its users fall back to Fiddle, which
265
+ is exactly what the fallback is for.
266
+
267
+ **musl.** Alpine has platform gems of its own, `x86_64-linux-musl` and
268
+ `aarch64-linux-musl`, whose extensions are built inside each Ruby's official `ruby:*-alpine`
269
+ image and need nothing beyond musl's libc. The glibc gems name their libc too
270
+ (`x86_64-linux-gnu`, `aarch64-linux-gnu`), which is what makes both `gem install` and
271
+ Bundler pick the right one on every supported Ruby: next to a plain `x86_64-linux` gem,
272
+ RubyGems before 4.0 resolves that one on Alpine instead, even under
273
+ `--platform x86_64-linux-musl`.
274
+
275
+ **Nothing in this gem is ever compiled, on any platform.** Its one dependency can be:
276
+ `fiddle` is a bundled gem on Ruby 4.0 and a default gem on 3.3 and 3.4, and `gem install` is
277
+ satisfied by the copy Ruby ships. Bundler resolves the newest `fiddle` on rubygems.org
278
+ instead, and when that is newer than the one your Ruby ships — true today on 3.3 and 3.4, not
279
+ on 4.0 — it builds Fiddle's own C extension, which takes a compiler and libffi's headers
280
+ (`apk add build-base libffi-dev` on Alpine). Holding `fiddle` at your Ruby's version in the
281
+ `Gemfile.lock` avoids that.
282
+
283
+ Both Windows architectures get a Magnus gem. MinGW is the *only* Windows flavour `rb-sys`
284
+ targets (`x64-mingw-ucrt` and `aarch64-mingw-ucrt`, both `supported: true` in its own
285
+ `data/toolchains.json`); the one it has no support for is MSVC. Windows is also where the
286
+ Fiddle fallback costs the most: measured on win-x64, Ruby 3.4, the Magnus backend does
287
+ `new_v4` in 406ns against Fiddle's 2407ns (**5.9x**) and `new_v7` in 595ns against 2759ns
288
+ (**4.6x**) — a far wider gap than any Linux or macOS leg shows — and on real Windows-on-ARM
289
+ hardware, Ruby 4.0.6, `new_v4` in 416ns against Fiddle's 2299ns (**5.5x**) and `new_v7` in
290
+ 621ns against 2474ns (**4.0x**), the same shape. Both extensions are built for the `gnullvm`
291
+ Rust targets rather than `gnu` — the same mingw-w64/UCRT ABI RubyInstaller's Ruby uses,
292
+ linked with LLVM and compiler-rt instead of GCC and a statically-linked libgcc, which is what
293
+ keeps the shipped extension small. The build script, and the two flags that are load-bearing
294
+ on the ARM leg (a static libunwind, and a clang-spelled `--target` for bindgen), live in the
295
+ forge's `ruby-magnus` action (`build-magnus.sh`), shared with every other Hyper* repo.
296
+
297
+ ## Development
298
+
299
+ Everything below runs from a checkout, with `rust/` beside `ruby/`; none of it is needed to
300
+ use the gem. The Fiddle backend finds the in-repo build on its own when nothing
301
+ is staged under `lib/hyperuuid/native/`. The Magnus backend does not — an extension has to be
302
+ built for the Ruby you are running and put where `require` looks — and that is what
303
+ `rake native:dev` is for.
304
+
305
+ ```sh
306
+ cd rust
307
+ cargo cdylib # libhyperuuid, what the Fiddle backend loads
308
+
309
+ cd ../ruby
310
+ bundle install
311
+ bundle exec rake native:dev # build the Magnus extension for this Ruby and stage it
312
+ bundle exec rspec # BACKEND == :native
313
+ HYPERUUID_PURE=1 bundle exec rspec # BACKEND == :fiddle
314
+ bundle exec rake docs:check # every public object carries a doc comment
315
+ ruby benchmark/uuid_benchmark.rb # prints the backend it measured
316
+ ```
317
+
318
+ `rake native:dev` runs `cargo ruby-ext` (an alias in `rust/.cargo/config.toml` that builds the
319
+ `ruby` feature into `rust/target/ruby/`, never over the plain library in
320
+ `rust/target/release/`) and copies the result to
321
+ `lib/hyperuuid/<minor>/hyperuuid_native.<so|bundle>` — the path `lib/hyperuuid.rb` tries
322
+ first. The copy is also a rename, and it is the step `cargo ruby-ext` alone does not do: cargo
323
+ names its output `libhyperuuid.so`, and Ruby derives the `Init_` function it calls from the
324
+ file name it was asked to `require`. Three things worth knowing:
325
+
326
+ - **Run it again after changing `rust/`.** The staged file is a copy; nothing rebuilds it.
327
+ - **One staging per Ruby.** An extension is bound to one Ruby minor, so under a second Ruby
328
+ (`RBENV_VERSION=3.4.11 bundle exec rake native:dev`, say) the task rebuilds for that ABI
329
+ and stages beside the first, each Ruby loading its own.
330
+ - **Without it, `bundle exec rspec` still passes — on Fiddle.** The examples under "native
331
+ backend" report themselves pending with `BACKEND=fiddle`; that line, or
332
+ `ruby -Ilib -rhyperuuid -e 'p HyperUuid::BACKEND'`, is how to tell which backend a run
333
+ exercised. Deleting `lib/hyperuuid/<minor>/` goes back to Fiddle.
334
+
335
+ The task covers Linux and macOS. The Windows extensions need the `gnullvm` targets and linker
336
+ flags in the forge's `build-magnus.sh`, which is also what CI uses on every platform.
337
+
338
+ The musl build has no host to run on outside a container. With the core built for musl at
339
+ `rust/target/musl/linux-musl-x64/libhyperuuid.so`, this runs the Fiddle suite against it on
340
+ Alpine, with the checkout mounted read-only:
341
+
342
+ ```sh
343
+ docker run --rm -v "$PWD/..":/src:ro ruby:4.0-alpine sh -euc '
344
+ mkdir /work && cp -r /src/ruby /work/ruby
345
+ mkdir -p /work/ruby/lib/hyperuuid/native/linux-musl-x64
346
+ cp /src/rust/target/musl/linux-musl-x64/libhyperuuid.so /work/ruby/lib/hyperuuid/native/linux-musl-x64/
347
+ cd /work/ruby && rm -f Gemfile.lock
348
+ bundle install --quiet --prefer-local
349
+ HYPERUUID_PURE=1 bundle exec rspec'
350
+ ```
351
+
352
+ The floor's test is the same container on `ruby:3.3-alpine`. Bundler would compile a newer
353
+ Fiddle there (see [Install](#install)), so this one goes around Bundler and runs against the
354
+ Fiddle that Ruby 3.3 ships:
355
+
356
+ ```sh
357
+ docker run --rm -v "$PWD/..":/src:ro ruby:3.3-alpine sh -euc '
358
+ mkdir /work && cp -r /src/ruby /work/ruby
359
+ mkdir -p /work/ruby/lib/hyperuuid/native/linux-musl-x64
360
+ cp /src/rust/target/musl/linux-musl-x64/libhyperuuid.so /work/ruby/lib/hyperuuid/native/linux-musl-x64/
361
+ cd /work/ruby && rm -f Gemfile Gemfile.lock
362
+ gem install rspec -v "~> 3.13" --no-document --silent
363
+ HYPERUUID_PURE=1 rspec'
364
+ ```
365
+
366
+ The musl Magnus extension is built the way CI builds it by the forge's
367
+ `ruby-magnus-musl/build-magnus-musl.sh`, which runs on any machine with Docker: from the repo
368
+ root, `build-magnus-musl.sh hyperuuid linux-musl-x64 4.0 . <dir with the musl libhyperuuid.so> <out-dir>`
369
+ compiles it in `ruby:4.0-alpine`, then runs this suite through it on a bare copy of that image.
370
+
371
+ See [the repo root README](../README.md) for the full RFC 9562 coverage table and the state of every other language binding.
372
+
373
+ ## License
374
+
375
+ [MIT](https://github.com/SkunkWerkx/HyperUuid/blob/master/LICENSE)
@@ -0,0 +1,12 @@
1
+ module HyperUuid
2
+ # Raised when the operating system's random source fails while a UUID is being minted —
3
+ # the one way `new_v4`, `new_v6`, `new_v7` and their batch forms can fail once their
4
+ # arguments are valid. Carries the same message on every backend.
5
+ class RandomSourceError < StandardError; end
6
+
7
+ # Raised when a timestamp cannot be embedded in the UUID being minted: a version 7 value
8
+ # past RFC 9562's 48-bit millisecond field, a version 6 value past its 60-bit field, or
9
+ # any negative one (a `Time` before the Unix epoch included). Carries the same message on
10
+ # every backend.
11
+ class TimestampOutOfRangeError < StandardError; end
12
+ end
@@ -0,0 +1,13 @@
1
+ module HyperUuid
2
+ # Well-known namespace UUIDs defined in RFC 9562 Section 6.6.
3
+ module Namespaces
4
+ # The DNS namespace UUID.
5
+ DNS = Uuid.parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
6
+ # The URL namespace UUID.
7
+ URL = Uuid.parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")
8
+ # The ISO OID namespace UUID.
9
+ OID = Uuid.parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")
10
+ # The X.500 DN namespace UUID.
11
+ X500 = Uuid.parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ module HyperUuid
2
+ # Maps the running RUBY_PLATFORM to the RID-style directory (matching the other bindings'
3
+ # runtimes/{rid}/native/ / native/{rid}/ convention) and native library filename to load.
4
+ module NativePlatform
5
+ class UnsupportedPlatformError < StandardError; end
6
+
7
+ # +platform+ is a parameter only so the specs can walk the whole table from one host;
8
+ # every real caller takes the default.
9
+ def self.rid_and_library_name(platform = RUBY_PLATFORM)
10
+ arch = platform.match?(/arm64|aarch64/) ? "arm64" : "x64"
11
+
12
+ case platform
13
+ when /mingw|mswin|windows/
14
+ ["win-#{arch}", "hyperuuid.dll"]
15
+ when /darwin/
16
+ ["osx-#{arch}", "libhyperuuid.dylib"]
17
+ when /linux/
18
+ # Two C libraries, two builds: a glibc-linked library cannot be relied on to dlopen
19
+ # into a musl process (Alpine), so musl gets RIDs of its own. Ruby names the libc
20
+ # in its platform string there ("x86_64-linux-musl") and leaves it off on glibc.
21
+ os = platform.include?("musl") ? "linux-musl" : "linux"
22
+ ["#{os}-#{arch}", "libhyperuuid.so"]
23
+ else
24
+ raise UnsupportedPlatformError, "hyperuuid: unsupported platform RUBY_PLATFORM=#{platform}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,266 @@
1
+ require "fiddle"
2
+
3
+ module HyperUuid
4
+ # Fiddle plumbing for the native libhyperuuid shared library — dlopen/dlsym plus a raw
5
+ # C-ABI call, no runtime bridge (the same "no shim" positioning as the Swift binding's
6
+ # dlopen approach). Fiddle ships with every Ruby install; it's a plain gem dependency here
7
+ # (see hyperuuid.gemspec) rather than a third-party one — mirroring Python's
8
+ # zero-dependency PyO3 wheels.
9
+ #
10
+ # A Ruby gem's files are plain files on disk once installed, so native/{rid}/{lib} can be
11
+ # dlopen'd directly, with no extraction step.
12
+ module Runtime
13
+ # The package's two exceptions live on HyperUuid itself (errors.rb). These are the names
14
+ # they had through 0.3.0, kept as aliases of the very same classes so a
15
+ # `rescue HyperUuid::Runtime::TimestampOutOfRangeError` written against an earlier
16
+ # release still catches.
17
+ RandomSourceError = HyperUuid::RandomSourceError
18
+ TimestampOutOfRangeError = HyperUuid::TimestampOutOfRangeError
19
+
20
+ # One copy of each out-of-range message for the Fiddle backend and for the
21
+ # doors' own range check in hyperuuid.rb; the Magnus extension (rust/src/ruby_ext.rs)
22
+ # carries the same text, and spec/native_backend_spec.rb pins that the two agree.
23
+ V6_TIMESTAMP_OUT_OF_RANGE = "unix_millis does not fit the 60-bit v6 timestamp field"
24
+ V7_TIMESTAMP_OUT_OF_RANGE = "unix_millis must fit within the RFC 9562 48-bit field"
25
+
26
+ NATIVE_DIR = File.join(__dir__, "native")
27
+
28
+ @mutex = Mutex.new
29
+ @functions = nil
30
+
31
+ class << self
32
+ def new_v4
33
+ out = scratch
34
+ rc = functions[:new_v4].call(out)
35
+ raise random_source_failure("uuid_new_v4") unless rc.zero?
36
+ out[0, 16]
37
+ end
38
+
39
+ def new_v5(namespace_bytes, name_bytes)
40
+ out = scratch
41
+ # Fiddle passes a String's own bytes for void* (read-only, no copy of them) — the
42
+ # same crossing every other input here uses. An empty name crosses as it is, a
43
+ # valid pointer with length 0, never as nil: NULL is not part of this export's
44
+ # contract.
45
+ rc = functions[:new_v5].call(namespace_bytes, name_bytes, name_bytes.bytesize, out)
46
+ raise random_source_failure("uuid_new_v5") unless rc.zero?
47
+ out[0, 16]
48
+ end
49
+
50
+ def new_v6(unix_millis)
51
+ out = scratch
52
+ rc = functions[:new_v6].call(unix_millis, out)
53
+ case rc
54
+ when 0 then out[0, 16]
55
+ when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
56
+ else raise random_source_failure("uuid_new_v6")
57
+ end
58
+ end
59
+
60
+ def v6_unix_millis(bytes)
61
+ functions[:v6_unix_millis].call(bytes)
62
+ end
63
+
64
+ def new_v6_batch(count, unix_millis)
65
+ return "" if count.zero?
66
+ out = Fiddle::Pointer.malloc(count * 16, Fiddle::RUBY_FREE)
67
+ rc = functions[:new_v6_batch].call(unix_millis, count, out)
68
+ case rc
69
+ when 0 then out[0, count * 16]
70
+ when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
71
+ else raise random_source_failure("uuid_new_v6_batch")
72
+ end
73
+ end
74
+
75
+ def new_v7(unix_millis)
76
+ out = scratch
77
+ rc = functions[:new_v7].call(unix_millis, out)
78
+ case rc
79
+ when 0 then out[0, 16]
80
+ when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
81
+ else raise random_source_failure("uuid_new_v7")
82
+ end
83
+ end
84
+
85
+ def v7_unix_millis(bytes)
86
+ functions[:v7_unix_millis].call(bytes)
87
+ end
88
+
89
+ def new_v7_batch(count, unix_millis)
90
+ return "" if count.zero?
91
+ out = Fiddle::Pointer.malloc(count * 16, Fiddle::RUBY_FREE)
92
+ rc = functions[:new_v7_batch].call(unix_millis, count, out)
93
+ case rc
94
+ when 0 then out[0, count * 16]
95
+ when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
96
+ else raise random_source_failure("uuid_new_v7_batch")
97
+ end
98
+ end
99
+
100
+ def v7_to_sql_order(bytes)
101
+ rewrite(:v7_to_sql_order, bytes)
102
+ end
103
+
104
+ def v7_to_rfc_order(bytes)
105
+ rewrite(:v7_to_rfc_order, bytes)
106
+ end
107
+
108
+ def v6_to_sql_order(bytes)
109
+ rewrite(:v6_to_sql_order, bytes)
110
+ end
111
+
112
+ def v6_to_rfc_order(bytes)
113
+ rewrite(:v6_to_rfc_order, bytes)
114
+ end
115
+
116
+ # The loaded core's version word, major << 16 | minor << 8 | patch, straight from the
117
+ # library's zero-argument hyperuuid_version export. HyperUuid.native_version unpacks
118
+ # it; like every method here, each backend replaces this one in place.
119
+ def packed_version
120
+ functions[:version].call
121
+ end
122
+
123
+ # The one failure every generating export shares, with the one message every backend
124
+ # raises for it: the export's name, and that the system's random source failed.
125
+ def random_source_failure(export)
126
+ RandomSourceError.new("#{export}: the system random source failed")
127
+ end
128
+
129
+ private
130
+
131
+ # The shared library to dlopen: this install's native/{rid}/{lib}, or — the
132
+ # development loop — the in-repo cargo build, exactly what the other bindings' local
133
+ # staging does (HyperCast's runtime has had this since its first release). Nil when
134
+ # neither exists.
135
+ def library_path
136
+ rid, lib_name = NativePlatform.rid_and_library_name
137
+ path = File.join(NATIVE_DIR, rid, lib_name)
138
+ return path if File.exist?(path)
139
+
140
+ repo_build = File.expand_path(File.join(__dir__, "../../../rust/target/release", lib_name))
141
+ File.exist?(repo_build) ? repo_build : nil
142
+ end
143
+
144
+ # Why Fiddle found nothing to load. A precompiled platform gem is the one install where
145
+ # that is by design rather than a gap: it carries only its Magnus extensions, and the
146
+ # Fiddle backend is reached there only by forcing it (HYPERUUID_PURE) or because none
147
+ # of its extensions loaded — a gem RubyGems matched to a Ruby it was not built for,
148
+ # such as the glibc Linux gem that `gem install` on RubyGems 3.x picks on Alpine. So
149
+ # that case names its fix, the universal gem, which carries every platform's library,
150
+ # instead of a missing path that reads like a packaging bug. Both arguments are
151
+ # parameters only so the specs can ask for every wording.
152
+ def missing_library_message(gem_platform = Gem.loaded_specs["hyperuuid"]&.platform,
153
+ forced = ENV.key?("HYPERUUID_PURE"))
154
+ rid, lib_name = NativePlatform.rid_and_library_name
155
+ missing = File.join(NATIVE_DIR, rid, lib_name)
156
+ if gem_platform && gem_platform.to_s != Gem::Platform::RUBY
157
+ reason =
158
+ if forced
159
+ "HYPERUUID_PURE forces the Fiddle backend (unset it to use the extension)"
160
+ else
161
+ "none of its extensions loads on this Ruby (#{RUBY_VERSION}, #{RUBY_PLATFORM})"
162
+ end
163
+ "hyperuuid: this #{gem_platform} platform gem carries only Magnus extensions, no " \
164
+ "Fiddle library, and #{reason}. The universal gem has the Fiddle backend for every " \
165
+ "platform: `gem install hyperuuid --platform ruby`, or Bundler's force_ruby_platform " \
166
+ "(#{missing} not found)"
167
+ else
168
+ "hyperuuid: #{missing} not found (unsupported platform, or this gem was built " \
169
+ "without a native library for it)"
170
+ end
171
+ end
172
+
173
+ # One 16-byte scratch allocation per thread, reused by every single-item call —
174
+ # Fiddle::Pointer.malloc(..., RUBY_FREE) registers a GC finalizer per call, measured
175
+ # (in HyperCast, same mechanism) as the dominant per-call cost by an order of
176
+ # magnitude. Batches keep a per-call buffer: one malloc amortized over `count` IDs.
177
+ def scratch
178
+ Thread.current[:hyperuuid_scratch] ||= Fiddle::Pointer.malloc(16, Fiddle::RUBY_FREE)
179
+ end
180
+
181
+ # The in-place byte-order rewrites are the one shape that must copy in: the native
182
+ # call genuinely mutates the buffer, and the input String is frozen.
183
+ def rewrite(symbol, bytes)
184
+ buf = scratch
185
+ buf[0, 16] = bytes
186
+ functions[symbol].call(buf)
187
+ buf[0, 16]
188
+ end
189
+
190
+ # Loaded lazily and exactly once, mirroring Java's class initializer / Swift's lazy
191
+ # static let — the native library and its function pointers live for the process's
192
+ # lifetime, same as every other binding (never dlclose'd). The unsynchronized read is
193
+ # the hot path; the mutex only guards the one-time load (a benign race — idempotent).
194
+ def functions
195
+ @functions || @mutex.synchronize { @functions ||= load_functions }
196
+ end
197
+
198
+ def load_functions
199
+ path = library_path
200
+ raise LoadError, missing_library_message if path.nil?
201
+
202
+ handle = Fiddle.dlopen(path)
203
+ {
204
+ new_v4: Fiddle::Function.new(handle["uuid_new_v4"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT),
205
+ new_v5: Fiddle::Function.new(
206
+ handle["uuid_new_v5"],
207
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
208
+ Fiddle::TYPE_INT
209
+ ),
210
+ new_v6: Fiddle::Function.new(
211
+ handle["uuid_new_v6"],
212
+ [Fiddle::TYPE_UINT64_T, Fiddle::TYPE_VOIDP],
213
+ Fiddle::TYPE_INT
214
+ ),
215
+ v6_unix_millis: Fiddle::Function.new(
216
+ handle["uuid_v6_unix_millis"],
217
+ [Fiddle::TYPE_VOIDP],
218
+ Fiddle::TYPE_UINT64_T
219
+ ),
220
+ new_v6_batch: Fiddle::Function.new(
221
+ handle["uuid_new_v6_batch"],
222
+ [Fiddle::TYPE_UINT64_T, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
223
+ Fiddle::TYPE_INT
224
+ ),
225
+ new_v7: Fiddle::Function.new(
226
+ handle["uuid_new_v7"],
227
+ [Fiddle::TYPE_UINT64_T, Fiddle::TYPE_VOIDP],
228
+ Fiddle::TYPE_INT
229
+ ),
230
+ v7_unix_millis: Fiddle::Function.new(
231
+ handle["uuid_v7_unix_millis"],
232
+ [Fiddle::TYPE_VOIDP],
233
+ Fiddle::TYPE_UINT64_T
234
+ ),
235
+ new_v7_batch: Fiddle::Function.new(
236
+ handle["uuid_new_v7_batch"],
237
+ [Fiddle::TYPE_UINT64_T, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
238
+ Fiddle::TYPE_INT
239
+ ),
240
+ v7_to_sql_order: Fiddle::Function.new(
241
+ handle["uuid_v7_to_sql_order"],
242
+ [Fiddle::TYPE_VOIDP],
243
+ Fiddle::TYPE_VOID
244
+ ),
245
+ v7_to_rfc_order: Fiddle::Function.new(
246
+ handle["uuid_v7_to_rfc_order"],
247
+ [Fiddle::TYPE_VOIDP],
248
+ Fiddle::TYPE_VOID
249
+ ),
250
+ v6_to_sql_order: Fiddle::Function.new(
251
+ handle["uuid_v6_to_sql_order"],
252
+ [Fiddle::TYPE_VOIDP],
253
+ Fiddle::TYPE_VOID
254
+ ),
255
+ v6_to_rfc_order: Fiddle::Function.new(
256
+ handle["uuid_v6_to_rfc_order"],
257
+ [Fiddle::TYPE_VOIDP],
258
+ Fiddle::TYPE_VOID
259
+ ),
260
+ # The one export that mints nothing: the zero-argument version probe.
261
+ version: Fiddle::Function.new(handle["hyperuuid_version"], [], Fiddle::TYPE_UINT32_T),
262
+ }
263
+ end
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,179 @@
1
+ module HyperUuid
2
+ # A parsed 16-byte RFC 9562 UUID value. Minimal by design — this gem has no runtime
3
+ # dependency on the `uuid` gem, the same "no extra dependency" positioning as the Go
4
+ # binding's lone google/uuid requirement and the Python binding's dependency-free PyO3
5
+ # wheels.
6
+ class Uuid
7
+ include Comparable
8
+
9
+ # The UUID's 16 raw bytes in RFC 9562 (big-endian) order.
10
+ attr_reader :bytes
11
+
12
+ # Wraps a raw 16-byte RFC 9562 (big-endian) UUID value. The bytes are copied, so the
13
+ # String passed in stays the caller's.
14
+ #
15
+ # +owned+ is for this gem's own use: it says +bytes+ is a String the native core has
16
+ # just produced — sixteen bytes, already ASCII-8BIT, held by nothing else — so there is
17
+ # nothing to check, copy or re-tag, and that copy is most of what constructing one costs.
18
+ #
19
+ # @raise [ArgumentError] if +bytes+ isn't exactly 16 bytes.
20
+ def initialize(bytes, owned = false)
21
+ if owned
22
+ @bytes = bytes.freeze
23
+ else
24
+ raise ArgumentError, "bytes must be exactly 16 bytes" unless bytes.bytesize == 16
25
+
26
+ @bytes = bytes.dup.force_encoding(Encoding::BINARY).freeze
27
+ end
28
+ end
29
+
30
+ # The RFC 9562 §5.9 Nil UUID — all 128 bits zero.
31
+ NIL = new(("\x00" * 16).b).freeze
32
+
33
+ # The RFC 9562 §5.10 Max UUID — all 128 bits one.
34
+ MAX = new(("\xFF" * 16).b).freeze
35
+
36
+ # The one text shape .parse accepts: 8-4-4-4-12 hex digits, either case, the four hyphens
37
+ # exactly where #to_s puts them.
38
+ HYPHENATED = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
39
+ private_constant :HYPHENATED
40
+
41
+ # Parses an 8-4-4-4-12 hyphenated hex UUID string — exactly the shape #to_s produces, in
42
+ # either case. Nothing else parses: not the bare 32 hex digits, not hyphens anywhere but
43
+ # those four positions, not braces or a `urn:uuid:` prefix.
44
+ #
45
+ # @raise [ArgumentError] if +string+ isn't a String in that shape.
46
+ def self.parse(string)
47
+ unless string.is_a?(String) && string.match?(HYPHENATED)
48
+ raise ArgumentError, "invalid UUID string: #{string.inspect}"
49
+ end
50
+
51
+ new([string.delete("-")].pack("H*"))
52
+ end
53
+
54
+ # The RFC 9562 version nibble (bits 48-51, the high nibble of octet 6).
55
+ def version
56
+ (bytes.getbyte(6) >> 4) & 0x0F
57
+ end
58
+
59
+ # The RFC 9562 variant bits (top two bits of octet 8). +0b10+ means RFC 9562/4122.
60
+ def variant
61
+ (bytes.getbyte(8) >> 6) & 0b11
62
+ end
63
+
64
+ # The 8-4-4-4-12 hyphenated hex string representation.
65
+ def to_s
66
+ hex = bytes.unpack1("H*")
67
+ "#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
68
+ end
69
+ alias_method :to_str, :to_s
70
+
71
+ # The UTC timestamp embedded in a version 6 or 7 UUID's timestamp field. Only meaningful
72
+ # when `version` is 6 or 7 — the RFC 9562 bit layout doesn't distinguish "not a time-based
73
+ # UUID" from "time-based UUID with a very early timestamp", so the caller is responsible
74
+ # for checking `version` first if that matters.
75
+ #
76
+ # Raises by default for any other version; pass `raise_on_mismatch: false` to get `nil`
77
+ # back instead — for a caller that doesn't already know (or want to separately check)
78
+ # whether this UUID is time-based.
79
+ def timestamp(raise_on_mismatch: true)
80
+ millis =
81
+ case version
82
+ when 6 then Runtime.v6_unix_millis(bytes)
83
+ when 7 then Runtime.v7_unix_millis(bytes)
84
+ else
85
+ raise ArgumentError, "timestamp is only defined for version 6 or 7 UUIDs, got version #{version}" if raise_on_mismatch
86
+ return nil
87
+ end
88
+ Time.at(millis / 1000, millis % 1000, :millisecond).utc
89
+ end
90
+
91
+ # Converts an RFC 9562-ordered version 6 or 7 UUID to the byte order SQL Server's
92
+ # `uniqueidentifier` needs on the wire to sort by creation order. Dispatches on `version`
93
+ # the same way #timestamp does.
94
+ #
95
+ # `System.Data.SqlTypes.SqlGuid` comparison — and therefore T-SQL `ORDER BY` on a
96
+ # `uniqueidentifier` column — doesn't compare a GUID's 16 bytes left to right; it uses a
97
+ # fixed, non-sequential byte significance order (octets 10,11,12,13,14,15,8,9,6,7,4,5,
98
+ # 0,1,2,3, most significant first). Computed once in the native Rust core and verified
99
+ # there (and independently, against the real SqlGuid comparator, in this project's C#
100
+ # test suite); this binding calls the same native functions rather than reimplementing
101
+ # the byte math.
102
+ #
103
+ # For v7, this moves the timestamp and counter — the two fields that determine creation
104
+ # order — into that comparison's most-significant bytes, and moves the trailing entropy,
105
+ # which carries no ordering information, into the least-significant ones as one intact
106
+ # block. For v6, which has no monotonic counter the way v7 does, the only field that
107
+ # determines creation order is the 60-bit timestamp itself, so that moves into the most
108
+ # significant bytes instead, with `clock_seq`/`node` (independently random per call, not
109
+ # a counter, so no ordering value either way) relocated into the rest. v6's much simpler
110
+ # byte layout needs no bit-level repacking to do this — just whole-octet-group
111
+ # relocation — unlike v7's, and its version/variant land at different sql-order offsets
112
+ # as a result (octet 8's top nibble / octet 6's top two bits, not 7/8).
113
+ #
114
+ # **v6-specific caveat, unlike v7:** two version 6 UUIDs minted at the same millisecond
115
+ # have identical timestamp bits, so they aren't guaranteed to sort in creation order any
116
+ # more than plain RFC order already does — a pre-existing RFC 9562 v6 limitation, not one
117
+ # this transform introduces.
118
+ #
119
+ # Meaningful only for a genuine version 6 or 7 UUID.
120
+ def to_sql_order
121
+ case version
122
+ when 7 then self.class.new(Runtime.v7_to_sql_order(bytes), true)
123
+ when 6 then self.class.new(Runtime.v6_to_sql_order(bytes), true)
124
+ else raise ArgumentError, "to_sql_order is only defined for version 6 or 7 UUIDs, got version #{version}"
125
+ end
126
+ end
127
+
128
+ # Inverse of #to_sql_order — converts a SQL-Server-ordered version 6 or 7 UUID back to
129
+ # RFC 9562 order.
130
+ #
131
+ # A SQL-ordered value's version nibble sits at a different octet depending on which
132
+ # version produced it (octet 7's top nibble = 7 for v7-sql-order, octet 8's top nibble =
133
+ # 6 for v6-sql-order — #version itself assumes RFC order's octet 6 and can't tell these
134
+ # apart), so this checks both fixed positions directly rather than calling #version.
135
+ #
136
+ # Order matters here and isn't arbitrary: octet 8 must be checked *first*. For v6-sql-order
137
+ # it's deterministic (top nibble always 0x6, by construction), and for v7-sql-order it's
138
+ # also deterministic but structurally excluded from ever reading 0x6 (its top two bits are
139
+ # the fixed variant `10`, so the nibble only ever lands in 0x8-0xB) — no collision either
140
+ # way. Octet 7, by contrast, is *not* safe to check first: for v7-sql-order it's
141
+ # deterministically 0x7, but for v6-sql-order it holds `clock_seq`'s fully random low
142
+ # byte, which has a real (~1-in-16) chance of a top nibble that also happens to read 0x7 —
143
+ # confirmed by an actual test failure during development, not a hypothetical. Checking
144
+ # octet 8 first rules v6 in or out unambiguously before octet 7's reading can matter.
145
+ def from_sql_order
146
+ octet8_version = (bytes.getbyte(8) >> 4) & 0x0F
147
+ octet7_version = (bytes.getbyte(7) >> 4) & 0x0F
148
+ if octet8_version == 6
149
+ self.class.new(Runtime.v6_to_rfc_order(bytes), true)
150
+ elsif octet7_version == 7
151
+ self.class.new(Runtime.v7_to_rfc_order(bytes), true)
152
+ else
153
+ raise ArgumentError, "from_sql_order: not a recognized version 6 or 7 SQL-ordered UUID"
154
+ end
155
+ end
156
+
157
+ # Whether +other+ wraps the same 16 raw bytes.
158
+ def ==(other)
159
+ other.is_a?(Uuid) && bytes == other.bytes
160
+ end
161
+ alias_method :eql?, :==
162
+
163
+ # Hash code consistent with #==, based on the raw bytes.
164
+ def hash
165
+ bytes.hash
166
+ end
167
+
168
+ # Byte-order comparison against +other+, or +nil+ if +other+ isn't a Uuid.
169
+ def <=>(other)
170
+ return nil unless other.is_a?(Uuid)
171
+ bytes <=> other.bytes
172
+ end
173
+
174
+ # Debug representation, e.g. <tt>#<HyperUuid::Uuid ...></tt>.
175
+ def inspect
176
+ "#<HyperUuid::Uuid #{self}>"
177
+ end
178
+ end
179
+ end
data/lib/hyperuuid.rb ADDED
@@ -0,0 +1,247 @@
1
+ require_relative "hyperuuid/errors"
2
+ require_relative "hyperuuid/uuid"
3
+ require_relative "hyperuuid/namespaces"
4
+ require_relative "hyperuuid/native_platform"
5
+ require_relative "hyperuuid/runtime"
6
+
7
+ # RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation from one
8
+ # Rust core, reached through whichever of two backends this install can load: the core
9
+ # linked straight into a Magnus native extension (shipped precompiled in the platform gems),
10
+ # or the native libhyperuuid shared library called through Fiddle (the universal gem bundles
11
+ # one per supported platform, picked by HyperUuid::NativePlatform). No runtime bridge in
12
+ # either, and nothing compiled at install time. HyperUuid::BACKEND names the one that loaded; the selection logic
13
+ # is at the bottom of this file.
14
+ #
15
+ # The doors below — with Uuid, Namespaces and the two exception classes — are the public
16
+ # surface, and are shared by every backend, argument validation included: a caller bug raises
17
+ # the same error whichever backend is live.
18
+ module HyperUuid
19
+ # This gem's own version — distinct from the RFC 9562 UUID *versions* (v4/v5/v6/v7) the
20
+ # rest of this module generates.
21
+ VERSION = "0.6.0"
22
+
23
+ # The widest batch count, and the longest v5 name in bytes, the native ABI carries (a u32).
24
+ # (The millisecond count is a u64; unix_millis_from refuses anything wider.)
25
+ U32_MAX = 0xFFFF_FFFF
26
+ private_constant :U32_MAX
27
+
28
+ # Creates a random UUID version 4 (RFC 9562 §5.4).
29
+ def self.new_v4
30
+ Uuid.new(Runtime.new_v4, true)
31
+ end
32
+
33
+ # Creates a deterministic UUID version 5 (RFC 9562 §5.5) from a namespace and a name. The
34
+ # same (namespace, name) pair always produces the same UUID. `name` may be a text String
35
+ # (encoded as UTF-8) or already-raw ASCII-8BIT bytes, which are used as-is; it may be empty.
36
+ #
37
+ # @raise [TypeError] if +namespace+ isn't a HyperUuid::Uuid or +name+ isn't a String.
38
+ def self.new_v5(namespace, name)
39
+ raise TypeError, "namespace must be a HyperUuid::Uuid; got #{namespace.class}" unless namespace.is_a?(Uuid)
40
+ raise TypeError, "name must be a String; got #{name.class}" unless name.is_a?(String)
41
+
42
+ name_bytes =
43
+ if name.encoding == Encoding::ASCII_8BIT
44
+ name
45
+ else
46
+ name.encode(Encoding::UTF_8).dup.force_encoding(Encoding::BINARY)
47
+ end
48
+ raise ArgumentError, "name must be at most #{U32_MAX} bytes" if name_bytes.bytesize > U32_MAX
49
+
50
+ Uuid.new(Runtime.new_v5(namespace.bytes, name_bytes), true)
51
+ end
52
+
53
+ # Converts +value+ to a Unix-epoch millisecond integer: +nil+ becomes the current time, a
54
+ # +Time+ is converted exactly (via its own Rational seconds, avoiding float rounding), and
55
+ # an Integer millisecond count passes through unchanged. Shared by every
56
+ # `new_v6`/`new_v7`/batch door below so a caller can pass either a `Time` or a raw
57
+ # millisecond count interchangeably.
58
+ #
59
+ # This is also where a caller bug is caught once, for every backend: anything else is a
60
+ # TypeError, and a count that cannot cross the ABI as a u64 at all — negative (a `Time`
61
+ # before the epoch included) or past 64 bits — is the same TimestampOutOfRangeError, with
62
+ # the same +out_of_range+ message, the core raises for a value it can see but not embed.
63
+ # Left to the backends those cases diverged: a RangeError from the extension, a wrapped
64
+ # value from Fiddle.
65
+ #
66
+ # The two common arguments go first and cheapest: no argument is the clock, which needs no
67
+ # range check, and an Integer is checked by its bit length rather than against 2**64 - 1: a
68
+ # comparison with a number that size is a bignum comparison, and cost more than the mint.
69
+ private_class_method def self.unix_millis_from(value, out_of_range)
70
+ return Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) if value.nil?
71
+
72
+ millis =
73
+ if value.is_a?(Integer) then value
74
+ elsif value.is_a?(Time) then (value.to_r * 1000).floor
75
+ else raise TypeError, "unix_millis must be a Time, an Integer or nil; got #{value.class}"
76
+ end
77
+ raise TimestampOutOfRangeError, out_of_range unless millis >= 0 && millis.bit_length <= 64
78
+
79
+ millis
80
+ end
81
+
82
+ # Checks a batch +count+ once, for every backend: an Integer the ABI's u32 can carry.
83
+ private_class_method def self.batch_count(count)
84
+ raise TypeError, "count must be an Integer; got #{count.class}" unless count.is_a?(Integer)
85
+ raise ArgumentError, "count must be between 0 and #{U32_MAX}; got #{count}" unless count.between?(0, U32_MAX)
86
+
87
+ count
88
+ end
89
+
90
+ # Creates a time-sortable UUID version 6 (RFC 9562 §5.6), a field-compatible reordering of
91
+ # version 1 for better sort/index locality. Defaults to the current time; pass an explicit
92
+ # `Time` or Unix-epoch millisecond integer to embed a specific time instead. `clock_seq` and
93
+ # `node` are randomly generated on every call — unlike version 7, there is no monotonic
94
+ # counter, so calls within the same millisecond are not guaranteed to sort in creation order.
95
+ #
96
+ # @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
97
+ # @raise [TypeError] if +unix_millis+ isn't a Time, an Integer or nil.
98
+ def self.new_v6(unix_millis = nil)
99
+ Uuid.new(Runtime.new_v6(unix_millis_from(unix_millis, Runtime::V6_TIMESTAMP_OUT_OF_RANGE)), true)
100
+ end
101
+
102
+ # Creates `count` time-sortable version 6 UUIDs sharing one timestamp capture — one FFI call
103
+ # and one random-bytes fetch instead of `count` of each. Defaults to the current time; pass
104
+ # an explicit `Time` or Unix-epoch millisecond integer to embed a specific time instead.
105
+ #
106
+ # @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
107
+ # @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
108
+ def self.new_v6_batch(count, unix_millis = nil)
109
+ bytes = new_v6_batch_bytes(count, unix_millis)
110
+ Array.new(count) { |i| Uuid.new(bytes[i * 16, 16], true) }
111
+ end
112
+
113
+ # Creates a time-sortable UUID version 7 (RFC 9562 §6.2). Defaults to the current time; pass
114
+ # an explicit `Time` or Unix-epoch millisecond integer (non-negative, fitting in 48 bits) to
115
+ # embed a specific time instead.
116
+ #
117
+ # @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
118
+ # @raise [TypeError] if +unix_millis+ isn't a Time, an Integer or nil.
119
+ def self.new_v7(unix_millis = nil)
120
+ Uuid.new(Runtime.new_v7(unix_millis_from(unix_millis, Runtime::V7_TIMESTAMP_OUT_OF_RANGE)), true)
121
+ end
122
+
123
+ # Creates `count` time-sortable version 7 UUIDs sharing one timestamp capture and one
124
+ # contiguous block of the monotonic counter — one FFI call and one random-bytes fetch
125
+ # instead of `count` of each. Defaults to the current time; pass an explicit `Time` or
126
+ # Unix-epoch millisecond integer to embed a specific time instead.
127
+ #
128
+ # @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
129
+ # @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
130
+ def self.new_v7_batch(count, unix_millis = nil)
131
+ bytes = new_v7_batch_bytes(count, unix_millis)
132
+ Array.new(count) { |i| Uuid.new(bytes[i * 16, 16], true) }
133
+ end
134
+
135
+ # Returns `count` version 7 UUIDs as one binary String of raw RFC 9562-ordered bytes,
136
+ # 16 per UUID, instead of an Array of Uuid objects.
137
+ #
138
+ # Far faster than #new_v7_batch for a large batch — the README's "Bulk generation into
139
+ # bytes" section carries the measured figures. The difference is not the native call —
140
+ # that is identical — it is that #new_v7_batch then allocates `count` Uuid objects and
141
+ # their byte Strings on top of it. This hands back the bytes the native core already
142
+ # produced, untouched.
143
+ #
144
+ # Use it when bytes are the destination: a BYTEA/uniqueidentifier bind parameter, a wire
145
+ # format, a bulk COPY. If you need Uuid objects, keep using #new_v7_batch — slicing this
146
+ # String into them yourself just moves the same allocations into your own code.
147
+ #
148
+ # Slice it with `bytes[i * 16, 16]`, which is what #new_v7_batch does internally.
149
+ #
150
+ # @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
151
+ # @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
152
+ def self.new_v7_batch_bytes(count, unix_millis = nil)
153
+ count = batch_count(count)
154
+ Runtime.new_v7_batch(count, unix_millis_from(unix_millis, Runtime::V7_TIMESTAMP_OUT_OF_RANGE))
155
+ end
156
+
157
+ # Returns `count` version 6 UUIDs as one binary String of raw RFC 9562-ordered bytes,
158
+ # 16 per UUID. The version 6 counterpart to #new_v7_batch_bytes, with the same rationale and
159
+ # the same guidance about when it is the right call.
160
+ #
161
+ # clock_seq and node are independently random per item; unlike version 7 there is no
162
+ # monotonic counter, so items minted in the same millisecond are not guaranteed to sort in
163
+ # creation order.
164
+ #
165
+ # @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
166
+ # @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
167
+ def self.new_v6_batch_bytes(count, unix_millis = nil)
168
+ count = batch_count(count)
169
+ Runtime.new_v6_batch(count, unix_millis_from(unix_millis, Runtime::V6_TIMESTAMP_OUT_OF_RANGE))
170
+ end
171
+
172
+ # The version of the native core actually loaded, as "major.minor.patch" — read from the
173
+ # library itself (its `hyperuuid_version` export), not from this gem, so a consumer can
174
+ # prove the core behind the doors is the one this binding was built against (and name the
175
+ # mismatch against HyperUuid::VERSION when it is not). The cheapest possible probe that the
176
+ # backend resolved at all: takes nothing, mints nothing, cannot fail once the backend loads.
177
+ #
178
+ # @raise [LoadError] if no backend could be loaded for this platform.
179
+ def self.native_version
180
+ word = Runtime.packed_version
181
+ "#{word >> 16}.#{(word >> 8) & 0xFF}.#{word & 0xFF}"
182
+ end
183
+
184
+ # Whether a backend actually loaded and exports the ABI this binding was built against —
185
+ # what a consumer with a fallback of its own (SecureRandom.uuid, say) checks before
186
+ # committing to these doors. Probed once (a native_version round trip), cached, and never
187
+ # raises: a missing shared library, an unsupported platform, or an older core without the
188
+ # version export all answer false. The doors themselves keep their own behavior — the
189
+ # first call on an unavailable backend raises its precise error — this only answers the
190
+ # question quietly.
191
+ def self.available?
192
+ return @available unless @available.nil?
193
+
194
+ @available = begin
195
+ native_version.is_a?(String)
196
+ rescue LoadError, StandardError
197
+ false
198
+ end
199
+ end
200
+ end
201
+
202
+ # --- backend selection: the Magnus extension, when present, replaces the Runtime methods
203
+ # above in place (no delegation layer) — Fiddle's measured per-call marshalling floor drops
204
+ # to an ordinary extension call, while everything above Runtime (Uuid, the module doors,
205
+ # batch slicing) stays shared byte-for-byte between backends. The pure-Fiddle definitions
206
+ # remain the universal zero-compile fallback — the last resort, for a Ruby or a platform no
207
+ # platform gem covers; precompiled platform gems are how the extension ships without ever
208
+ # making a consumer compile anything, and they carry no Fiddle library at all.
209
+ #
210
+ # HYPERUUID_PURE forces Fiddle. It is a testing and diagnostic switch — CI runs the whole suite
211
+ # through it, and it is how a suspected extension bug is ruled in or out — not a setting a
212
+ # deployment needs. It is read for presence, not value, so "0" and the empty string force it
213
+ # too. Inside a platform gem there is no library for it to load: BACKEND still reads :fiddle,
214
+ # HyperUuid.available? answers false, and the first door call raises a LoadError naming the
215
+ # universal gem, which is where the Fiddle backend lives.
216
+ HyperUuid::BACKEND =
217
+ if ENV["HYPERUUID_PURE"]
218
+ :fiddle
219
+ else
220
+ # Two layouts, and both have to work. A released platform gem is a "fat" gem carrying one
221
+ # extension per supported Ruby ABI under lib/hyperuuid/<minor>/ (see the Rakefile's
222
+ # native:gem task for why an ABI-per-file is unavoidable — Magnus has no `abi3`
223
+ # equivalent), and `rake native:dev` — the local dev loop — stages its own build at that
224
+ # same versioned path. CI's in-job staging additionally drops a single extension flat at
225
+ # lib/, which is also where a hand copy of `cargo ruby-ext`'s output goes (the alias only
226
+ # builds into rust/target/ruby/release/; nothing but that rake task copies it here).
227
+ # Trying the versioned path first and the flat one second means neither has to know the
228
+ # other exists.
229
+ #
230
+ # A miss on both is not an error: it means this Ruby/platform combination has no
231
+ # precompiled extension, which is precisely what the Fiddle backend below is for. A
232
+ # platform with no shared library either still lands on Fiddle, so the first call raises
233
+ # its own precise error — a "not found" LoadError naming the path, or
234
+ # NativePlatform::UnsupportedPlatformError naming the platform. HyperUuid.available? is
235
+ # the quiet way to ask.
236
+ begin
237
+ require "hyperuuid/#{RUBY_VERSION[/\d+\.\d+/]}/hyperuuid_native"
238
+ :native
239
+ rescue LoadError
240
+ begin
241
+ require "hyperuuid_native"
242
+ :native
243
+ rescue LoadError
244
+ :fiddle
245
+ end
246
+ end
247
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hyperuuid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.0
5
+ platform: aarch64-linux-musl
6
+ authors:
7
+ - Brian Buvinghausen
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: fiddle
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: yard
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.9'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.9'
54
+ description: |
55
+ RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation, with
56
+ batch and raw-bytes forms, from one native Rust core. Two backends behind one surface,
57
+ selected automatically and both shipped prebuilt: a Magnus extension where a precompiled
58
+ platform gem matches, and stdlib Fiddle everywhere else. No runtime bridge, no
59
+ dependencies beyond Fiddle.
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - LICENSE
65
+ - README.md
66
+ - lib/hyperuuid.rb
67
+ - lib/hyperuuid/3.4/hyperuuid_native.so
68
+ - lib/hyperuuid/4.0/hyperuuid_native.so
69
+ - lib/hyperuuid/errors.rb
70
+ - lib/hyperuuid/namespaces.rb
71
+ - lib/hyperuuid/native_platform.rb
72
+ - lib/hyperuuid/runtime.rb
73
+ - lib/hyperuuid/uuid.rb
74
+ homepage: https://github.com/SkunkWerkx/HyperUuid
75
+ licenses:
76
+ - MIT
77
+ metadata:
78
+ source_code_uri: https://github.com/SkunkWerkx/HyperUuid
79
+ changelog_uri: https://github.com/SkunkWerkx/HyperUuid/blob/master/CHANGELOG.md
80
+ bug_tracker_uri: https://github.com/SkunkWerkx/HyperUuid/issues
81
+ documentation_uri: https://github.com/SkunkWerkx/HyperUuid/tree/master/ruby#readme
82
+ rubygems_mfa_required: 'true'
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '3.4'
91
+ - - "<"
92
+ - !ruby/object:Gem::Version
93
+ version: '4.1'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubygems_version: 4.0.20
101
+ specification_version: 4
102
+ summary: RFC 9562 UUID v4/v5/v6/v7 generation over a native Rust core, shipped prebuilt
103
+ test_files: []