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 +7 -0
- data/LICENSE +21 -0
- data/README.md +185 -0
- data/lib/hyperuuid/3.4/hyperuuid_native.so +0 -0
- data/lib/hyperuuid/4.0/hyperuuid_native.so +0 -0
- data/lib/hyperuuid/namespaces.rb +13 -0
- data/lib/hyperuuid/native/README.md +6 -0
- data/lib/hyperuuid/native/linux-arm64/libhyperuuid.so +0 -0
- data/lib/hyperuuid/native/linux-x64/libhyperuuid.so +0 -0
- data/lib/hyperuuid/native/osx-arm64/libhyperuuid.dylib +0 -0
- data/lib/hyperuuid/native/osx-x64/libhyperuuid.dylib +0 -0
- data/lib/hyperuuid/native/win-arm64/hyperuuid.dll +0 -0
- data/lib/hyperuuid/native/win-x64/hyperuuid.dll +0 -0
- data/lib/hyperuuid/native_platform.rb +22 -0
- data/lib/hyperuuid/runtime.rb +204 -0
- data/lib/hyperuuid/uuid.rb +160 -0
- data/lib/hyperuuid.rb +142 -0
- metadata +118 -0
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
|
+
[](https://github.com/SkunkWerkx/HyperUuid/actions/workflows/ci.yml)
|
|
4
|
+
[](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.
|
|
Binary file
|
|
Binary file
|
|
@@ -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.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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: []
|