hyperuuid 0.0.1

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: d2b7cb6d510ac9faccd193dde27916dc8bd7384b6b9c1d387dfc47682eeb9274
4
+ data.tar.gz: f4d3082562a32328b0519b6f2c0b461d8fb3303d731fca8b1b96422266f97e4c
5
+ SHA512:
6
+ metadata.gz: 1d80438fc78ff61048568c3ff70fff7bc6493ba639ac60299c21495b863419fc49f5e61f38bdfaf1b9be9910930da9124deaf98254bc119d9ec0d8f11b6cf1fc
7
+ data.tar.gz: beed906a491217aef07a6d3ce2ac36f77410710c7379ae1cffa0733cdb3c33c2eb1deeecd24fac7d32f96160068b512b6d5984fbd5aa7508e6fa23590e950169
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # hyperuuid
2
+
3
+ **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.**
4
+
5
+ RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation, with
6
+ two backends sharing one public surface. The fast path is a native extension built with
7
+ [Magnus](https://github.com/matsadler/magnus) — the Rust core linked directly into the Ruby
8
+ VM, auto-selected when loadable — which redefines the low-level `Runtime` methods in place
9
+ on require; everything above them (`Uuid`, the module doors, batch slicing) is shared
10
+ byte-for-byte between backends. The universal fallback calls the native `libhyperuuid`
11
+ shared library via [`Fiddle`](https://docs.ruby-lang.org/en/master/Fiddle.html) —
12
+ dlopen/dlsym plus a raw C-ABI call, no runtime bridge, nothing to compile on
13
+ `bundle install`. Set `HYPERUUID_PURE=1` to force the Fiddle backend;
14
+ `HyperUuid::BACKEND` reports which one is live. Bundles a native build for every supported
15
+ platform (linux/darwin/windows × x64/arm64) and picks the right one at runtime.
16
+
17
+ ```ruby
18
+ require "hyperuuid"
19
+
20
+ id = HyperUuid.new_v4
21
+ id2 = HyperUuid.new_v5(HyperUuid::Namespaces::DNS, "example.com")
22
+ id3 = HyperUuid.new_v6
23
+ id4 = HyperUuid.new_v7
24
+
25
+ id4.timestamp # recover the embedded UTC Time
26
+ id4.to_sql_order # byte order SQL Server's uniqueidentifier needs to sort by creation order
27
+
28
+ # One native call, one random-bytes fetch, one counter reservation for the whole batch:
29
+ batch = HyperUuid.new_v7_batch(1000)
30
+ ```
31
+
32
+ Returns `HyperUuid::Uuid`, a minimal value object (`#bytes`, `#to_s`, `#version`, `#variant`,
33
+ comparable/hashable) — this gem has no runtime dependency on the `uuid` gem.
34
+ `HyperUuid::Namespaces::DNS`/`URL`/`OID`/`X500` are RFC 9562 Section 6.6's well-known
35
+ namespaces. `#timestamp` recovers the embedded UTC `Time` from a version 6 or 7 UUID.
36
+ `#to_sql_order`/`#from_sql_order` convert a version 6 or 7 UUID to and from the byte order SQL
37
+ Server's `uniqueidentifier` needs on the wire to sort by creation order (`#to_sql_order`
38
+ dispatches on the UUID's own version, matching `#timestamp`'s convention) — computed once in
39
+ the native Rust core rather than reimplemented in Ruby, and verified there (and independently
40
+ against the real `System.Data.SqlTypes.SqlGuid` comparator in the C# binding's test suite).
41
+ Same-millisecond v6 UUIDs aren't guaranteed to sort correctly afterward — v6 has no counter,
42
+ so `clock_seq`/`node` (not the timestamp) decide ties, the same pre-existing RFC 9562 v6
43
+ limitation plain order already has. `#from_sql_order` figures out which version to invert by checking a byte position that's
44
+ provably collision-free between the two (see the method's own doc comment).
45
+ `HyperUuid::Uuid::NIL`/`MAX` are the RFC 9562 §5.9/§5.10 special-value UUIDs.
46
+ `HyperUuid.new_v6_batch(count)`/`new_v7_batch(count)` generate `count` UUIDs sharing one
47
+ timestamp capture and one native call, instead of `count` of each.
48
+
49
+ ## Why not `SecureRandom.uuid`?
50
+
51
+ `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":
52
+
53
+ 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.
54
+ 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`.
55
+ 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.
56
+ 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.
57
+
58
+ 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.
59
+
60
+ ## Benchmarks
61
+
62
+ 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):
63
+
64
+ | Call | i/s | vs `SecureRandom.uuid` |
65
+ |---|---:|---:|
66
+ | `SecureRandom.uuid` | 775,868 | baseline |
67
+ | `HyperUuid.new_v7` (explicit ms) | 2,275,763 | **2.9x faster** |
68
+ | `HyperUuid.new_v6` (explicit ms) | 2,197,698 | **2.8x faster** |
69
+ | `HyperUuid.new_v4` | 2,176,244 | **2.8x faster** |
70
+ | `HyperUuid.new_v5` | 1,458,385 | 1.9x faster |
71
+ | `HyperUuid.new_v7` (current time) | 701,373 | parity (1.1x slower) |
72
+ | `HyperUuid.new_v6` (current time) | 703,748 | parity (1.1x slower) |
73
+
74
+ 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.
75
+
76
+ 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.
77
+
78
+ 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.
79
+
80
+ Batch generation still amortizes per-call cost on both backends — one native call for the whole batch:
81
+
82
+ | Call | i/s (Magnus backend) |
83
+ |---|---:|
84
+ | `new_v6` × 1000 (individual) | 731.3 |
85
+ | `new_v6_batch(1000)` | 2,655.6 (**3.6x**) |
86
+ | `new_v7` × 1000 (individual) | 710.0 |
87
+ | `new_v7_batch(1000)` | 2,744.3 (**3.9x**) |
88
+
89
+ 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.
90
+
91
+ ## Install
92
+
93
+ Not yet published to RubyGems.org under a registered `SkunkWerkx`/`buvinghausen` presence —
94
+ for now this is proven by CI building and testing the native core plus this gem on real
95
+ hardware for every platform leg. Consume via a direct
96
+ `gem "hyperuuid", git: "https://github.com/SkunkWerkx/HyperUuid", glob: "ruby/*.gemspec"` in
97
+ the meantime.
98
+
99
+ 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 ctypes-only stance.
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,153 @@
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 stdlib-ctypes-only one.
5
+ class Uuid
6
+ include Comparable
7
+
8
+ # The UUID's 16 raw bytes in RFC 9562 (big-endian) order.
9
+ attr_reader :bytes
10
+
11
+ # Wraps a raw 16-byte RFC 9562 (big-endian) UUID value.
12
+ #
13
+ # @raise [ArgumentError] if +bytes+ isn't exactly 16 bytes.
14
+ def initialize(bytes)
15
+ raise ArgumentError, "bytes must be exactly 16 bytes" unless bytes.bytesize == 16
16
+ @bytes = bytes.dup.force_encoding(Encoding::BINARY).freeze
17
+ end
18
+
19
+ # The RFC 9562 §5.9 Nil UUID — all 128 bits zero.
20
+ NIL = new(("\x00" * 16).b).freeze
21
+
22
+ # The RFC 9562 §5.10 Max UUID — all 128 bits one.
23
+ MAX = new(("\xFF" * 16).b).freeze
24
+
25
+ # Parses an 8-4-4-4-12 hyphenated hex UUID string.
26
+ #
27
+ # @raise [ArgumentError] if +string+ isn't a valid UUID string.
28
+ def self.parse(string)
29
+ hex = string.delete("-")
30
+ raise ArgumentError, "invalid UUID string: #{string.inspect}" unless hex.match?(/\A[0-9a-fA-F]{32}\z/)
31
+ new([hex].pack("H*"))
32
+ end
33
+
34
+ # The RFC 9562 version nibble (bits 48-51, the high nibble of octet 6).
35
+ def version
36
+ (bytes.getbyte(6) >> 4) & 0x0F
37
+ end
38
+
39
+ # The RFC 9562 variant bits (top two bits of octet 8). +0b10+ means RFC 9562/4122.
40
+ def variant
41
+ (bytes.getbyte(8) >> 6) & 0b11
42
+ end
43
+
44
+ # The 8-4-4-4-12 hyphenated hex string representation.
45
+ def to_s
46
+ hex = bytes.unpack1("H*")
47
+ "#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
48
+ end
49
+ alias_method :to_str, :to_s
50
+
51
+ # The UTC timestamp embedded in a version 6 or 7 UUID's timestamp field. Only meaningful
52
+ # when `version` is 6 or 7 — the RFC 9562 bit layout doesn't distinguish "not a time-based
53
+ # UUID" from "time-based UUID with a very early timestamp", so the caller is responsible
54
+ # for checking `version` first if that matters.
55
+ def timestamp
56
+ millis =
57
+ case version
58
+ when 6 then Runtime.v6_unix_millis(bytes)
59
+ when 7 then Runtime.v7_unix_millis(bytes)
60
+ else raise ArgumentError, "timestamp is only defined for version 6 or 7 UUIDs, got version #{version}"
61
+ end
62
+ Time.at(millis / 1000, millis % 1000, :millisecond).utc
63
+ end
64
+
65
+ # Converts an RFC 9562-ordered version 6 or 7 UUID to the byte order SQL Server's
66
+ # `uniqueidentifier` needs on the wire to sort by creation order. Dispatches on `version`
67
+ # the same way #timestamp does.
68
+ #
69
+ # `System.Data.SqlTypes.SqlGuid` comparison — and therefore T-SQL `ORDER BY` on a
70
+ # `uniqueidentifier` column — doesn't compare a GUID's 16 bytes left to right; it uses a
71
+ # fixed, non-sequential byte significance order (octets 10,11,12,13,14,15,8,9,6,7,4,5,
72
+ # 0,1,2,3, most significant first). Computed once in the native Rust core and verified
73
+ # there (and independently, against the real SqlGuid comparator, in this project's C#
74
+ # test suite); this binding calls the same native functions rather than reimplementing
75
+ # the byte math.
76
+ #
77
+ # For v7, this moves the timestamp and counter — the two fields that determine creation
78
+ # order — into that comparison's most-significant bytes, and moves the trailing entropy,
79
+ # which carries no ordering information, into the least-significant ones as one intact
80
+ # block. For v6, which has no monotonic counter the way v7 does, the only field that
81
+ # determines creation order is the 60-bit timestamp itself, so that moves into the most
82
+ # significant bytes instead, with `clock_seq`/`node` (independently random per call, not
83
+ # a counter, so no ordering value either way) relocated into the rest. v6's much simpler
84
+ # byte layout needs no bit-level repacking to do this — just whole-octet-group
85
+ # relocation — unlike v7's, and its version/variant land at different sql-order offsets
86
+ # as a result (octet 8's top nibble / octet 6's top two bits, not 7/8).
87
+ #
88
+ # **v6-specific caveat, unlike v7:** two version 6 UUIDs minted at the same millisecond
89
+ # have identical timestamp bits, so they aren't guaranteed to sort in creation order any
90
+ # more than plain RFC order already does — a pre-existing RFC 9562 v6 limitation, not one
91
+ # this transform introduces.
92
+ #
93
+ # Meaningful only for a genuine version 6 or 7 UUID.
94
+ def to_sql_order
95
+ case version
96
+ when 7 then self.class.new(Runtime.v7_to_sql_order(bytes))
97
+ when 6 then self.class.new(Runtime.v6_to_sql_order(bytes))
98
+ else raise ArgumentError, "to_sql_order is only defined for version 6 or 7 UUIDs, got version #{version}"
99
+ end
100
+ end
101
+
102
+ # Inverse of #to_sql_order — converts a SQL-Server-ordered version 6 or 7 UUID back to
103
+ # RFC 9562 order.
104
+ #
105
+ # A SQL-ordered value's version nibble sits at a different octet depending on which
106
+ # version produced it (octet 7's top nibble = 7 for v7-sql-order, octet 8's top nibble =
107
+ # 6 for v6-sql-order — #version itself assumes RFC order's octet 6 and can't tell these
108
+ # apart), so this checks both fixed positions directly rather than calling #version.
109
+ #
110
+ # Order matters here and isn't arbitrary: octet 8 must be checked *first*. For v6-sql-order
111
+ # it's deterministic (top nibble always 0x6, by construction), and for v7-sql-order it's
112
+ # also deterministic but structurally excluded from ever reading 0x6 (its top two bits are
113
+ # the fixed variant `10`, so the nibble only ever lands in 0x8-0xB) — no collision either
114
+ # way. Octet 7, by contrast, is *not* safe to check first: for v7-sql-order it's
115
+ # deterministically 0x7, but for v6-sql-order it holds `clock_seq`'s fully random low
116
+ # byte, which has a real (~1-in-16) chance of a top nibble that also happens to read 0x7 —
117
+ # confirmed by an actual test failure during development, not a hypothetical. Checking
118
+ # octet 8 first rules v6 in or out unambiguously before octet 7's reading can matter.
119
+ def from_sql_order
120
+ octet8_version = (bytes.getbyte(8) >> 4) & 0x0F
121
+ octet7_version = (bytes.getbyte(7) >> 4) & 0x0F
122
+ if octet8_version == 6
123
+ self.class.new(Runtime.v6_to_rfc_order(bytes))
124
+ elsif octet7_version == 7
125
+ self.class.new(Runtime.v7_to_rfc_order(bytes))
126
+ else
127
+ raise ArgumentError, "from_sql_order: not a recognized version 6 or 7 SQL-ordered UUID"
128
+ end
129
+ end
130
+
131
+ # Whether +other+ wraps the same 16 raw bytes.
132
+ def ==(other)
133
+ other.is_a?(Uuid) && bytes == other.bytes
134
+ end
135
+ alias_method :eql?, :==
136
+
137
+ # Hash code consistent with #==, based on the raw bytes.
138
+ def hash
139
+ bytes.hash
140
+ end
141
+
142
+ # Byte-order comparison against +other+, or +nil+ if +other+ isn't a Uuid.
143
+ def <=>(other)
144
+ return nil unless other.is_a?(Uuid)
145
+ bytes <=> other.bytes
146
+ end
147
+
148
+ # Debug representation, e.g. <tt>#<HyperUuid::Uuid ...></tt>.
149
+ def inspect
150
+ "#<HyperUuid::Uuid #{self}>"
151
+ end
152
+ end
153
+ end
data/lib/hyperuuid.rb ADDED
@@ -0,0 +1,89 @@
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.1.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
+ # Creates a time-sortable UUID version 6 (RFC 9562 §5.6), a field-compatible reordering of
37
+ # version 1 for better sort/index locality. Defaults to the current time; pass an explicit
38
+ # Unix-epoch millisecond timestamp to embed a specific time instead. `clock_seq` and `node`
39
+ # are randomly generated on every call — unlike version 7, there is no monotonic counter, so
40
+ # calls within the same millisecond are not guaranteed to sort in creation order.
41
+ def self.new_v6(unix_millis = nil)
42
+ unix_millis ||= Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
43
+ Uuid.new(Runtime.new_v6(unix_millis))
44
+ end
45
+
46
+ # Creates `count` time-sortable version 6 UUIDs sharing one timestamp capture — one FFI call
47
+ # and one random-bytes fetch instead of `count` of each. Defaults to the current time.
48
+ def self.new_v6_batch(count, unix_millis = nil)
49
+ unix_millis ||= Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
50
+ bytes = Runtime.new_v6_batch(count, unix_millis)
51
+ Array.new(count) { |i| Uuid.new(bytes[i * 16, 16]) }
52
+ end
53
+
54
+ # Creates a time-sortable UUID version 7 (RFC 9562 §6.2). Defaults to the current time; pass
55
+ # an explicit Unix-epoch millisecond timestamp (non-negative, fitting in 48 bits) to embed a
56
+ # specific time instead.
57
+ def self.new_v7(unix_millis = nil)
58
+ unix_millis ||= Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
59
+ Uuid.new(Runtime.new_v7(unix_millis))
60
+ end
61
+
62
+ # Creates `count` time-sortable version 7 UUIDs sharing one timestamp capture and one
63
+ # contiguous block of the monotonic counter — one FFI call and one random-bytes fetch
64
+ # instead of `count` of each. Defaults to the current time.
65
+ def self.new_v7_batch(count, unix_millis = nil)
66
+ unix_millis ||= Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
67
+ bytes = Runtime.new_v7_batch(count, unix_millis)
68
+ Array.new(count) { |i| Uuid.new(bytes[i * 16, 16]) }
69
+ end
70
+ end
71
+
72
+ # --- backend selection: the Magnus extension, when present, replaces the Runtime methods
73
+ # above in place (no delegation layer) — Fiddle's measured per-call marshalling floor drops
74
+ # to an ordinary extension call, while everything above Runtime (Uuid, the module doors,
75
+ # batch slicing) stays shared byte-for-byte between backends. The pure-Fiddle definitions
76
+ # remain the universal zero-compile fallback; precompiled platform gems are how the
77
+ # extension ships without ever making a consumer compile anything. Set HYPERUUID_PURE=1 to
78
+ # force Fiddle.
79
+ HyperUuid::BACKEND =
80
+ if ENV["HYPERUUID_PURE"]
81
+ :fiddle
82
+ else
83
+ begin
84
+ require "hyperuuid_native"
85
+ :native
86
+ rescue LoadError
87
+ :fiddle
88
+ end
89
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hyperuuid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
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
+ - README.md
77
+ - lib/hyperuuid.rb
78
+ - lib/hyperuuid/namespaces.rb
79
+ - lib/hyperuuid/native/README.md
80
+ - lib/hyperuuid/native/linux-arm64/libhyperuuid.so
81
+ - lib/hyperuuid/native/linux-x64/libhyperuuid.so
82
+ - lib/hyperuuid/native/osx-arm64/libhyperuuid.dylib
83
+ - lib/hyperuuid/native/osx-x64/libhyperuuid.dylib
84
+ - lib/hyperuuid/native/win-arm64/hyperuuid.dll
85
+ - lib/hyperuuid/native/win-x64/hyperuuid.dll
86
+ - lib/hyperuuid/native_platform.rb
87
+ - lib/hyperuuid/runtime.rb
88
+ - lib/hyperuuid/uuid.rb
89
+ homepage: https://github.com/SkunkWerkx/HyperUuid
90
+ licenses:
91
+ - MIT
92
+ metadata:
93
+ source_code_uri: https://github.com/SkunkWerkx/HyperUuid
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '3.2'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubygems_version: 3.6.9
109
+ specification_version: 4
110
+ summary: RFC 9562 UUID v4/v5/v6/v7 generation — direct native FFI into a Rust core,
111
+ no runtime bridge.
112
+ test_files: []