hyperuuid 0.3.0-x86_64-linux → 0.4.0-x86_64-linux

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ee3dd8398d212a82743b932c04edbf25492f100d487e6adcf48f4b487028b1f
4
- data.tar.gz: b82ef025aa16ce19dbf190421cc9e5dc375ea291e36838dea15d87308bf324b5
3
+ metadata.gz: e7bb71114e32b62e18d3acea28221fbd111f2aa6d7925cb106d3b32f852bd364
4
+ data.tar.gz: cae2dd2712f450af1e28e1f378c5c54117e2b48ed63cdea7cdb267944cbdd9af
5
5
  SHA512:
6
- metadata.gz: cc0815f68794a9e67b58db76f76ad241ccd1be496939dd7a2b59f67fb98ff403f59b7af9e5869f5ff0efafeebd1b74952eebfeb1037b242ed5ad25203b21d613
7
- data.tar.gz: 5dbed4630a7446247546cb2144f40e39a260340463819f1db65f5df0593386dcb6cbdbef658d7c3027071d4b66591209c2b0e80aa01074b02894d9e9185ed7ca
6
+ metadata.gz: 0bb0977d503d56bc8ce2a994213565f1f178e4779fe2d77023d377b1320b7b50b66de809ae318202102a54a1bc7bfdd804c0084188029f961f6e1a0b60d11489
7
+ data.tar.gz: 418665e67759e03373d856321872def9e5edf3d5479a04c8febc0410159a1787749cc8c753576cfe0bb5116d1e3da9f2352e53e7ea4eceb03a94f893a68ad5b9
data/README.md CHANGED
@@ -2,23 +2,22 @@
2
2
 
