hyperuuid 0.2.0-x64-mingw-ucrt

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: 8601b6074d2d6bbde0afeba53e5719d525522d43aab82291b9944c786f46d588
4
+ data.tar.gz: a8a66899d0566baf69e3a7fa7c889b532eebecb9a7d065f122495d53e2e1860b
5
+ SHA512:
6
+ metadata.gz: 7d0d4f031d24a3d9e972d034c58de8a4334353b46c64b811854cf49af3d85cb3d0e1af1e476f3d914dc5844050cc40a4c6b616092cda7563b566d53ab8137620
7
+ data.tar.gz: 3a092005c188968cae23093c1e03346e3f5297678363e0c9cccfebfad8a56aae31a0739c0eaf288016c45715502d5211c4c7348cf76f329fcfbe72a8aad36041
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,185 @@
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
+
6
+ **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.**
7
+
8
+ RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation, with
9
+ two backends sharing one public surface. The fast path is a native extension built with
10
+ [Magnus](https://github.com/matsadler/magnus) — the Rust core linked directly into the Ruby
11
+ VM, auto-selected when loadable — which redefines the low-level `Runtime` methods in place
12
+ on require; everything above them (`Uuid`, the module doors, batch slicing) is shared
13
+ byte-for-byte between backends. The universal fallback calls the native `libhyperuuid`
14
+ shared library via [`Fiddle`](https://docs.ruby-lang.org/en/master/Fiddle.html) —
15
+ dlopen/dlsym plus a raw C-ABI call, no runtime bridge, nothing to compile on
16
+ `bundle install`. Set `HYPERUUID_PURE=1` to force the Fiddle backend;
17
+ `HyperUuid::BACKEND` reports which one is live. Bundles a native build for every supported
18
+ platform (linux/darwin/windows × x64/arm64) and picks the right one at runtime.
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
+ Returns `HyperUuid::Uuid`, a minimal value object (`#bytes`, `#to_s`, `#version`, `#variant`,
37
+ comparable/hashable) — this gem has no runtime dependency on the `uuid` gem.
38
+ `HyperUuid::Namespaces::DNS`/`URL`/`OID`/`X500` are RFC 9562 Section 6.6's well-known
39
+ namespaces. `#timestamp` recovers the embedded UTC `Time` from a version 6 or 7 UUID; pass
40
+ `raise_on_mismatch: false` to get `nil` back for any other version instead of raising.
41
+ `.new_v6`/`.new_v7` also accept a `Time` directly in place of a raw millisecond count.
42
+ `#to_sql_order`/`#from_sql_order` convert a version 6 or 7 UUID to and from the byte order SQL
43
+ Server's `uniqueidentifier` needs on the wire to sort by creation order (`#to_sql_order`
44
+ dispatches on the UUID's own version, matching `#timestamp`'s convention) — computed once in
45
+ the native Rust core rather than reimplemented in Ruby, and verified there (and independently
46
+ against the real `System.Data.SqlTypes.SqlGuid` comparator in the C# binding's test suite).
47
+ Same-millisecond v6 UUIDs aren't guaranteed to sort correctly afterward — v6 has no counter,
48
+ so `clock_seq`/`node` (not the timestamp) decide ties, the same pre-existing RFC 9562 v6
49
+ limitation plain order already has. `#from_sql_order` figures out which version to invert by checking a byte position that's
50
+ provably collision-free between the two (see the method's own doc comment).
51
+ `HyperUuid::Uuid::NIL`/`MAX` are the RFC 9562 §5.9/§5.10 special-value UUIDs.
52
+ `HyperUuid.new_v6_batch(count)`/`new_v7_batch(count)` generate `count` UUIDs sharing one
53
+ timestamp capture and one native call, instead of `count` of each.
54
+
55
+ ## Why not `SecureRandom.uuid`?
56
+
57
+ `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":
58
+
59
+ 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.
60
+ 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, nothing to compile on `bundle install`.
61
+ 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.
62
+ 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.
63
+
64
+ The honest trade-off: this gem `dlopen`s a native library instead of being pure Ruby, so it needs a platform-specific `libhyperuuid.so`/`.dylib`/`.dll` bundled alongside it. If plain v4 randomness is all you need, `SecureRandom.uuid` is simpler and already in stdlib — that's a completely reasonable choice.
65
+
66
+ ## Bulk generation into bytes
67
+
68
+ `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:
69
+
70
+ ```ruby
71
+ bytes = HyperUuid.new_v7_batch_bytes(1000)
72
+ first = bytes[0, 16] # ready for a BINARY(16) bind parameter
73
+ ```
74
+
75
+ **About 15x faster than `new_v7_batch`** for a 1000-UUID batch (24 µs versus 370 µs). The native call is identical — the difference is that `new_v7_batch` then allocates 1000 `Uuid` objects and 1000 String slices on top of it. This hands back the bytes the native core already produced, untouched.
76
+
77
+ 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.
78
+
79
+ Slice it with `bytes[i * 16, 16]` — which is exactly what `new_v7_batch` does internally.
80
+
81
+ ## Benchmarks
82
+
83
+ Real numbers, `benchmark-ips` on Ruby 4.0.6, linux-arm64 (`ruby benchmark/uuid_benchmark.rb`) — not claimed, measured. With the Magnus backend (the default wherever the extension loads):
84
+
85
+ | Call | i/s | vs `SecureRandom.uuid` |
86
+ |---|---:|---:|
87
+ | `SecureRandom.uuid` | 775,868 | baseline |
88
+ | `HyperUuid.new_v7` (explicit ms) | 2,275,763 | **2.9x faster** |
89
+ | `HyperUuid.new_v6` (explicit ms) | 2,197,698 | **2.8x faster** |
90
+ | `HyperUuid.new_v4` | 2,176,244 | **2.8x faster** |
91
+ | `HyperUuid.new_v5` | 1,458,385 | 1.9x faster |
92
+ | `HyperUuid.new_v7` (current time) | 701,373 | parity (1.1x slower) |
93
+ | `HyperUuid.new_v6` (current time) | 703,748 | parity (1.1x slower) |
94
+
95
+ An earlier edition of this section said single-item calls "lose to `SecureRandom.uuid`, full stop" and called the gap "structural, not a bug to fix — no amount of tuning closes that gap." That was wrong, and the receipts above are the correction: the gap was `Fiddle`'s per-call marshalling, and replacing the mechanism (the same play as this repo's Python PyO3 backend) closed it with room to spare. A `HyperUuid.new_v4` — real entropy, correct version/variant bits, minted by the shared Rust core — now costs a third of what `SecureRandom.uuid` does.
96
+
97
+ The two "current time" rows deserve their honest footnote: the explicit-ms rows isolate the binding's own cost (~440-460ns), and the difference is one `Process.clock_gettime(CLOCK_REALTIME)` wall-clock read — which this WSL2 measurement box prices at ~1µs because its Hyper-V clock defeats the vDSO fast path (verified: `CLOCK_REALTIME_COARSE` costs 102ns on the same box). On bare-metal Linux that read is tens of nanoseconds, and the default-time rows land next to the explicit-ms ones. `SecureRandom.uuid` never reads a clock — random v4 is the only thing it does.
98
+
99
+ 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.27x slower than `SecureRandom.uuid` for v4 (was 1.30x before the diet, from a worse baseline run) with the same structural story as before: `Fiddle`'s interpreted marshalling is the floor, and the batch doors are how you amortize it.
100
+
101
+ Batch generation still amortizes per-call cost on both backends — one native call for the whole batch:
102
+
103
+ | Call | i/s (Magnus backend) |
104
+ |---|---:|
105
+ | `new_v6` × 1000 (individual) | 731.3 |
106
+ | `new_v6_batch(1000)` | 2,655.6 (**3.6x**) |
107
+ | `new_v7` × 1000 (individual) | 710.0 |
108
+ | `new_v7_batch(1000)` | 2,744.3 (**3.9x**) |
109
+
110
+ The batch multiplier shrank from 11x to ~3.8x for the best reason available: the individual calls got 3x faster, so there's less waste left to amortize. 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.
111
+
112
+ ## Install
113
+
114
+ ```sh
115
+ gem install hyperuuid
116
+ ```
117
+
118
+ Published to [RubyGems.org](https://rubygems.org/gems/hyperuuid) as real precompiled
119
+ "platform gems" — `bundle`/`gem install` auto-selects the matching one for
120
+ linux-x64/arm64, osx-x64/arm64, x64-mingw-ucrt or aarch64-mingw-ucrt (the compiled Magnus
121
+ native extension, `backend: :native`), falling back automatically to the universal
122
+ `ruby`-platform gem (pure Fiddle, zero compile, bundles all 6 platforms' native libs)
123
+ everywhere else. No extra configuration needed either way.
124
+
125
+ Selection has **two** axes here, unlike every other binding in this repo. A Magnus extension
126
+ is bound to one Ruby minor ABI — there's no `abi3` equivalent to collapse the version axis the
127
+ way [the Python binding's](../python/) wheels do — so each platform gem is a "fat" gem
128
+ carrying one compiled extension per supported Ruby, under `lib/hyperuuid/<minor>/`, and picks
129
+ one at `require` time:
130
+
131
+ | Ruby | linux-x64/arm64, osx-x64/arm64, x64-mingw-ucrt, aarch64-mingw-ucrt | anywhere else (musl/Alpine, …) |
132
+ | --- | --- | --- |
133
+ | 4.0 (primary) | Magnus, `backend: :native` | Fiddle |
134
+ | 3.4 (floor, until its EOL 2028-03-31) | Magnus, `backend: :native` | Fiddle |
135
+ | 3.2 / 3.3 | Fiddle | Fiddle |
136
+
137
+ The platform gems declare `required_ruby_version >= 3.4, < 4.1` precisely so RubyGems
138
+ *declines* them outside that range and resolves the universal gem instead — a wrong-ABI
139
+ extension must never be installed in the first place. On Windows it would at least fail to
140
+ load cleanly (the extension imports `<arch>-ucrt-ruby<minor>.dll` by name —
141
+ `x64-ucrt-ruby400.dll` on x64, `aarch64-ucrt-ruby400.dll` on ARM), but Linux extensions
142
+ don't link libruby at all, so one can load successfully against the wrong ABI and misbehave
143
+ later. When 3.4 goes EOL it simply leaves the matrix and its users fall back to Fiddle, which
144
+ is exactly what the fallback is for.
145
+
146
+ An earlier edition of this section said the fallback covered "Windows included, since Magnus
147
+ doesn't target it." That was wrong in both halves. MinGW is the *only* Windows flavour
148
+ `rb-sys` targets — its own `data/toolchains.json` maps `x64-mingw-ucrt` to the
149
+ `x86_64-pc-windows-gnu` Rust target, `supported: true`; the target it has no support for is
150
+ `x86_64-pc-windows-msvc`. And Windows is where the Fiddle fallback cost the most: measured
151
+ on win-x64, Ruby 3.4, the Magnus backend does `new_v4` in 406ns against Fiddle's 2407ns
152
+ (**5.9x**) and `new_v7` in 595ns against 2759ns (**4.6x**) — a far wider gap than any Linux
153
+ or macOS leg shows.
154
+
155
+ Windows-on-ARM is no longer the exception it was. `rb-sys` maps `aarch64-mingw-ucrt` to the
156
+ `aarch64-pc-windows-gnullvm` Rust target (`supported: true`), and RubyInstaller ships an
157
+ `aarch64-mingw-ucrt` build of both ABIs in the table above, so win-arm64 now gets the same fat
158
+ platform gem as every other leg. Measured on real Windows-on-ARM hardware, Ruby 4.0.6, the
159
+ Magnus backend does `new_v4` in 416ns against Fiddle's 2299ns (**5.5x**) and `new_v7` in 621ns
160
+ against 2474ns (**4.0x**) — the same shape the x64 leg shows.
161
+
162
+ What differs from the x64 build follows from `gnullvm` being LLVM-based where
163
+ `x86_64-pc-windows-gnu` is GCC-based. The linker driver is `aarch64-w64-mingw32-clang` from
164
+ MSYS2's CLANGARM64 environment — RubyInstaller's own ARM devkit installs it locally, and
165
+ `ruby/setup-ruby` installs it on `windows-11-arm` — so there is no `link-self-contained=no`
166
+ dance. Two flags are load-bearing:
167
+
168
+ - **`-l static=unwind`.** rustc emits its `-lunwind` in the linker's *dynamic* section, where
169
+ CLANGARM64 offers both `libunwind.a` and `libunwind.dll.a` and lld prefers the latter. Left
170
+ alone, the extension acquires a runtime dependency on `libunwind.dll` — a file no consumer
171
+ of the gem would have.
172
+ - **`BINDGEN_EXTRA_CLANG_ARGS=--target=aarch64-w64-mingw32`.** bindgen hands clang the *Rust*
173
+ target triple when cargo cross-compiles, and clang rejects `aarch64-pc-windows-gnullvm`
174
+ outright (`version 'llvm' in target triple ... is invalid`), then fails behind that on a
175
+ missing `stdalign.h` it never reached its own resource dir to find. `aarch64-w64-mingw32`
176
+ is the same ABI spelled the way clang accepts, exactly as x64 spells its gnu target
177
+ `x86_64-w64-mingw32`.
178
+
179
+ That second one was briefly documented here as *unnecessary*, on the strength of a local
180
+ build where it genuinely was: that build used a `gnullvm` **host** toolchain, so host equals
181
+ target, cargo was not cross-compiling, and bindgen injected no `--target` at all. CI's host is
182
+ MSVC, so it does. A local build says nothing about that step unless its host triple matches
183
+ the runner's.
184
+
185
+ See [the repo root README](../README.md) for the full RFC 9562 coverage table and the state of every other language binding.
@@ -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,6 @@
1
+ # native/
2
+
3
+ Populated per-RID with the platform's native `libhyperuuid` build (`native/{rid}/{lib}`) by CI
4
+ and by `cargo build --release` for local dev — see `../../../.gitignore`. This file exists so
5
+ the directory has at least one tracked file on a fresh checkout, matching the Go/Swift
6
+ bindings' `native/`/`NativeLibs/` placeholder convention.
@@ -0,0 +1,22 @@
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
+ def self.rid_and_library_name
8
+ is_arm = RUBY_PLATFORM.match?(/arm64|aarch64/)
9
+
10
+ case RUBY_PLATFORM
11
+ when /mingw|mswin|windows/
12
+ is_arm ? ["win-arm64", "hyperuuid.dll"] : ["win-x64", "hyperuuid.dll"]
13
+ when /darwin/
14
+ is_arm ? ["osx-arm64", "libhyperuuid.dylib"] : ["osx-x64", "libhyperuuid.dylib"]
15
+ when /linux/
16
+ is_arm ? ["linux-arm64", "libhyperuuid.so"] : ["linux-x64", "libhyperuuid.so"]
17
+ else
18
+ raise UnsupportedPlatformError, "hyperuuid: unsupported platform RUBY_PLATFORM=#{RUBY_PLATFORM}"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,204 @@
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 Go/Swift bindings'
6
+ # purego/dlopen approach). Fiddle ships with every Ruby install; it's a plain gem
7
+ # dependency here (see hyperuuid.gemspec) rather than a third-party one — mirroring Go's
8
+ # "no cgo" and Python's zero-dependency PyO3 wheels.
9
+ #
10
+ # Unlike the Go/Swift bindings, which embed their native builds inside a single compiled
11
+ # archive and must extract to a temp file before dlopen can see a real path, a Ruby gem's
12
+ # files are already plain files on disk once installed — native/{rid}/{lib} can be
13
+ # dlopen'd directly, no extraction step needed.
14
+ module Runtime
15
+ class RandomSourceError < StandardError; end
16
+ class TimestampOutOfRangeError < StandardError; end
17
+
18
+ NATIVE_DIR = File.join(__dir__, "native")
19
+
20
+ @mutex = Mutex.new
21
+ @functions = nil
22
+
23
+ class << self
24
+ def new_v4
25
+ out = scratch
26
+ rc = functions[:new_v4].call(out)
27
+ raise RandomSourceError, "uuid_new_v4 failed with code #{rc}" unless rc.zero?
28
+ out[0, 16]
29
+ end
30
+
31
+ def new_v5(namespace_bytes, name_bytes)
32
+ out = scratch
33
+ # Fiddle passes a String's bytes for void* directly (read-only) — no Pointer
34
+ # wrapper, no copy — the same zero-copy crossing every other input here uses.
35
+ name = name_bytes.empty? ? nil : name_bytes
36
+ rc = functions[:new_v5].call(namespace_bytes, name, name_bytes.bytesize, out)
37
+ raise RandomSourceError, "uuid_new_v5 failed with code #{rc}" unless rc.zero?
38
+ out[0, 16]
39
+ end
40
+
41
+ def new_v6(unix_millis)
42
+ out = scratch
43
+ rc = functions[:new_v6].call(unix_millis, out)
44
+ case rc
45
+ when 0 then out[0, 16]
46
+ when 2 then raise TimestampOutOfRangeError, "unix_millis does not fit the 60-bit v6 timestamp field"
47
+ else raise RandomSourceError, "uuid_new_v6 failed with code #{rc}"
48
+ end
49
+ end
50
+
51
+ def v6_unix_millis(bytes)
52
+ functions[:v6_unix_millis].call(bytes)
53
+ end
54
+
55
+ def new_v6_batch(count, unix_millis)
56
+ return "" if count.zero?
57
+ out = Fiddle::Pointer.malloc(count * 16, Fiddle::RUBY_FREE)
58
+ rc = functions[:new_v6_batch].call(unix_millis, count, out)
59
+ case rc
60
+ when 0 then out[0, count * 16]
61
+ when 2 then raise TimestampOutOfRangeError, "unix_millis does not fit the 60-bit v6 timestamp field"
62
+ else raise RandomSourceError, "uuid_new_v6_batch failed with code #{rc}"
63
+ end
64
+ end
65
+
66
+ def new_v7(unix_millis)
67
+ out = scratch
68
+ rc = functions[:new_v7].call(unix_millis, out)
69
+ case rc
70
+ when 0 then out[0, 16]
71
+ when 2 then raise TimestampOutOfRangeError, "unix_millis must fit within the RFC 9562 48-bit field"
72
+ else raise RandomSourceError, "uuid_new_v7 failed with code #{rc}"
73
+ end
74
+ end
75
+
76
+ def v7_unix_millis(bytes)
77
+ functions[:v7_unix_millis].call(bytes)
78
+ end
79
+
80
+ def new_v7_batch(count, unix_millis)
81
+ return "" if count.zero?
82
+ out = Fiddle::Pointer.malloc(count * 16, Fiddle::RUBY_FREE)
83
+ rc = functions[:new_v7_batch].call(unix_millis, count, out)
84
+ case rc
85
+ when 0 then out[0, count * 16]
86
+ when 2 then raise TimestampOutOfRangeError, "unix_millis must fit within the RFC 9562 48-bit field"
87
+ else raise RandomSourceError, "uuid_new_v7_batch failed with code #{rc}"
88
+ end
89
+ end
90
+
91
+ def v7_to_sql_order(bytes)
92
+ rewrite(:v7_to_sql_order, bytes)
93
+ end
94
+
95
+ def v7_to_rfc_order(bytes)
96
+ rewrite(:v7_to_rfc_order, bytes)
97
+ end
98
+
99
+ def v6_to_sql_order(bytes)
100
+ rewrite(:v6_to_sql_order, bytes)
101
+ end
102
+
103
+ def v6_to_rfc_order(bytes)
104
+ rewrite(:v6_to_rfc_order, bytes)
105
+ end
106
+
107
+ private
108
+
109
+ # One 16-byte scratch allocation per thread, reused by every single-item call —
110
+ # Fiddle::Pointer.malloc(..., RUBY_FREE) registers a GC finalizer per call, measured
111
+ # (in HyperCast, same mechanism) as the dominant per-call cost by an order of
112
+ # magnitude. Batches keep a per-call buffer: one malloc amortized over `count` IDs.
113
+ def scratch
114
+ Thread.current[:hyperuuid_scratch] ||= Fiddle::Pointer.malloc(16, Fiddle::RUBY_FREE)
115
+ end
116
+
117
+ # The in-place byte-order rewrites are the one shape that must copy in: the native
118
+ # call genuinely mutates the buffer, and the input String is frozen.
119
+ def rewrite(symbol, bytes)
120
+ buf = scratch
121
+ buf[0, 16] = bytes
122
+ functions[symbol].call(buf)
123
+ buf[0, 16]
124
+ end
125
+
126
+ # Loaded lazily and exactly once, mirroring the Go binding's sync.Once / Swift's lazy
127
+ # static let — the native library and its function pointers live for the process's
128
+ # lifetime, same as every other binding (never dlclose'd). The unsynchronized read is
129
+ # the hot path; the mutex only guards the one-time load (a benign race — idempotent).
130
+ def functions
131
+ @functions || @mutex.synchronize { @functions ||= load_functions }
132
+ end
133
+
134
+ def load_functions
135
+ rid, lib_name = NativePlatform.rid_and_library_name
136
+ path = File.join(NATIVE_DIR, rid, lib_name)
137
+ unless File.exist?(path)
138
+ raise LoadError,
139
+ "hyperuuid: #{path} not found (unsupported platform, or this gem was built without a native library for it)"
140
+ end
141
+
142
+ handle = Fiddle.dlopen(path)
143
+ {
144
+ new_v4: Fiddle::Function.new(handle["uuid_new_v4"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT),
145
+ new_v5: Fiddle::Function.new(
146
+ handle["uuid_new_v5"],
147
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
148
+ Fiddle::TYPE_INT
149
+ ),
150
+ new_v6: Fiddle::Function.new(
151
+ handle["uuid_new_v6"],
152
+ [Fiddle::TYPE_UINT64_T, Fiddle::TYPE_VOIDP],
153
+ Fiddle::TYPE_INT
154
+ ),
155
+ v6_unix_millis: Fiddle::Function.new(
156
+ handle["uuid_v6_unix_millis"],
157
+ [Fiddle::TYPE_VOIDP],
158
+ Fiddle::TYPE_UINT64_T
159
+ ),
160
+ new_v6_batch: Fiddle::Function.new(
161
+ handle["uuid_new_v6_batch"],
162
+ [Fiddle::TYPE_UINT64_T, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
163
+ Fiddle::TYPE_INT
164
+ ),
165
+ new_v7: Fiddle::Function.new(
166
+ handle["uuid_new_v7"],
167
+ [Fiddle::TYPE_UINT64_T, Fiddle::TYPE_VOIDP],
168
+ Fiddle::TYPE_INT
169
+ ),
170
+ v7_unix_millis: Fiddle::Function.new(
171
+ handle["uuid_v7_unix_millis"],
172
+ [Fiddle::TYPE_VOIDP],
173
+ Fiddle::TYPE_UINT64_T
174
+ ),
175
+ new_v7_batch: Fiddle::Function.new(
176
+ handle["uuid_new_v7_batch"],
177
+ [Fiddle::TYPE_UINT64_T, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
178
+ Fiddle::TYPE_INT
179
+ ),
180
+ v7_to_sql_order: Fiddle::Function.new(
181
+ handle["uuid_v7_to_sql_order"],
182
+ [Fiddle::TYPE_VOIDP],
183
+ Fiddle::TYPE_VOID
184
+ ),
185
+ v7_to_rfc_order: Fiddle::Function.new(
186
+ handle["uuid_v7_to_rfc_order"],
187
+ [Fiddle::TYPE_VOIDP],
188
+ Fiddle::TYPE_VOID
189
+ ),
190
+ v6_to_sql_order: Fiddle::Function.new(
191
+ handle["uuid_v6_to_sql_order"],
192
+ [Fiddle::TYPE_VOIDP],
193
+ Fiddle::TYPE_VOID
194
+ ),
195
+ v6_to_rfc_order: Fiddle::Function.new(
196
+ handle["uuid_v6_to_rfc_order"],
197
+ [Fiddle::TYPE_VOIDP],
198
+ Fiddle::TYPE_VOID
199
+ ),
200
+ }
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,160 @@
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 purego-only/no-cgo approach 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.
13
+ #
14
+ # @raise [ArgumentError] if +bytes+ isn't exactly 16 bytes.
15
+ def initialize(bytes)
16
+ raise ArgumentError, "bytes must be exactly 16 bytes" unless bytes.bytesize == 16
17
+ @bytes = bytes.dup.force_encoding(Encoding::BINARY).freeze
18
+ end
19
+
20
+ # The RFC 9562 §5.9 Nil UUID — all 128 bits zero.
21
+ NIL = new(("\x00" * 16).b).freeze
22
+
23
+ # The RFC 9562 §5.10 Max UUID — all 128 bits one.
24
+ MAX = new(("\xFF" * 16).b).freeze
25
+
26
+ # Parses an 8-4-4-4-12 hyphenated hex UUID string.
27
+ #
28
+ # @raise [ArgumentError] if +string+ isn't a valid UUID string.
29
+ def self.parse(string)
30
+ hex = string.delete("-")
31
+ raise ArgumentError, "invalid UUID string: #{string.inspect}" unless hex.match?(/\A[0-9a-fA-F]{32}\z/)
32
+ new([hex].pack("H*"))
33
+ end
34
+
35
+ # The RFC 9562 version nibble (bits 48-51, the high nibble of octet 6).
36
+ def version
37
+ (bytes.getbyte(6) >> 4) & 0x0F
38
+ end
39
+
40
+ # The RFC 9562 variant bits (top two bits of octet 8). +0b10+ means RFC 9562/4122.
41
+ def variant
42
+ (bytes.getbyte(8) >> 6) & 0b11
43
+ end
44
+
45
+ # The 8-4-4-4-12 hyphenated hex string representation.
46
+ def to_s
47
+ hex = bytes.unpack1("H*")
48
+ "#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
49
+ end
50
+ alias_method :to_str, :to_s
51
+
52
+ # The UTC timestamp embedded in a version 6 or 7 UUID's timestamp field. Only meaningful
53
+ # when `version` is 6 or 7 — the RFC 9562 bit layout doesn't distinguish "not a time-based
54
+ # UUID" from "time-based UUID with a very early timestamp", so the caller is responsible
55
+ # for checking `version` first if that matters.
56
+ #
57
+ # Raises by default for any other version; pass `raise_on_mismatch: false` to get `nil`
58
+ # back instead — for a caller that doesn't already know (or want to separately check)
59
+ # whether this UUID is time-based.
60
+ def timestamp(raise_on_mismatch: true)
61
+ millis =
62
+ case version
63
+ when 6 then Runtime.v6_unix_millis(bytes)
64
+ when 7 then Runtime.v7_unix_millis(bytes)
65
+ else
66
+ raise ArgumentError, "timestamp is only defined for version 6 or 7 UUIDs, got version #{version}" if raise_on_mismatch
67
+ return nil
68
+ end
69
+ Time.at(millis / 1000, millis % 1000, :millisecond).utc
70
+ end
71
+
72
+ # Converts an RFC 9562-ordered version 6 or 7 UUID to the byte order SQL Server's
73
+ # `uniqueidentifier` needs on the wire to sort by creation order. Dispatches on `version`
74
+ # the same way #timestamp does.
75
+ #
76
+ # `System.Data.SqlTypes.SqlGuid` comparison — and therefore T-SQL `ORDER BY` on a
77
+ # `uniqueidentifier` column — doesn't compare a GUID's 16 bytes left to right; it uses a
78
+ # fixed, non-sequential byte significance order (octets 10,11,12,13,14,15,8,9,6,7,4,5,
79
+ # 0,1,2,3, most significant first). Computed once in the native Rust core and verified
80
+ # there (and independently, against the real SqlGuid comparator, in this project's C#
81
+ # test suite); this binding calls the same native functions rather than reimplementing
82
+ # the byte math.
83
+ #
84
+ # For v7, this moves the timestamp and counter — the two fields that determine creation
85
+ # order — into that comparison's most-significant bytes, and moves the trailing entropy,
86
+ # which carries no ordering information, into the least-significant ones as one intact
87
+ # block. For v6, which has no monotonic counter the way v7 does, the only field that
88
+ # determines creation order is the 60-bit timestamp itself, so that moves into the most
89
+ # significant bytes instead, with `clock_seq`/`node` (independently random per call, not
90
+ # a counter, so no ordering value either way) relocated into the rest. v6's much simpler
91
+ # byte layout needs no bit-level repacking to do this — just whole-octet-group
92
+ # relocation — unlike v7's, and its version/variant land at different sql-order offsets
93
+ # as a result (octet 8's top nibble / octet 6's top two bits, not 7/8).
94
+ #
95
+ # **v6-specific caveat, unlike v7:** two version 6 UUIDs minted at the same millisecond
96
+ # have identical timestamp bits, so they aren't guaranteed to sort in creation order any
97
+ # more than plain RFC order already does — a pre-existing RFC 9562 v6 limitation, not one
98
+ # this transform introduces.
99
+ #
100
+ # Meaningful only for a genuine version 6 or 7 UUID.
101
+ def to_sql_order
102
+ case version
103
+ when 7 then self.class.new(Runtime.v7_to_sql_order(bytes))
104
+ when 6 then self.class.new(Runtime.v6_to_sql_order(bytes))
105
+ else raise ArgumentError, "to_sql_order is only defined for version 6 or 7 UUIDs, got version #{version}"
106
+ end
107
+ end
108
+
109
+ # Inverse of #to_sql_order — converts a SQL-Server-ordered version 6 or 7 UUID back to
110
+ # RFC 9562 order.
111
+ #
112
+ # A SQL-ordered value's version nibble sits at a different octet depending on which
113
+ # version produced it (octet 7's top nibble = 7 for v7-sql-order, octet 8's top nibble =
114
+ # 6 for v6-sql-order — #version itself assumes RFC order's octet 6 and can't tell these
115
+ # apart), so this checks both fixed positions directly rather than calling #version.
116
+ #
117
+ # Order matters here and isn't arbitrary: octet 8 must be checked *first*. For v6-sql-order
118
+ # it's deterministic (top nibble always 0x6, by construction), and for v7-sql-order it's
119
+ # also deterministic but structurally excluded from ever reading 0x6 (its top two bits are
120
+ # the fixed variant `10`, so the nibble only ever lands in 0x8-0xB) — no collision either
121
+ # way. Octet 7, by contrast, is *not* safe to check first: for v7-sql-order it's
122
+ # deterministically 0x7, but for v6-sql-order it holds `clock_seq`'s fully random low
123
+ # byte, which has a real (~1-in-16) chance of a top nibble that also happens to read 0x7 —
124
+ # confirmed by an actual test failure during development, not a hypothetical. Checking
125
+ # octet 8 first rules v6 in or out unambiguously before octet 7's reading can matter.
126
+ def from_sql_order
127
+ octet8_version = (bytes.getbyte(8) >> 4) & 0x0F
128
+ octet7_version = (bytes.getbyte(7) >> 4) & 0x0F
129
+ if octet8_version == 6
130
+ self.class.new(Runtime.v6_to_rfc_order(bytes))
131
+ elsif octet7_version == 7
132
+ self.class.new(Runtime.v7_to_rfc_order(bytes))
133
+ else
134
+ raise ArgumentError, "from_sql_order: not a recognized version 6 or 7 SQL-ordered UUID"
135
+ end
136
+ end
137
+
138
+ # Whether +other+ wraps the same 16 raw bytes.
139
+ def ==(other)
140
+ other.is_a?(Uuid) && bytes == other.bytes
141
+ end
142
+ alias_method :eql?, :==
143
+
144
+ # Hash code consistent with #==, based on the raw bytes.
145
+ def hash
146
+ bytes.hash
147
+ end
148
+
149
+ # Byte-order comparison against +other+, or +nil+ if +other+ isn't a Uuid.
150
+ def <=>(other)
151
+ return nil unless other.is_a?(Uuid)
152
+ bytes <=> other.bytes
153
+ end
154
+
155
+ # Debug representation, e.g. <tt>#<HyperUuid::Uuid ...></tt>.
156
+ def inspect
157
+ "#<HyperUuid::Uuid #{self}>"
158
+ end
159
+ end
160
+ end
data/lib/hyperuuid.rb ADDED
@@ -0,0 +1,142 @@
1
+ require "time"
2
+
3
+ require_relative "hyperuuid/uuid"
4
+ require_relative "hyperuuid/namespaces"
5
+ require_relative "hyperuuid/native_platform"
6
+ require_relative "hyperuuid/runtime"
7
+
8
+ # RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation, calling
9
+ # directly into the native libhyperuuid shared library via Fiddle — no runtime bridge, no
10
+ # extra gem dependency. Bundles a native build for every supported platform (see
11
+ # HyperUuid::NativePlatform) and picks the right one at runtime, the same trick the Go/
12
+ # Java bindings use since RubyGems has no per-platform native selection wired up here.
13
+ module HyperUuid
14
+ # This gem's own version — distinct from the RFC 9562 UUID *versions* (v4/v5/v6/v7) the
15
+ # rest of this module generates.
16
+ VERSION = "0.2.0"
17
+
18
+ # Creates a random UUID version 4 (RFC 9562 §5.4).
19
+ def self.new_v4
20
+ Uuid.new(Runtime.new_v4)
21
+ end
22
+
23
+ # Creates a deterministic UUID version 5 (RFC 9562 §5.5) from a namespace and a name. The
24
+ # same (namespace, name) pair always produces the same UUID. `name` may be a text String
25
+ # (encoded as UTF-8) or already-raw ASCII-8BIT bytes, which are used as-is.
26
+ def self.new_v5(namespace, name)
27
+ name_bytes =
28
+ if name.encoding == Encoding::ASCII_8BIT
29
+ name
30
+ else
31
+ name.encode(Encoding::UTF_8).dup.force_encoding(Encoding::BINARY)
32
+ end
33
+ Uuid.new(Runtime.new_v5(namespace.bytes, name_bytes))
34
+ end
35
+
36
+ # Converts +value+ to a Unix-epoch millisecond integer: +nil+ becomes the current time, a
37
+ # +Time+ is converted exactly (via its own Rational seconds, avoiding float rounding), and
38
+ # anything else (an Integer millisecond count) passes through unchanged. Shared by every
39
+ # `new_v6`/`new_v7`/batch door below so a caller can pass either a `Time` or a raw
40
+ # millisecond count interchangeably.
41
+ private_class_method def self.unix_millis_from(value)
42
+ case value
43
+ when nil then Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
44
+ when Time then (value.to_r * 1000).to_i
45
+ else value
46
+ end
47
+ end
48
+
49
+ # Creates a time-sortable UUID version 6 (RFC 9562 §5.6), a field-compatible reordering of
50
+ # version 1 for better sort/index locality. Defaults to the current time; pass an explicit
51
+ # `Time` or Unix-epoch millisecond integer to embed a specific time instead. `clock_seq` and
52
+ # `node` are randomly generated on every call — unlike version 7, there is no monotonic
53
+ # counter, so calls within the same millisecond are not guaranteed to sort in creation order.
54
+ def self.new_v6(unix_millis = nil)
55
+ Uuid.new(Runtime.new_v6(unix_millis_from(unix_millis)))
56
+ end
57
+
58
+ # Creates `count` time-sortable version 6 UUIDs sharing one timestamp capture — one FFI call
59
+ # and one random-bytes fetch instead of `count` of each. Defaults to the current time; pass
60
+ # an explicit `Time` or Unix-epoch millisecond integer to embed a specific time instead.
61
+ def self.new_v6_batch(count, unix_millis = nil)
62
+ bytes = Runtime.new_v6_batch(count, unix_millis_from(unix_millis))
63
+ Array.new(count) { |i| Uuid.new(bytes[i * 16, 16]) }
64
+ end
65
+
66
+ # Creates a time-sortable UUID version 7 (RFC 9562 §6.2). Defaults to the current time; pass
67
+ # an explicit `Time` or Unix-epoch millisecond integer (non-negative, fitting in 48 bits) to
68
+ # embed a specific time instead.
69
+ def self.new_v7(unix_millis = nil)
70
+ Uuid.new(Runtime.new_v7(unix_millis_from(unix_millis)))
71
+ end
72
+
73
+ # Creates `count` time-sortable version 7 UUIDs sharing one timestamp capture and one
74
+ # contiguous block of the monotonic counter — one FFI call and one random-bytes fetch
75
+ # instead of `count` of each. Defaults to the current time; pass an explicit `Time` or
76
+ # Unix-epoch millisecond integer to embed a specific time instead.
77
+ def self.new_v7_batch(count, unix_millis = nil)
78
+ bytes = Runtime.new_v7_batch(count, unix_millis_from(unix_millis))
79
+ Array.new(count) { |i| Uuid.new(bytes[i * 16, 16]) }
80
+ end
81
+
82
+ # Returns `count` version 7 UUIDs as one binary String of raw RFC 9562-ordered bytes,
83
+ # 16 per UUID, instead of an Array of Uuid objects.
84
+ #
85
+ # Roughly 11x faster than #new_v7_batch for a 1000-UUID batch (about 35 us versus 400 us).
86
+ # The difference is not the native call — that is identical — it is that #new_v7_batch then
87
+ # allocates `count` Uuid objects and `count` String slices on top of it. This hands back the
88
+ # bytes the native core already produced, untouched.
89
+ #
90
+ # Use it when bytes are the destination: a BYTEA/uniqueidentifier bind parameter, a wire
91
+ # format, a bulk COPY. If you need Uuid objects, keep using #new_v7_batch — slicing this
92
+ # String into them yourself just moves the same allocations into your own code.
93
+ #
94
+ # Slice it with `bytes[i * 16, 16]`, which is what #new_v7_batch does internally.
95
+ def self.new_v7_batch_bytes(count, unix_millis = nil)
96
+ Runtime.new_v7_batch(count, unix_millis_from(unix_millis))
97
+ end
98
+
99
+ # Returns `count` version 6 UUIDs as one binary String of raw RFC 9562-ordered bytes,
100
+ # 16 per UUID. The version 6 counterpart to #new_v7_batch_bytes, with the same rationale and
101
+ # the same guidance about when it is the right call.
102
+ #
103
+ # clock_seq and node are independently random per item; unlike version 7 there is no
104
+ # monotonic counter, so items minted in the same millisecond are not guaranteed to sort in
105
+ # creation order.
106
+ def self.new_v6_batch_bytes(count, unix_millis = nil)
107
+ Runtime.new_v6_batch(count, unix_millis_from(unix_millis))
108
+ end
109
+ end
110
+
111
+ # --- backend selection: the Magnus extension, when present, replaces the Runtime methods
112
+ # above in place (no delegation layer) — Fiddle's measured per-call marshalling floor drops
113
+ # to an ordinary extension call, while everything above Runtime (Uuid, the module doors,
114
+ # batch slicing) stays shared byte-for-byte between backends. The pure-Fiddle definitions
115
+ # remain the universal zero-compile fallback; precompiled platform gems are how the
116
+ # extension ships without ever making a consumer compile anything. Set HYPERUUID_PURE=1 to
117
+ # force Fiddle.
118
+ HyperUuid::BACKEND =
119
+ if ENV["HYPERUUID_PURE"]
120
+ :fiddle
121
+ else
122
+ # Two layouts, and both have to work. A released platform gem is a "fat" gem carrying one
123
+ # extension per supported Ruby ABI under lib/hyperuuid/<minor>/ (see the Rakefile's
124
+ # native:gem task for why an ABI-per-file is unavoidable — Magnus has no `abi3`
125
+ # equivalent). CI's in-job staging and a local `cargo build --release --features ruby`
126
+ # instead drop a single extension flat at lib/. Trying the versioned path first and the
127
+ # flat one second means neither has to know the other exists.
128
+ #
129
+ # A miss on both is not an error: it means this Ruby/platform combination has no
130
+ # precompiled extension, which is precisely what the Fiddle backend below is for.
131
+ begin
132
+ require "hyperuuid/#{RUBY_VERSION[/\d+\.\d+/]}/hyperuuid_native"
133
+ :native
134
+ rescue LoadError
135
+ begin
136
+ require "hyperuuid_native"
137
+ :native
138
+ rescue LoadError
139
+ :fiddle
140
+ end
141
+ end
142
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hyperuuid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: x64-mingw-ucrt
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: benchmark-ips
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.15'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.15'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: yard
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.9'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.9'
68
+ description: |
69
+ High-performance, allocation-free RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7
70
+ (time-sortable) generation, calling directly into the native libhyperuuid shared library
71
+ via Fiddle (Ruby's standard-library FFI) — no runtime bridge, no extra gem dependency.
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - LICENSE
77
+ - README.md
78
+ - lib/hyperuuid.rb
79
+ - lib/hyperuuid/3.4/hyperuuid_native.so
80
+ - lib/hyperuuid/4.0/hyperuuid_native.so
81
+ - lib/hyperuuid/namespaces.rb
82
+ - lib/hyperuuid/native/README.md
83
+ - lib/hyperuuid/native/linux-arm64/libhyperuuid.so
84
+ - lib/hyperuuid/native/linux-x64/libhyperuuid.so
85
+ - lib/hyperuuid/native/osx-arm64/libhyperuuid.dylib
86
+ - lib/hyperuuid/native/osx-x64/libhyperuuid.dylib
87
+ - lib/hyperuuid/native/win-arm64/hyperuuid.dll
88
+ - lib/hyperuuid/native/win-x64/hyperuuid.dll
89
+ - lib/hyperuuid/native_platform.rb
90
+ - lib/hyperuuid/runtime.rb
91
+ - lib/hyperuuid/uuid.rb
92
+ homepage: https://github.com/SkunkWerkx/HyperUuid
93
+ licenses:
94
+ - MIT
95
+ metadata:
96
+ source_code_uri: https://github.com/SkunkWerkx/HyperUuid
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '3.4'
105
+ - - "<"
106
+ - !ruby/object:Gem::Version
107
+ version: '4.1'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubygems_version: 4.0.16
115
+ specification_version: 4
116
+ summary: RFC 9562 UUID v4/v5/v6/v7 generation — direct native FFI into a Rust core,
117
+ no runtime bridge.
118
+ test_files: []