3
3
  [![CI](https://github.com/SkunkWerkx/HyperUuid/actions/workflows/ci.yml/badge.svg)](https://github.com/SkunkWerkx/HyperUuid/actions/workflows/ci.yml)
4
4
  [![RubyGems](https://img.shields.io/gem/v/hyperuuid.svg)](https://rubygems.org/gems/hyperuuid)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/SkunkWerkx/HyperUuid/blob/master/LICENSE)
5
6
 
6
7
  **Ruby's own stdlib stops at `SecureRandom.uuid` — random v4, full stop. No v5, no v6, no v7. This gem is the whole RFC, with zero gem dependency beyond `Fiddle` (which ships with every Ruby install) — and it's faster than `SecureRandom.uuid` too.**
7
8
 
8
9
  RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation, with
9
- three 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. A third
19
- backend runs the same core as a WebAssembly module inside the
20
- [`wasmtime`](https://rubygems.org/gems/wasmtime) gem, for any platform with no native build
21
- at all — see [WebAssembly (wasmtime)](#webassembly-wasmtime).
10
+ three backends sharing one public surface. Ruby 3.3 is the floor. The fast path is a native
11
+ extension built with [Magnus](https://github.com/matsadler/magnus) — the Rust core linked
12
+ directly into the Ruby VM, auto-selected when loadable — which redefines the low-level
13
+ `Runtime` methods in place on require; everything above them (`Uuid`, the module doors and
14
+ their argument checks, batch slicing) is shared byte-for-byte between backends. The
15
+ universal fallback calls the native `libhyperuuid` shared library via
16
+ [`Fiddle`](https://docs.ruby-lang.org/en/master/Fiddle.html) — dlopen/dlsym plus a raw C-ABI
17
+ call, no runtime bridge. A third backend runs the same core as a WebAssembly module inside
18
+ the [`wasmtime`](https://rubygems.org/gems/wasmtime) gem, for any platform with no native
19
+ build at all. `HyperUuid::BACKEND` reports which one is live; `HYPERUUID_PURE=1` forces
20
+ Fiddle, `HYPERUUID_WASM=1` the wasm module — see [Backends](#backends).
22
21
 
23
22
  ```ruby
24
23
  require "hyperuuid"
@@ -36,12 +35,31 @@ id4.to_sql_order # byte order SQL Server's uniqueidentifier needs to sort by cre
36
35
  batch = HyperUuid.new_v7_batch(1000)
37
36
  ```
38
37
 
39
- Returns `HyperUuid::Uuid`, a minimal value object (`#bytes`, `#to_s`, `#version`, `#variant`,
40
- comparable/hashable) — this gem has no runtime dependency on the `uuid` gem.
41
- `HyperUuid::Namespaces::DNS`/`URL`/`OID`/`X500` are RFC 9562 Section 6.6's well-known
42
- namespaces. `#timestamp` recovers the embedded UTC `Time` from a version 6 or 7 UUID; pass
43
- `raise_on_mismatch: false` to get `nil` back for any other version instead of raising.
44
- `.new_v6`/`.new_v7` also accept a `Time` directly in place of a raw millisecond count.
38
+ ## The doors
39
+
40
+ | Door | Returns | Takes |
41
+ |---|---|---|
42
+ | `new_v4` | `Uuid` | — |
43
+ | `new_v5(namespace, name)` | `Uuid`, the same one for the same pair | a `Uuid` namespace (`Namespaces::DNS`/`URL`/`OID`/`X500`, or your own) and a `String` name — text is hashed as UTF-8, a binary String as its bytes, and it may be empty |
44
+ | `new_v6(time = nil)` `new_v7(time = nil)` | `Uuid` | nothing (now), a `Time`, or an Integer of Unix-epoch milliseconds |
45
+ | `new_v6_batch(count, time = nil)` `new_v7_batch(count, time = nil)` | `Array` of `Uuid` | a count, and the same time forms |
46
+ | `new_v6_batch_bytes(count, time = nil)` `new_v7_batch_bytes(count, time = nil)` | one binary `String`, 16 bytes per UUID | as the batch doors — see [Bulk generation into bytes](#bulk-generation-into-bytes) |
47
+ | `native_version` | the loaded core's `"major.minor.patch"` | — |
48
+ | `available?` | `true`/`false`, never raising | — |
49
+
50
+ Every generating door returns `HyperUuid::Uuid`, a minimal value object — this gem has no
51
+ runtime dependency on the `uuid` gem:
52
+
53
+ | On `Uuid` | |
54
+ |---|---|
55
+ | `Uuid.parse(text)` | the 8-4-4-4-12 hyphenated form `#to_s` produces, either case, and nothing else — an `ArgumentError` otherwise |
56
+ | `Uuid.new(bytes)` | wraps 16 raw RFC 9562-ordered bytes |
57
+ | `Uuid::NIL` `Uuid::MAX` | the RFC 9562 §5.9/§5.10 special values |
58
+ | `#bytes` `#to_s` `#version` `#variant` | the frozen binary String, the hyphenated text, the version nibble, the variant bits |
59
+ | `#timestamp(raise_on_mismatch: true)` | the UTC `Time` embedded in a version 6 or 7 UUID; `nil` instead of an `ArgumentError` for any other version when passed `false` |
60
+ | `#to_sql_order` `#from_sql_order` | to and from the byte order SQL Server's `uniqueidentifier` sorts by |
61
+ | `==` `eql?` `hash` `<=>` | value equality and byte-order comparison (`Comparable`) |
62
+
45
63
  `#to_sql_order`/`#from_sql_order` convert a version 6 or 7 UUID to and from the byte order SQL
46
64
  Server's `uniqueidentifier` needs on the wire to sort by creation order (`#to_sql_order`
47
65
  dispatches on the UUID's own version, matching `#timestamp`'s convention) — computed once in
@@ -49,18 +67,50 @@ the native Rust core rather than reimplemented in Ruby, and verified there (and
49
67
  against the real `System.Data.SqlTypes.SqlGuid` comparator in the C# binding's test suite).
50
68
  Same-millisecond v6 UUIDs aren't guaranteed to sort correctly afterward — v6 has no counter,
51
69
  so `clock_seq`/`node` (not the timestamp) decide ties, the same pre-existing RFC 9562 v6
52
- limitation plain order already has. `#from_sql_order` figures out which version to invert by checking a byte position that's
53
- provably collision-free between the two (see the method's own doc comment).
54
- `HyperUuid::Uuid::NIL`/`MAX` are the RFC 9562 §5.9/§5.10 special-value UUIDs.
55
- `HyperUuid.new_v6_batch(count)`/`new_v7_batch(count)` generate `count` UUIDs sharing one
56
- timestamp capture and one native call, instead of `count` of each.
70
+ limitation plain order already has. `#from_sql_order` figures out which version to invert by
71
+ checking a byte position that's provably collision-free between the two (see the method's own
72
+ doc comment).
73
+
74
+ ### Errors
75
+
76
+ Two exceptions are this gem's own, and both carry the same message on every backend:
77
+
78
+ - `HyperUuid::TimestampOutOfRangeError` — the time cannot be embedded: past version 7's
79
+ 48-bit millisecond field, past version 6's 60-bit one, or negative (a `Time` before the Unix
80
+ epoch included).
81
+ - `HyperUuid::RandomSourceError` — the operating system's random source failed.
82
+
83
+ Through 0.3.0 these were reachable only as `HyperUuid::Runtime::TimestampOutOfRangeError` and
84
+ `HyperUuid::Runtime::RandomSourceError`; those names remain, as aliases of the same classes.
85
+
86
+ Everything else is a caller bug and raises Ruby's own error for it, checked once in the shared
87
+ doors rather than left to whichever backend is live: a `TypeError` for a time that is not a
88
+ `Time`, an Integer or `nil`, for a count that is not an Integer, for a namespace that is not a
89
+ `Uuid` or a name that is not a `String`; an `ArgumentError` for a count outside
90
+ 0..2³² − 1.
91
+
92
+ ### Is the native core there?
93
+
94
+ `HyperUuid.available?` answers whether a backend actually loaded — probed once, cached, never
95
+ raising — for a consumer with a fallback of its own:
96
+
97
+ ```ruby
98
+ id = HyperUuid.available? ? HyperUuid.new_v7.to_s : SecureRandom.uuid
99
+ ```
100
+
101
+ `HyperUuid.native_version` reads the version out of the loaded core itself (its
102
+ `hyperuuid_version` export), so a mismatch against `HyperUuid::VERSION` can be named before
103
+ the first UUID is minted. `HyperUuid::BACKEND` says which backend was *selected*, which is not
104
+ the same question: it reads `:fiddle` on a platform with no library at all, because that is
105
+ the backend whose first call explains what is missing — a `LoadError` naming the path it
106
+ looked for, or `HyperUuid::NativePlatform::UnsupportedPlatformError` naming the platform.
57
107
 
58
108
  ## Why not `SecureRandom.uuid`?
59
109
 
60
110
  `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":
61
111
 
62
112
  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.
63
- 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`.
113
+ 2. **No native-extension compile step.** Third-party UUID gems that go beyond v4 are typically pure Ruby or wrap a C extension compiled at install time; this gem ships its fast path as a prebuilt platform-gem extension and its fallback as a `dlopen`ed prebuilt library — either way, the gem itself compiles nothing at install time ([Install](#install) has the one caveat, which is Fiddle's own).
64
114
  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.
65
115
  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.
66
116
 
@@ -75,7 +125,7 @@ bytes = HyperUuid.new_v7_batch_bytes(1000)
75
125
  first = bytes[0, 16] # ready for a BINARY(16) bind parameter
76
126
  ```
77
127
 
78
- **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.
128
+ **About 20x faster than `new_v7_batch`** for a 1000-UUID batch (10.4 µs versus 210 µs). The native call is identical — the difference is that `new_v7_batch` then allocates a `Uuid` object and its byte Strings for every item on top of it. This hands back the bytes the native core already produced, untouched.
79
129
 
80
130
  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.
81
131
 
@@ -83,34 +133,68 @@ Slice it with `bytes[i * 16, 16]` — which is exactly what `new_v7_batch` does
83
133
 
84
134
  ## Benchmarks
85
135
 
86
- 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):
136
+ `ruby benchmark/uuid_benchmark.rb` reproduces every table here and the bytes-versus-objects
137
+ comparison above. Its first line names the backend, core version and Ruby it measured, so
138
+ run it again under `HYPERUUID_PURE=1` or `HYPERUUID_WASM=1` for the other two backends.
139
+
140
+ Real numbers, `benchmark-ips` on Ruby 4.0.7, linux-x64 on an Intel Core i9-11900H (`ruby benchmark/uuid_benchmark.rb`) — not claimed, measured. With the Magnus backend (the default wherever the extension loads):
87
141
 
88
142
  | Call | i/s | vs `SecureRandom.uuid` |
89
143
  |---|---:|---:|
90
- | `SecureRandom.uuid` | 775,868 | baseline |
91
- | `HyperUuid.new_v7` (explicit ms) | 2,275,763 | **2.9x faster** |
92
- | `HyperUuid.new_v6` (explicit ms) | 2,197,698 | **2.8x faster** |
93
- | `HyperUuid.new_v4` | 2,176,244 | **2.8x faster** |
94
- | `HyperUuid.new_v5` | 1,458,385 | 1.9x faster |
95
- | `HyperUuid.new_v7` (current time) | 701,373 | parity (1.1x slower) |
96
- | `HyperUuid.new_v6` (current time) | 703,748 | parity (1.1x slower) |
144
+ | `SecureRandom.uuid` | 872,365 | baseline |
145
+ | `HyperUuid.new_v4` | 4,278,000 | **4.9x faster** |
146
+ | `HyperUuid.new_v7` (explicit ms) | 3,517,000 | **4.0x faster** |
147
+ | `HyperUuid.new_v6` (explicit ms) | 3,487,000 | **4.0x faster** |
148
+ | `HyperUuid.new_v6` (current time) | 3,324,000 | **3.8x faster** |
149
+ | `HyperUuid.new_v7` (current time) | 3,320,000 | **3.8x faster** |
150
+ | `HyperUuid.new_v5` | 1,838,000 | 2.1x faster |
97
151
 
98
- 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.
152
+ A `HyperUuid.new_v4` — real entropy, correct version/variant bits, minted by the shared Rust core — costs a fifth of what `SecureRandom.uuid` does, because the Magnus extension is an ordinary native method call with nothing marshalled around it.
99
153
 
100
- 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.
154
+ The "current time" rows deserve a footnote, because they will not look like this everywhere. Here they land beside the explicit-ms rows: the only difference between the two is one `Process.clock_gettime(CLOCK_REALTIME)` wall-clock read, and on this machine that read is too cheap to see. On a machine with a slow clock it is the whole story — where a virtualized clock defeats the vDSO fast path the same read costs ~1µs, which puts both current-time rows at parity with `SecureRandom.uuid` while the explicit-ms rows stay well ahead of it. `SecureRandom.uuid` never reads a clock — random v4 is the only thing it does. If your clock is slow and you are minting many, read it once and pass the timestamp, or use the batch doors.
101
155
 
102
- 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.
156
+ The Fiddle fallback (`HYPERUUID_PURE=1`, and any platform without a prebuilt extension) keeps its own diet — a reused thread-local scratch buffer instead of two GC-finalizer-registering mallocs per call, zero-copy `String` passes for read-only inputs, an unsynchronized fast path past the load mutex — landing at 1.25x slower than `SecureRandom.uuid` for v4 (1.26 µs against 1.01 µs in its own run) and 1.45x slower for v6/v7, with the same structural story as before: `Fiddle`'s interpreted marshalling is the floor, and the batch doors are how you amortize it.
103
157
 
104
158
  Batch generation still amortizes per-call cost on both backends — one native call for the whole batch:
105
159
 
106
160
  | Call | i/s (Magnus backend) |
107
161
  |---|---:|
108
- | `new_v6` × 1000 (individual) | 731.3 |
109
- | `new_v6_batch(1000)` | 2,655.6 (**3.6x**) |
110
- | `new_v7` × 1000 (individual) | 710.0 |
111
- | `new_v7_batch(1000)` | 2,744.3 (**3.9x**) |
162
+ | `new_v6` × 1000 (individual) | 3,498 |
163
+ | `new_v6_batch(1000)` | 4,633 (**1.3x**) |
164
+ | `new_v7` × 1000 (individual) | 3,539 |
165
+ | `new_v7_batch(1000)` | 4,677 (**1.3x**) |
166
+
167
+ The multiplier is small on this backend for the best reason available: the individual calls are cheap, so there is little waste left to amortize, and what `new_v7_batch` spends its 214 µs on is building a thousand `Uuid` objects — the byte form above does the same native work in 10 µs. On the Fiddle backend, where each call costs 1.4 µs, the same batch is 6.7x the loop. If you need v5/v6/v7, need many at once, or need this Ruby service's IDs to agree byte-for-byte with a Go or Python service's, that's what this gem is for — and now it's the fast option too, not just the capable one.
168
+
169
+ ## Backends
170
+
171
+ | `HyperUuid::BACKEND` | What runs | Chosen when |
172
+ |---|---|---|
173
+ | `:native` | the core linked into a Magnus extension | a precompiled platform gem carries an extension for this Ruby's ABI — see [Install](#install) |
174
+ | `:fiddle` | `libhyperuuid` for this platform, `dlopen`ed through Fiddle | no extension loads; also the answer when nothing loads at all |
175
+ | `:wasm` | the core as a `wasm32-wasip1` module inside the `wasmtime` gem | no extension and no library for this platform, and `wasmtime` is installed — see [WebAssembly (wasmtime)](#webassembly-wasmtime) |
176
+
177
+ Selection happens once, at `require`, in that order. Two environment variables override it:
112
178
 
113
- 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.
179
+ | Variable | Effect |
180
+ |---|---|
181
+ | `HYPERUUID_WASM` | forces `:wasm`; `require` raises a `LoadError` naming the gem if `wasmtime` is missing |
182
+ | `HYPERUUID_PURE` | forces `:fiddle`, even where an extension would load |
183
+
184
+ Both are read for presence, not value — set to anything at all, `0` and the empty string
185
+ included, they force their backend — and `HYPERUUID_WASM` wins when both are set. A forced
186
+ backend that turns out to have nothing to load does not fall through to another one: the
187
+ first call raises, and `HyperUuid.available?` answers `false`.
188
+
189
+ **Threads.** Every backend is safe to call from any number of threads. The extension runs
190
+ under the GVL. Fiddle releases the GVL for the duration of each call, so that is the one
191
+ backend where Ruby threads run the core truly in parallel — each through its own scratch
192
+ buffer, with the core's v7 counter shared and atomic. The wasm backend serializes every call
193
+ on one Mutex around one instance. `spec/hyperuuid_spec.rb`'s "concurrent callers" examples
194
+ run under all three.
195
+
196
+ **Ractors.** Main Ractor only, on every backend: called from another Ractor the doors raise
197
+ `Ractor::UnsafeError` (`Ractor::IsolationError` under wasm).
114
198
 
115
199
  ## WebAssembly (wasmtime)
116
200
 
@@ -147,21 +231,25 @@ single-threaded, so every call is serialized under one Mutex around one shared i
147
231
  which is also what keeps the core's v7 counter (it lives inside the instance) monotonic
148
232
  across threads and batches, exactly as the one dlopen'd library does natively.
149
233
 
150
- Measured, same box as the benchmarks above (Ruby 4.0.6, linux-arm64, wasmtime 47.0.3):
234
+ Measured, same box and same session as the benchmarks above (Ruby 4.0.7, linux-x64,
235
+ wasmtime 48.0.1), all three backends:
151
236
 
152
- | Call | wasmtime | native (Magnus) |
153
- |---|---:|---:|
154
- | `uuid_new_v7`, single, per call | 867 ns | ~450 ns |
155
- | `uuid_new_v7_batch(1000)`, per call | 40.6 µs (50.6 µs with the 16 KB read back into Ruby) | 24 µs |
237
+ | Call | wasmtime | Fiddle | Magnus |
238
+ |---|---:|---:|---:|
239
+ | `new_v4` | 1.07 µs | 1.26 µs | 234 ns |
240
+ | `new_v7` (explicit ms) | 1.44 µs | 1.47 µs | 284 ns |
241
+ | `new_v7_batch_bytes(1000)` | 22.6 µs | 13.6 µs | 10.4 µs |
242
+ | `new_v7_batch(1000)` → `Uuid` objects | 233 µs | 227 µs | 210 µs |
156
243
 
157
- So roughly 2x the native cost per call, and the batch doors amortize it the same way they do
158
- for Fiddle. The guest's own work is not where the time goes — the identical module runs at
159
- 14 µs per thousand under a JIT-compiled host — it is the crossing, and Ruby's is one of the
160
- cheaper ones.
244
+ So a single call costs what it costs on Fiddle, about five times the Magnus extension, and
245
+ the batch doors amortize it the same way they do for Fiddle: the byte batch is twice the
246
+ native cost, and the object batch is nearly the same on all three because building the objects is
247
+ the whole of it. The guest's own work is not where the time goes — the identical module
248
+ fills a thousand UUIDs in 11 µs under GraalWasm's JIT — it is the crossing.
161
249
 
162
250
  ## Verifying provenance
163
251
 
164
- Every gem RubyGems.org serves — the universal fallback and each of the six precompiled
252
+ Every gem RubyGems.org serves — the universal fallback and each of the five precompiled
165
253
  platform gems — carries its own GitHub build-provenance attestation, signed directly by
166
254
  this repo's own `release.yml` (the `rubygems-publish` job attests `ruby/pkg/*.gem` right
167
255
  before the push), so plain `--repo` verifies any of them:
@@ -172,10 +260,11 @@ gh attestation verify hyperuuid-X.Y.Z-<platform>.gem --repo SkunkWerkx/HyperUuid
172
260
  ```
173
261
 
174
262
  That's the release's second layer of checking, not the only one: before any gem gets built,
175
- the same job verifies all ten native binaries it packs (six FFI libs, four Magnus
176
- extensions) against *their own* attestations — those are signed from `SkunkWerkx/.github`
177
- by `hyper-build-native.yml`, so that check needs `--signer-repo SkunkWerkx/.github` added —
178
- and refuses to proceed on an unverified one. RubyGems.org has no unpublish and no
263
+ the same job verifies every native binary it packs — the Fiddle libraries, the Magnus
264
+ extensions (one per Ruby ABI per platform) and the wasm module — against *their own*
265
+ attestations — those are signed from `SkunkWerkx/.github` by `hyper-build-native.yml`, so
266
+ that check needs `--signer-repo SkunkWerkx/.github` added — and refuses to proceed on an
267
+ unverified one. RubyGems.org has no unpublish and no
179
268
  duplicate-version overwrite, so this all happens while a bad artifact is still reversible.
180
269
  The release run's job summary then re-fetches every gem from the CDN and records
181
270
  attested-vs-served digests, turning "rubygems.org stores an upload verbatim" into a
@@ -189,12 +278,15 @@ more on why `--signer-repo` is needed for some artifacts here and not others.
189
278
  gem install hyperuuid
190
279
  ```
191
280
 
192
- Published to [RubyGems.org](https://rubygems.org/gems/hyperuuid) as real precompiled
193
- "platform gems" — `bundle`/`gem install` auto-selects the matching one for
194
- linux-x64/arm64, osx-x64/arm64, x64-mingw-ucrt or aarch64-mingw-ucrt (the compiled Magnus
195
- native extension, `backend: :native`), falling back automatically to the universal
196
- `ruby`-platform gem (pure Fiddle, zero compile, bundles all 6 platforms' native libs)
197
- everywhere else. No extra configuration needed either way.
281
+ Six gems are published per release: one universal `ruby`-platform gem (Fiddle, with every
282
+ platform's native library and the wasm module bundled) plus five precompiled Magnus platform
283
+ gems (`x86_64-linux`, `aarch64-linux`, `arm64-darwin`, `x64-mingw-ucrt`,
284
+ `aarch64-mingw-ucrt`) that `gem install` and `bundle` auto-select when they match. There is
285
+ no `x86_64-darwin` platform gem: an Intel Mac installs the universal gem and runs on Fiddle
286
+ over the bundled `osx-x64` library, as Alpine does. A platform
287
+ gem carries the same libraries and module beside its extensions, so the Fiddle and wasm
288
+ backends are still there behind `HYPERUUID_PURE` and `HYPERUUID_WASM`. No extra configuration
289
+ needed either way.
198
290
 
199
291
  Selection has **two** axes here, unlike every other binding in this repo. A Magnus extension
200
292
  is bound to one Ruby minor ABI — there's no `abi3` equivalent to collapse the version axis the
@@ -202,11 +294,20 @@ way [the Python binding's](../python/) wheels do — so each platform gem is a "
202
294
  carrying one compiled extension per supported Ruby, under `lib/hyperuuid/<minor>/`, and picks
203
295
  one at `require` time:
204
296
 
205
- | Ruby | linux-x64/arm64, osx-x64/arm64, x64-mingw-ucrt, aarch64-mingw-ucrt | anywhere else (musl/Alpine, …) |
206
- | --- | --- | --- |
207
- | 4.0 (primary) | Magnus, `backend: :native` | Fiddle |
208
- | 3.4 (floor, until its EOL 2028-03-31) | Magnus, `backend: :native` | Fiddle |
209
- | 3.2 / 3.3 | Fiddle | Fiddle |
297
+ | Ruby | glibc Linux, macOS, Windows — x64 and arm64 | musl Linux (Alpine) — x64 and arm64 | anywhere else |
298
+ | --- | --- | --- | --- |
299
+ | 4.0 (primary) | Magnus, `BACKEND == :native` | Fiddle | wasm, if `wasmtime` is installed |
300
+ | 3.4 (until its EOL 2028-03-31) | Magnus, `BACKEND == :native` | Fiddle | wasm, if `wasmtime` is installed |
301
+ | 3.3 (the floor) | Fiddle | Fiddle | wasm, if `wasmtime` is installed |
302
+
303
+ What stands behind each cell: CI runs the whole suite for the first column on every push —
304
+ Magnus on Ruby 3.4 and 4.0, Fiddle and wasm on 4.0, on all five platforms with a CI leg
305
+ (Intel macOS has none) — and the Fiddle
306
+ suite for the musl column inside an Alpine container. The 3.3 row is the same Fiddle code
307
+ path, run with the Docker command under [Development](#development) (Ruby 3.3 with the
308
+ Fiddle 1.1.2 it ships) rather than on every push. The last column — wasm chosen
309
+ automatically because nothing native exists — has no leg of its own: CI runs the wasm
310
+ backend's suite forced, on platforms that have native builds too.
210
311
 
211
312
  The platform gems declare `required_ruby_version >= 3.4, < 4.1` precisely so RubyGems
212
313
  *declines* them outside that range and resolves the universal gem instead — a wrong-ABI
@@ -217,43 +318,111 @@ don't link libruby at all, so one can load successfully against the wrong ABI an
217
318
  later. When 3.4 goes EOL it simply leaves the matrix and its users fall back to Fiddle, which
218
319
  is exactly what the fallback is for.
219
320
 
220
- An earlier edition of this section said the fallback covered "Windows included, since Magnus
221
- doesn't target it." That was wrong in both halves. MinGW is the *only* Windows flavour
222
- `rb-sys` targets — its own `data/toolchains.json` maps `x64-mingw-ucrt` to the
223
- `x86_64-pc-windows-gnu` Rust target, `supported: true`; the target it has no support for is
224
- `x86_64-pc-windows-msvc`. And Windows is where the Fiddle fallback cost the most: measured
225
- on win-x64, Ruby 3.4, the Magnus backend does `new_v4` in 406ns against Fiddle's 2407ns
226
- (**5.9x**) and `new_v7` in 595ns against 2759ns (**4.6x**) — a far wider gap than any Linux
227
- or macOS leg shows.
228
-
229
- Windows-on-ARM is no longer the exception it was. `rb-sys` maps `aarch64-mingw-ucrt` to the
230
- `aarch64-pc-windows-gnullvm` Rust target (`supported: true`), and RubyInstaller ships an
231
- `aarch64-mingw-ucrt` build of both ABIs in the table above, so win-arm64 now gets the same fat
232
- platform gem as every other leg. Measured on real Windows-on-ARM hardware, Ruby 4.0.6, the
233
- Magnus backend does `new_v4` in 416ns against Fiddle's 2299ns (**5.5x**) and `new_v7` in 621ns
234
- against 2474ns (**4.0x**) — the same shape the x64 leg shows.
235
-
236
- What differs from the x64 build follows from `gnullvm` being LLVM-based where
237
- `x86_64-pc-windows-gnu` is GCC-based. The linker driver is `aarch64-w64-mingw32-clang` from
238
- MSYS2's CLANGARM64 environment — RubyInstaller's own ARM devkit installs it locally, and
239
- `ruby/setup-ruby` installs it on `windows-11-arm` — so there is no `link-self-contained=no`
240
- dance. Two flags are load-bearing:
241
-
242
- - **`-l static=unwind`.** rustc emits its `-lunwind` in the linker's *dynamic* section, where
243
- CLANGARM64 offers both `libunwind.a` and `libunwind.dll.a` and lld prefers the latter. Left
244
- alone, the extension acquires a runtime dependency on `libunwind.dll` — a file no consumer
245
- of the gem would have.
246
- - **`BINDGEN_EXTRA_CLANG_ARGS=--target=aarch64-w64-mingw32`.** bindgen hands clang the *Rust*
247
- target triple when cargo cross-compiles, and clang rejects `aarch64-pc-windows-gnullvm`
248
- outright (`version 'llvm' in target triple ... is invalid`), then fails behind that on a
249
- missing `stdalign.h` it never reached its own resource dir to find. `aarch64-w64-mingw32`
250
- is the same ABI spelled the way clang accepts, exactly as x64 spells its gnu target
251
- `x86_64-w64-mingw32`.
252
-
253
- That second one was briefly documented here as *unnecessary*, on the strength of a local
254
- build where it genuinely was: that build used a `gnullvm` **host** toolchain, so host equals
255
- target, cargo was not cross-compiling, and bindgen injected no `--target` at all. CI's host is
256
- MSVC, so it does. A local build says nothing about that step unless its host triple matches
257
- the runner's.
321
+ **musl.** RubyGems does not tell musl from glibc for these gems: on Alpine, `gem install`
322
+ picks the `x86_64-linux` or `aarch64-linux` platform gem exactly as it does on any other
323
+ Linux (checked with RubyGems 4.0.20 on `x86_64-linux-musl`). The Magnus extension inside is
324
+ linked against glibc and cannot load there — `require` fails cleanly on the missing
325
+ `ld-linux` loader — so the gem falls back to Fiddle, which loads the musl build of the core
326
+ (`native/linux-musl-x64` or `native/linux-musl-arm64`, chosen from `RUBY_PLATFORM`) that
327
+ every gem carries. Alpine therefore works as installed, on the Fiddle backend and at Fiddle's
328
+ speed. Releases through 0.3.0 carried no musl library: there the fallback found only the
329
+ glibc one, and the first call raised `Fiddle::DLError`.
330
+
331
+ **Nothing in this gem is ever compiled, on any platform.** Its one dependency can be:
332
+ `fiddle` is a bundled gem on Ruby 4.0 and a default gem on 3.3 and 3.4, and `gem install` is
333
+ satisfied by the copy Ruby ships. Bundler resolves the newest `fiddle` on rubygems.org
334
+ instead, and when that is newer than the one your Ruby ships — true today on 3.3 and 3.4, not
335
+ on 4.0 — it builds Fiddle's own C extension, which takes a compiler and libffi's headers
336
+ (`apk add build-base libffi-dev` on Alpine). Holding `fiddle` at your Ruby's version in the
337
+ `Gemfile.lock` avoids that.
338
+
339
+ Both Windows architectures get a Magnus gem. MinGW is the *only* Windows flavour `rb-sys`
340
+ targets (`x64-mingw-ucrt` and `aarch64-mingw-ucrt`, both `supported: true` in its own
341
+ `data/toolchains.json`); the one it has no support for is MSVC. Windows is also where the
342
+ Fiddle fallback costs the most: measured on win-x64, Ruby 3.4, the Magnus backend does
343
+ `new_v4` in 406ns against Fiddle's 2407ns (**5.9x**) and `new_v7` in 595ns against 2759ns
344
+ (**4.6x**) — a far wider gap than any Linux or macOS leg shows — and on real Windows-on-ARM
345
+ hardware, Ruby 4.0.6, `new_v4` in 416ns against Fiddle's 2299ns (**5.5x**) and `new_v7` in
346
+ 621ns against 2474ns (**4.0x**), the same shape. Both extensions are built for the `gnullvm`
347
+ Rust targets rather than `gnu` — the same mingw-w64/UCRT ABI RubyInstaller's Ruby uses,
348
+ linked with LLVM and compiler-rt instead of GCC and a statically-linked libgcc, which is what
349
+ keeps the shipped extension small. The build script, and the two flags that are load-bearing
350
+ on the ARM leg (a static libunwind, and a clang-spelled `--target` for bindgen), live in the
351
+ forge's `ruby-magnus` action (`build-magnus.sh`), shared with every other Hyper* repo.
352
+
353
+ ## Development
354
+
355
+ Everything below runs from a checkout, with `rust/` beside `ruby/`; none of it is needed to
356
+ use the gem. The Fiddle and wasm backends find the in-repo builds on their own when nothing
357
+ is staged under `lib/hyperuuid/native/`. The Magnus backend does not — an extension has to be
358
+ built for the Ruby you are running and put where `require` looks — and that is what
359
+ `rake native:dev` is for.
360
+
361
+ ```sh
362
+ cd rust
363
+ cargo build --release # libhyperuuid, what the Fiddle backend loads
364
+ cargo build --release --target wasm32-wasip1 # hyperuuid.wasm, what the wasm backend loads
365
+
366
+ cd ../ruby
367
+ bundle install
368
+ bundle exec rake native:dev # build the Magnus extension for this Ruby and stage it
369
+ bundle exec rspec # BACKEND == :native
370
+ HYPERUUID_PURE=1 bundle exec rspec # BACKEND == :fiddle
371
+ HYPERUUID_WASM=1 bundle exec rspec # BACKEND == :wasm
372
+ bundle exec rake docs:check # every public object carries a doc comment
373
+ ruby benchmark/uuid_benchmark.rb # prints the backend it measured
374
+ ```
375
+
376
+ `rake native:dev` runs `cargo ruby-ext` (an alias in `rust/.cargo/config.toml` that builds the
377
+ `ruby` feature into `rust/target/ruby/`, never over the plain library in
378
+ `rust/target/release/`) and copies the result to
379
+ `lib/hyperuuid/<minor>/hyperuuid_native.<so|bundle>` — the path `lib/hyperuuid.rb` tries
380
+ first. The copy is also a rename, and it is the step `cargo ruby-ext` alone does not do: cargo
381
+ names its output `libhyperuuid.so`, and Ruby derives the `Init_` function it calls from the
382
+ file name it was asked to `require`. Three things worth knowing:
383
+
384
+ - **Run it again after changing `rust/`.** The staged file is a copy; nothing rebuilds it.
385
+ - **One staging per Ruby.** An extension is bound to one Ruby minor, so under a second Ruby
386
+ (`RBENV_VERSION=3.4.11 bundle exec rake native:dev`, say) the task rebuilds for that ABI
387
+ and stages beside the first, each Ruby loading its own.
388
+ - **Without it, `bundle exec rspec` still passes — on Fiddle.** The examples under "native
389
+ backend" report themselves pending with `BACKEND=fiddle`; that line, or
390
+ `ruby -Ilib -rhyperuuid -e 'p HyperUuid::BACKEND'`, is how to tell which backend a run
391
+ exercised. Deleting `lib/hyperuuid/<minor>/` goes back to Fiddle.
392
+
393
+ The task covers Linux and macOS. The Windows extensions need the `gnullvm` targets and linker
394
+ flags in the forge's `build-magnus.sh`, which is also what CI uses on every platform.
395
+
396
+ The musl build has no host to run on outside a container. With the core built for musl at
397
+ `rust/target/musl/linux-musl-x64/libhyperuuid.so`, this runs the Fiddle suite against it on
398
+ Alpine, with the checkout mounted read-only:
399
+
400
+ ```sh
401
+ docker run --rm -v "$PWD/..":/src:ro ruby:4.0-alpine sh -euc '
402
+ mkdir /work && cp -r /src/ruby /work/ruby
403
+ mkdir -p /work/ruby/lib/hyperuuid/native/linux-musl-x64
404
+ cp /src/rust/target/musl/linux-musl-x64/libhyperuuid.so /work/ruby/lib/hyperuuid/native/linux-musl-x64/
405
+ cd /work/ruby && rm -f Gemfile.lock
406
+ BUNDLE_WITHOUT=wasm bundle install --quiet --prefer-local
407
+ HYPERUUID_PURE=1 BUNDLE_WITHOUT=wasm bundle exec rspec'
408
+ ```
409
+
410
+ The floor's test is the same container on `ruby:3.3-alpine`. Bundler would compile a newer
411
+ Fiddle there (see [Install](#install)), so this one goes around Bundler and runs against the
412
+ Fiddle that Ruby 3.3 ships:
413
+
414
+ ```sh
415
+ docker run --rm -v "$PWD/..":/src:ro ruby:3.3-alpine sh -euc '
416
+ mkdir /work && cp -r /src/ruby /work/ruby
417
+ mkdir -p /work/ruby/lib/hyperuuid/native/linux-musl-x64
418
+ cp /src/rust/target/musl/linux-musl-x64/libhyperuuid.so /work/ruby/lib/hyperuuid/native/linux-musl-x64/
419
+ cd /work/ruby && rm -f Gemfile Gemfile.lock
420
+ gem install rspec -v "~> 3.13" --no-document --silent
421
+ HYPERUUID_PURE=1 rspec'
422
+ ```
258
423
 
259
424
  See [the repo root README](../README.md) for the full RFC 9562 coverage table and the state of every other language binding.
425
+
426
+ ## License
427
+
428
+ [MIT](https://github.com/SkunkWerkx/HyperUuid/blob/master/LICENSE)
Binary file
Binary file
@@ -0,0 +1,12 @@
1
+ module HyperUuid
2
+ # Raised when the operating system's random source fails while a UUID is being minted —
3
+ # the one way `new_v4`, `new_v6`, `new_v7` and their batch forms can fail once their
4
+ # arguments are valid. Carries the same message on every backend.
5
+ class RandomSourceError < StandardError; end
6
+
7
+ # Raised when a timestamp cannot be embedded in the UUID being minted: a version 7 value
8
+ # past RFC 9562's 48-bit millisecond field, a version 6 value past its 60-bit field, or
9
+ # any negative one (a `Time` before the Unix epoch included). Carries the same message on
10
+ # every backend.
11
+ class TimestampOutOfRangeError < StandardError; end
12
+ end
@@ -4,18 +4,24 @@ module HyperUuid
4
4
  module NativePlatform
5
5
  class UnsupportedPlatformError < StandardError; end
6
6
 
7
- def self.rid_and_library_name
8
- is_arm = RUBY_PLATFORM.match?(/arm64|aarch64/)
7
+ # +platform+ is a parameter only so the specs can walk the whole table from one host;
8
+ # every real caller takes the default.
9
+ def self.rid_and_library_name(platform = RUBY_PLATFORM)
10
+ arch = platform.match?(/arm64|aarch64/) ? "arm64" : "x64"
9
11
 
10
- case RUBY_PLATFORM
12
+ case platform
11
13
  when /mingw|mswin|windows/
12
- is_arm ? ["win-arm64", "hyperuuid.dll"] : ["win-x64", "hyperuuid.dll"]
14
+ ["win-#{arch}", "hyperuuid.dll"]
13
15
  when /darwin/
14
- is_arm ? ["osx-arm64", "libhyperuuid.dylib"] : ["osx-x64", "libhyperuuid.dylib"]
16
+ ["osx-#{arch}", "libhyperuuid.dylib"]
15
17
  when /linux/
16
- is_arm ? ["linux-arm64", "libhyperuuid.so"] : ["linux-x64", "libhyperuuid.so"]
18
+ # Two C libraries, two builds: a glibc-linked library cannot be relied on to dlopen
19
+ # into a musl process (Alpine), so musl gets RIDs of its own. Ruby names the libc
20
+ # in its platform string there ("x86_64-linux-musl") and leaves it off on glibc.
21
+ os = platform.include?("musl") ? "linux-musl" : "linux"
22
+ ["#{os}-#{arch}", "libhyperuuid.so"]
17
23
  else
18
- raise UnsupportedPlatformError, "hyperuuid: unsupported platform RUBY_PLATFORM=#{RUBY_PLATFORM}"
24
+ raise UnsupportedPlatformError, "hyperuuid: unsupported platform RUBY_PLATFORM=#{platform}"
19
25
  end
20
26
  end
21
27
  end