hyperuuid-wasm 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 891b6e2ed78c68a12067a1dd6e8f4ba3a7328f6ba3f5ec3853e56d7c37275baa
4
+ data.tar.gz: 4252c3cee5c2654d852c3b3818b7de53cb9908ede2f30e6a530ebdaebf40cdd0
5
+ SHA512:
6
+ metadata.gz: 6fda24dba0eefff039f615a7b80ba66f1948cfc4579bc96be3d81d33f015a30ea821b18ce29cf5f6c9f76f5b72b3f4e7ffa01f3f111ea9b4bd5c7f148d805055
7
+ data.tar.gz: 011a274e14ea76f4ca6701ca0341c81dd2873b5b658929be8f2834832486dbf7e44312cb2f2ec6ae46da7b952d9e96e50e5c4d2785ef90c1dd32ba38e8bb9127
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,431 @@
1
+ # hyperuuid
2
+
3
+ [![CI](https://github.com/SkunkWerkx/HyperUuid/actions/workflows/ci.yml/badge.svg)](https://github.com/SkunkWerkx/HyperUuid/actions/workflows/ci.yml)
4
+ [![RubyGems](https://img.shields.io/gem/v/hyperuuid.svg)](https://rubygems.org/gems/hyperuuid)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/SkunkWerkx/HyperUuid/blob/master/LICENSE)
6
+
7
+ **Ruby's own stdlib stops at `SecureRandom.uuid` — random v4, full stop. No v5, no v6, no v7. This gem is the whole RFC, with zero gem dependency beyond `Fiddle` (which ships with every Ruby install) — and it's faster than `SecureRandom.uuid` too.**
8
+
9
+ RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation, with
10
+ two backends sharing one public surface. Ruby 3.3 is the floor. The fast path is a native
11
+ extension built with [Magnus](https://github.com/matsadler/magnus) — the Rust core linked
12
+ directly into the Ruby VM, auto-selected when loadable — which redefines the low-level
13
+ `Runtime` methods in place on require; everything above them (`Uuid`, the module doors and
14
+ their argument checks, batch slicing) is shared byte-for-byte between backends. The
15
+ last-resort fallback, in the universal gem only, calls the native `libhyperuuid` shared
16
+ library via [`Fiddle`](https://docs.ruby-lang.org/en/master/Fiddle.html) — dlopen/dlsym plus
17
+ a raw C-ABI call, no runtime bridge. `HyperUuid::BACKEND` reports which one is live;
18
+ `HYPERUUID_PURE=1` forces Fiddle for testing — see [Backends](#backends).
19
+
20
+ ```ruby
21
+ require "hyperuuid"
22
+
23
+ id = HyperUuid.new_v4
24
+ id2 = HyperUuid.new_v5(HyperUuid::Namespaces::DNS, "example.com")
25
+ id3 = HyperUuid.new_v6
26
+ id4 = HyperUuid.new_v7
27
+
28
+ id4.timestamp # recover the embedded UTC Time
29
+ id4.timestamp(raise_on_mismatch: false) # nil instead of raising if id4 isn't v6/v7
30
+ id4.to_sql_order # byte order SQL Server's uniqueidentifier needs to sort by creation order
31
+
32
+ # One native call, one random-bytes fetch, one counter reservation for the whole batch:
33
+ batch = HyperUuid.new_v7_batch(1000)
34
+ ```
35
+
36
+ ## The doors
37
+
38
+ | Door | Returns | Takes |
39
+ |---|---|---|
40
+ | `new_v4` | `Uuid` | — |
41
+ | `new_v5(namespace, name)` | `Uuid`, the same one for the same pair | a `Uuid` namespace (`Namespaces::DNS`/`URL`/`OID`/`X500`, or your own) and a `String` name — text is hashed as UTF-8, a binary String as its bytes, and it may be empty |
42
+ | `new_v6(time = nil)` `new_v7(time = nil)` | `Uuid` | nothing (now), a `Time`, or an Integer of Unix-epoch milliseconds |
43
+ | `new_v6_batch(count, time = nil)` `new_v7_batch(count, time = nil)` | `Array` of `Uuid` | a count, and the same time forms |
44
+ | `new_v6_batch_bytes(count, time = nil)` `new_v7_batch_bytes(count, time = nil)` | one binary `String`, 16 bytes per UUID | as the batch doors — see [Bulk generation into bytes](#bulk-generation-into-bytes) |
45
+ | `native_version` | the loaded core's `"major.minor.patch"` | — |
46
+ | `available?` | `true`/`false`, never raising | — |
47
+
48
+ Every generating door returns `HyperUuid::Uuid`, a minimal value object — this gem has no
49
+ runtime dependency on the `uuid` gem:
50
+
51
+ | On `Uuid` | |
52
+ |---|---|
53
+ | `Uuid.parse(text)` | the 8-4-4-4-12 hyphenated form `#to_s` produces, either case, and nothing else — an `ArgumentError` otherwise |
54
+ | `Uuid.new(bytes)` | wraps 16 raw RFC 9562-ordered bytes |
55
+ | `Uuid::NIL` `Uuid::MAX` | the RFC 9562 §5.9/§5.10 special values |
56
+ | `#bytes` `#to_s` `#version` `#variant` | the frozen binary String, the hyphenated text, the version nibble, the variant bits |
57
+ | `#timestamp(raise_on_mismatch: true)` | the UTC `Time` embedded in a version 6 or 7 UUID; `nil` instead of an `ArgumentError` for any other version when passed `false` |
58
+ | `#to_sql_order` `#from_sql_order` | to and from the byte order SQL Server's `uniqueidentifier` sorts by |
59
+ | `==` `eql?` `hash` `<=>` | value equality and byte-order comparison (`Comparable`) |
60
+
61
+ `#to_sql_order`/`#from_sql_order` convert a version 6 or 7 UUID to and from the byte order SQL
62
+ Server's `uniqueidentifier` needs on the wire to sort by creation order (`#to_sql_order`
63
+ dispatches on the UUID's own version, matching `#timestamp`'s convention) — computed once in
64
+ the native Rust core rather than reimplemented in Ruby, and verified there (and independently
65
+ against the real `System.Data.SqlTypes.SqlGuid` comparator in the C# binding's test suite).
66
+ Same-millisecond v6 UUIDs aren't guaranteed to sort correctly afterward — v6 has no counter,
67
+ so `clock_seq`/`node` (not the timestamp) decide ties, the same pre-existing RFC 9562 v6
68
+ limitation plain order already has. `#from_sql_order` figures out which version to invert by
69
+ checking a byte position that's provably collision-free between the two (see the method's own
70
+ doc comment).
71
+
72
+ ### Errors
73
+
74
+ Two exceptions are this gem's own, and both carry the same message on every backend:
75
+
76
+ - `HyperUuid::TimestampOutOfRangeError` — the time cannot be embedded: past version 7's
77
+ 48-bit millisecond field, past version 6's 60-bit one, or negative (a `Time` before the Unix
78
+ epoch included).
79
+ - `HyperUuid::RandomSourceError` — the operating system's random source failed.
80
+
81
+ Through 0.3.0 these were reachable only as `HyperUuid::Runtime::TimestampOutOfRangeError` and
82
+ `HyperUuid::Runtime::RandomSourceError`; those names remain, as aliases of the same classes.
83
+
84
+ Everything else is a caller bug and raises Ruby's own error for it, checked once in the shared
85
+ doors rather than left to whichever backend is live: a `TypeError` for a time that is not a
86
+ `Time`, an Integer or `nil`, for a count that is not an Integer, for a namespace that is not a
87
+ `Uuid` or a name that is not a `String`; an `ArgumentError` for a count outside
88
+ 0..2³² − 1.
89
+
90
+ ### Is the native core there?
91
+
92
+ `HyperUuid.available?` answers whether a backend actually loaded — probed once, cached, never
93
+ raising — for a consumer with a fallback of its own:
94
+
95
+ ```ruby
96
+ id = HyperUuid.available? ? HyperUuid.new_v7.to_s : SecureRandom.uuid
97
+ ```
98
+
99
+ `HyperUuid.native_version` reads the version out of the loaded core itself (its
100
+ `hyperuuid_version` export), so a mismatch against `HyperUuid::VERSION` can be named before
101
+ the first UUID is minted. `HyperUuid::BACKEND` says which backend was *selected*, which is not
102
+ the same question: it reads `:fiddle` on a platform with no library at all, because that is
103
+ the backend whose first call explains what is missing — a `LoadError` naming the path it
104
+ looked for, or `HyperUuid::NativePlatform::UnsupportedPlatformError` naming the platform.
105
+
106
+ ## Why not `SecureRandom.uuid`?
107
+
108
+ `SecureRandom.uuid` only ever gives you a random v4 UUID — Ruby's stdlib has no built-in v5, v6, or v7 at all. If you need more than that, the choice is really "which gem":
109
+
110
+ 1. **Full RFC 9562 coverage, one gem, zero extra dependency.** v4/v5/v6/v7 plus batch generation plus `Nil`/`Max`, and nothing added to your `Gemfile.lock` beyond `Fiddle` — Ruby's own bundled FFI layer, not a third-party C extension to compile — and on the precompiled platform gems not even that.
111
+ 2. **No native-extension compile step.** Third-party UUID gems that go beyond v4 are typically pure Ruby or wrap a C extension compiled at install time; this gem ships its fast path as a prebuilt platform-gem extension and its fallback as a `dlopen`ed prebuilt library — either way, the gem itself compiles nothing at install time ([Install](#install) has the one caveat, which is Fiddle's own).
112
+ 3. **Batch generation.** `new_v7_batch(1000)` shares one timestamp capture, one random-bytes fetch, and one counter reservation across the whole batch instead of paying per-item overhead a thousand times over.
113
+ 4. **Cross-language consistency.** The same Rust core mints v5 namespace UUIDs for Python, Go, C#, and every other binding in this repo — verified in CI to match Python's own `uuid.uuid5` byte-for-byte. If your system isn't Ruby-only, no Ruby-only gem can offer that.
114
+
115
+ The honest trade-off: this gem is native code, not pure Ruby — a precompiled extension in each platform gem, and on the universal gem a platform-specific `libhyperuuid.so`/`.dylib`/`.dll` loaded through Fiddle — so it runs only where one of those was built. If plain v4 randomness is all you need, `SecureRandom.uuid` is simpler and already in stdlib — that's a completely reasonable choice.
116
+
117
+ ## Bulk generation into bytes
118
+
119
+ `new_v6_batch_bytes` and `new_v7_batch_bytes` return the batch as one binary `String` of raw RFC 9562-ordered bytes — 16 per UUID — instead of an Array of `Uuid` objects:
120
+
121
+ ```ruby
122
+ bytes = HyperUuid.new_v7_batch_bytes(1000)
123
+ first = bytes[0, 16] # ready for a BINARY(16) bind parameter
124
+ ```
125
+
126
+ **About 20x faster than `new_v7_batch`** for a 1000-UUID batch (10.4 µs versus 210 µs). The native call is identical — the difference is that `new_v7_batch` then allocates a `Uuid` object and its byte Strings for every item on top of it. This hands back the bytes the native core already produced, untouched.
127
+
128
+ The catch, and it inverts the advice: **if you need `Uuid` objects, keep using `new_v7_batch`.** Slicing these bytes into objects yourself just relocates the identical allocations into your own code, and measures no better — sometimes worse. Reach for the byte form only when bytes are the destination: a bind parameter, a wire format, a bulk load.
129
+
130
+ Slice it with `bytes[i * 16, 16]` — which is exactly what `new_v7_batch` does internally.
131
+
132
+ ## Benchmarks
133
+
134
+ `ruby benchmark/uuid_benchmark.rb` reproduces every table here and the bytes-versus-objects
135
+ comparison above. Its first line names the backend, core version and Ruby it measured, so
136
+ run it again under `HYPERUUID_PURE=1` for the other backend.
137
+
138
+ Real numbers, `benchmark-ips` on Ruby 4.0.7, linux-x64 on an Intel Core i9-11900H (`ruby benchmark/uuid_benchmark.rb`) — not claimed, measured. With the Magnus backend (the default wherever the extension loads):
139
+
140
+ | Call | i/s | vs `SecureRandom.uuid` |
141
+ |---|---:|---:|
142
+ | `SecureRandom.uuid` | 872,365 | baseline |
143
+ | `HyperUuid.new_v4` | 4,278,000 | **4.9x faster** |
144
+ | `HyperUuid.new_v7` (explicit ms) | 3,517,000 | **4.0x faster** |
145
+ | `HyperUuid.new_v6` (explicit ms) | 3,487,000 | **4.0x faster** |
146
+ | `HyperUuid.new_v6` (current time) | 3,324,000 | **3.8x faster** |
147
+ | `HyperUuid.new_v7` (current time) | 3,320,000 | **3.8x faster** |
148
+ | `HyperUuid.new_v5` | 1,838,000 | 2.1x faster |
149
+
150
+ A `HyperUuid.new_v4` — real entropy, correct version/variant bits, minted by the shared Rust core — costs a fifth of what `SecureRandom.uuid` does, because the Magnus extension is an ordinary native method call with nothing marshalled around it.
151
+
152
+ The "current time" rows deserve a footnote, because they will not look like this everywhere. Here they land beside the explicit-ms rows: the only difference between the two is one `Process.clock_gettime(CLOCK_REALTIME)` wall-clock read, and on this machine that read is too cheap to see. On a machine with a slow clock it is the whole story — where a virtualized clock defeats the vDSO fast path the same read costs ~1µs, which puts both current-time rows at parity with `SecureRandom.uuid` while the explicit-ms rows stay well ahead of it. `SecureRandom.uuid` never reads a clock — random v4 is the only thing it does. If your clock is slow and you are minting many, read it once and pass the timestamp, or use the batch doors.
153
+
154
+ The Fiddle fallback (`HYPERUUID_PURE=1`, and any platform without a prebuilt extension) keeps its own diet — a reused thread-local scratch buffer instead of two GC-finalizer-registering mallocs per call, zero-copy `String` passes for read-only inputs, an unsynchronized fast path past the load mutex — landing at 1.25x slower than `SecureRandom.uuid` for v4 (1.26 µs against 1.01 µs in its own run) and 1.45x slower for v6/v7, with the same structural story as before: `Fiddle`'s interpreted marshalling is the floor, and the batch doors are how you amortize it.
155
+
156
+ Batch generation still amortizes per-call cost on both backends — one native call for the whole batch:
157
+
158
+ | Call | i/s (Magnus backend) |
159
+ |---|---:|
160
+ | `new_v6` × 1000 (individual) | 3,498 |
161
+ | `new_v6_batch(1000)` | 4,633 (**1.3x**) |
162
+ | `new_v7` × 1000 (individual) | 3,539 |
163
+ | `new_v7_batch(1000)` | 4,677 (**1.3x**) |
164
+
165
+ The multiplier is small on this backend for the best reason available: the individual calls are cheap, so there is little waste left to amortize, and what `new_v7_batch` spends its 214 µs on is building a thousand `Uuid` objects — the byte form above does the same native work in 10 µs. On the Fiddle backend, where each call costs 1.4 µs, the same batch is 6.7x the loop. If you need v5/v6/v7, need many at once, or need this Ruby service's IDs to agree byte-for-byte with a Go or Python service's, that's what this gem is for — and now it's the fast option too, not just the capable one.
166
+
167
+ ## Backends
168
+
169
+ | `HyperUuid::BACKEND` | What runs | Chosen when |
170
+ |---|---|---|
171
+ | `:native` | the core linked into a Magnus extension | a precompiled platform gem is installed — every one carries an extension for each Ruby it installs on; see [Install](#install) — or the `hyperuuid-wasm` gem is linked into a ruby.wasm interpreter; see [Ruby in the browser](#ruby-in-the-browser) |
172
+ | `:fiddle` | `libhyperuuid` for this platform, `dlopen`ed through Fiddle | no extension loads: the universal gem, on a Ruby or platform no platform gem covers; also the answer when nothing loads at all |
173
+
174
+ Selection happens once, at `require`, in that order.
175
+
176
+ `HYPERUUID_PURE` forces `:fiddle`, even where an extension would load. It is a testing and
177
+ diagnostic switch — CI runs the whole suite through it, and it is how to rule an extension
178
+ problem in or out — not a setting a deployment needs. It is read for presence, not value: set
179
+ to anything at all, `0` and the empty string included, it forces Fiddle. A forced backend that
180
+ turns out to have nothing to load does not fall through to another one: the first call raises,
181
+ and `HyperUuid.available?` answers `false`. That is what happens inside a platform gem, which
182
+ carries no Fiddle library: the `LoadError` names the universal gem
183
+ (`gem install hyperuuid --platform ruby`, or Bundler's `force_ruby_platform`), where the
184
+ Fiddle backend lives.
185
+
186
+ **Threads.** Every backend is safe to call from any number of threads. The extension runs
187
+ under the GVL. Fiddle releases the GVL for the duration of each call, so that is the one
188
+ backend where Ruby threads run the core truly in parallel — each through its own scratch
189
+ buffer, with the core's v7 counter shared and atomic. `spec/hyperuuid_spec.rb`'s "concurrent
190
+ callers" examples run under both.
191
+
192
+ **Ractors.** Main Ractor only, on every backend: called from another Ractor the doors raise
193
+ `Ractor::UnsafeError`.
194
+
195
+ ## Verifying provenance
196
+
197
+ Every gem RubyGems.org serves — the universal fallback, each of the seven precompiled
198
+ platform gems and `hyperuuid-wasm` — carries its own GitHub build-provenance attestation, signed directly by
199
+ this repo's own `release.yml` (the `rubygems-publish` job attests `ruby/pkg/*.gem` right
200
+ before the push), so plain `--repo` verifies any of them:
201
+
202
+ ```sh
203
+ gem fetch hyperuuid -v X.Y.Z --platform <platform> # or omit --platform for the universal gem
204
+ gh attestation verify hyperuuid-X.Y.Z-<platform>.gem --repo SkunkWerkx/HyperUuid
205
+ ```
206
+
207
+ That's the release's second layer of checking, not the only one: before any gem gets built,
208
+ the same job verifies every native binary it packs — the Fiddle libraries, the Magnus
209
+ extensions (one per Ruby ABI per platform), the `wasm32-wasip1` archives — against *their own*
210
+ attestations — those are signed from `SkunkWerkx/.github` by `hyper-build-native.yml` and
211
+ `hyper-build-wasm.yml`, so
212
+ that check needs `--signer-repo SkunkWerkx/.github` added — and refuses to proceed on an
213
+ unverified one. RubyGems.org has no unpublish and no
214
+ duplicate-version overwrite, so this all happens while a bad artifact is still reversible.
215
+ The release run's job summary then re-fetches every gem from the CDN and records
216
+ attested-vs-served digests, turning "rubygems.org stores an upload verbatim" into a
217
+ per-release measurement rather than an assumption — see
218
+ [csharp/README.md's provenance section](../csharp/README.md#native-binary-provenance) for
219
+ more on why `--signer-repo` is needed for some artifacts here and not others.
220
+
221
+ ## Install
222
+
223
+ ```sh
224
+ gem install hyperuuid
225
+ ```
226
+
227
+ Nine gems are published per release. This section is about eight of them: seven precompiled Magnus platform gems that
228
+ `gem install` and `bundle` auto-select when they match — `x86_64-linux-gnu`,
229
+ `aarch64-linux-gnu`, `x86_64-linux-musl`, `aarch64-linux-musl`, `arm64-darwin`,
230
+ `x64-mingw-ucrt` and `aarch64-mingw-ucrt` — and one universal `ruby`-platform gem. A platform gem carries its
231
+ Magnus extensions and nothing else native: no Fiddle library at all. The universal gem is
232
+ the last resort, Fiddle with every platform's native library bundled, and it is what
233
+ RubyGems resolves for a Ruby the platform gems do not cover (3.3, or a Ruby newer than the
234
+ release, such as 4.1 before a release ships for it) and on a platform no platform gem is
235
+ built for — Intel macOS among them, which runs on Fiddle. No extra configuration needed either
236
+ way. The ninth, `hyperuuid-wasm`, is only for ruby.wasm; see [Ruby in the browser](#ruby-in-the-browser).
237
+
238
+ Selection has **two** axes here, unlike every other binding in this repo. A Magnus extension
239
+ is bound to one Ruby minor ABI — there's no `abi3` equivalent to collapse the version axis the
240
+ way [the Python binding's](../python/) wheels do — so each platform gem is a "fat" gem
241
+ carrying one compiled extension per supported Ruby, under `lib/hyperuuid/<minor>/`, and picks
242
+ one at `require` time:
243
+
244
+ | Ruby | Linux (glibc and musl), Apple silicon macOS, Windows — x64 and arm64 | Gem installed |
245
+ | --- | --- | --- |
246
+ | a newer Ruby than the release covers (4.1+) | Fiddle | universal |
247
+ | 4.0 (primary) | Magnus, `BACKEND == :native` | platform |
248
+ | 3.4 (until its EOL 2028-03-31) | Magnus, `BACKEND == :native` | platform |
249
+ | 3.3 (the floor, until its EOL 2027-03-31) | Fiddle | universal |
250
+
251
+ What stands behind each cell: CI runs the whole suite through each Magnus extension on every
252
+ push — Ruby 3.4 and 4.0 on all seven platform-gem platforms, the two musl ones inside each
253
+ Ruby's own Alpine image — and the Fiddle suite on Ruby 4.0 on every one of them, plus on Ruby
254
+ 3.3 inside Alpine. Intel macOS has no CI leg: its library is cross-built and tested at the
255
+ core, and Ruby there runs the universal gem's Fiddle backend over it. Anywhere else — a platform with no native build at
256
+ all — the universal gem installs but `HyperUuid.available?` answers `false`, and the first
257
+ call raises `HyperUuid::NativePlatform::UnsupportedPlatformError`.
258
+
259
+ The platform gems declare `required_ruby_version >= 3.4, < 4.1` precisely so RubyGems
260
+ *declines* them outside that range and resolves the universal gem instead — a wrong-ABI
261
+ extension must never be installed in the first place. On Windows it would at least fail to
262
+ load cleanly (the extension imports `<arch>-ucrt-ruby<minor>.dll` by name —
263
+ `x64-ucrt-ruby400.dll` on x64, `aarch64-ucrt-ruby400.dll` on ARM), but Linux extensions
264
+ don't link libruby at all, so one can load successfully against the wrong ABI and misbehave
265
+ later. When 3.4 goes EOL it simply leaves the matrix and its users fall back to Fiddle, which
266
+ is exactly what the fallback is for.
267
+
268
+ **musl.** Alpine has platform gems of its own, `x86_64-linux-musl` and
269
+ `aarch64-linux-musl`, whose extensions are built inside each Ruby's official `ruby:*-alpine`
270
+ image and need nothing beyond musl's libc. The glibc gems name their libc too
271
+ (`x86_64-linux-gnu`, `aarch64-linux-gnu`), which is what makes both `gem install` and
272
+ Bundler pick the right one on every supported Ruby: next to a plain `x86_64-linux` gem,
273
+ RubyGems before 4.0 resolves that one on Alpine instead, even under
274
+ `--platform x86_64-linux-musl`.
275
+
276
+ **Nothing in this gem is ever compiled, on any platform.** The platform gems depend on nothing
277
+ at all. The universal gem depends on `fiddle`, which can be: `fiddle` is a bundled gem on Ruby
278
+ 4.0 and a default gem on 3.3 and 3.4, and `gem install` is satisfied by the copy Ruby ships.
279
+ Bundler resolves the newest `fiddle` on rubygems.org instead, and when that is newer than the
280
+ one your Ruby ships — true today on 3.3 and 3.4, not on 4.0 — it builds Fiddle's own C
281
+ extension, which takes a compiler and libffi's headers (`apk add build-base libffi-dev` on
282
+ Alpine). That only reaches you where the universal gem installs: Ruby 3.3, Intel macOS, a
283
+ Ruby newer than the release, or a platform with no platform gem. `bundle install
284
+ --prefer-local` makes Bundler use the copy Ruby ships where it can (it did on Ruby 3.4's
285
+ Bundler 2.6, not on 3.3's 2.5); otherwise install the compiler and headers. Pinning `fiddle`
286
+ to your Ruby's version in the `Gemfile.lock` does not stop the build.
287
+
288
+ Both Windows architectures get a Magnus gem. MinGW is the *only* Windows flavour `rb-sys`
289
+ targets (`x64-mingw-ucrt` and `aarch64-mingw-ucrt`, both `supported: true` in its own
290
+ `data/toolchains.json`); the one it has no support for is MSVC. Windows is also where the
291
+ Fiddle fallback costs the most: measured on win-x64, Ruby 3.4, the Magnus backend does
292
+ `new_v4` in 406ns against Fiddle's 2407ns (**5.9x**) and `new_v7` in 595ns against 2759ns
293
+ (**4.6x**) — a far wider gap than any Linux or macOS leg shows — and on real Windows-on-ARM
294
+ hardware, Ruby 4.0.6, `new_v4` in 416ns against Fiddle's 2299ns (**5.5x**) and `new_v7` in
295
+ 621ns against 2474ns (**4.0x**), the same shape. Both extensions are built for the `gnullvm`
296
+ Rust targets rather than `gnu` — the same mingw-w64/UCRT ABI RubyInstaller's Ruby uses,
297
+ linked with LLVM and compiler-rt instead of GCC and a statically-linked libgcc, which is what
298
+ keeps the shipped extension small. The build script, and the two flags that are load-bearing
299
+ on the ARM leg (a static libunwind, and a clang-spelled `--target` for bindgen), live in the
300
+ forge's `ruby-magnus` action (`build-magnus.sh`), shared with every other Hyper* repo.
301
+
302
+ ## Ruby in the browser
303
+
304
+ ruby.wasm cannot load an extension at runtime. `rbwasm build` cross-compiles the extension of
305
+ every gem in a Gemfile and links them all into the one interpreter it builds, so the browser
306
+ gets a gem of its own: `hyperuuid-wasm`, the same library and the same Magnus extension,
307
+ prebuilt for `wasm32-wasip1`. List it **instead of** `hyperuuid` in the Gemfile you build the
308
+ interpreter from (the two carry the same `lib/` files):
309
+
310
+ ```ruby
311
+ source "https://rubygems.org"
312
+
313
+ gem "hyperuuid-wasm"
314
+ gem "js" # JavaScript interop, which a browser app almost always wants
315
+
316
+ group :development do
317
+ gem "ruby_wasm"
318
+ end
319
+ ```
320
+
321
+ ```sh
322
+ bundle install
323
+ bundle exec rbwasm build --ruby-version 4.0 -o ruby.wasm
324
+ ```
325
+
326
+ Load `ruby.wasm` with [`@ruby/wasm-wasi`](https://www.npmjs.com/package/@ruby/wasm-wasi)
327
+ (`DefaultRubyVM` in a browser, `RubyVM.instantiateModule` under Node), then
328
+ `require "/bundle/setup"` and `require "hyperuuid"` as anywhere else. `HyperUuid::BACKEND` is
329
+ `:native`: every door, the batch and raw-bytes forms, and `new_v6`/`new_v7` with no argument,
330
+ which read the clock through WASI.
331
+
332
+ - **Ruby 3.4 and 4.0**, one archive each, the same minors the platform gems cover, picked by
333
+ `--ruby-version`. Any other minor stops the build with a message naming the ones the gem
334
+ carries. Built and tested against ruby_wasm 2.10.
335
+ - **No Rust toolchain.** The gem's `extconf.rb` only hands rbwasm the prebuilt archive, and
336
+ rbwasm downloads its own wasi-sdk. The first `rbwasm build` compiles Ruby itself and takes
337
+ 15–20 minutes; later builds reuse it.
338
+ - **Static linking only,** into ruby.wasm's default `wasm32-unknown-wasip1` interpreter.
339
+ rbwasm's dynamic-linking build (a pic target, for the component model) is not supported.
340
+ - **Size.** rbwasm packs every gem's files into the interpreter, the archives included, so the
341
+ gem adds about 2 MB to `ruby.wasm` (both archives are gzipped).
342
+ - **With HyperCast.** `hypercast-wasm` links into the same interpreter. Every Rust extension
343
+ that carries std defines a few of the same symbols (`rust_eh_personality`, which ruby.wasm's
344
+ own wasi-vfs defines too, rb-sys's `ruby_abi_version`, and one of std's), and both gems
345
+ rename them to names of their own when CI builds the archive, which also fails if a newer
346
+ Rust starts exporting another.
347
+
348
+ What stands behind it: CI builds each minor's archive from the commit, packs the gem the way
349
+ it ships, links it into a fresh interpreter and runs [`wasm-smoke/test.rb`](wasm-smoke/test.rb)
350
+ under Node and in headless Chrome (the forge's `hyper-build-wasm.yml`). The published gem is
351
+ packed around those attested archives.
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 backend finds the in-repo build on its 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 cdylib # libhyperuuid, what the Fiddle backend loads
364
+
365
+ cd ../ruby
366
+ bundle install
367
+ bundle exec rake native:dev # build the Magnus extension for this Ruby and stage it
368
+ bundle exec rspec # BACKEND == :native
369
+ HYPERUUID_PURE=1 bundle exec rspec # BACKEND == :fiddle
370
+ bundle exec rake docs:check # every public object carries a doc comment
371
+ ruby benchmark/uuid_benchmark.rb # prints the backend it measured
372
+ ```
373
+
374
+ `rake native:dev` runs `cargo ruby-ext` (an alias in `rust/.cargo/config.toml` that builds the
375
+ `ruby` feature into `rust/target/ruby/`, never over the plain library in
376
+ `rust/target/release/`) and copies the result to
377
+ `lib/hyperuuid/<minor>/hyperuuid_native.<so|bundle>` — the path `lib/hyperuuid.rb` tries
378
+ first. The copy is also a rename, and it is the step `cargo ruby-ext` alone does not do: cargo
379
+ names its output `libhyperuuid.so`, and Ruby derives the `Init_` function it calls from the
380
+ file name it was asked to `require`. Three things worth knowing:
381
+
382
+ - **Run it again after changing `rust/`.** The staged file is a copy; nothing rebuilds it.
383
+ - **One staging per Ruby.** An extension is bound to one Ruby minor, so under a second Ruby
384
+ (`RBENV_VERSION=3.4.11 bundle exec rake native:dev`, say) the task rebuilds for that ABI
385
+ and stages beside the first, each Ruby loading its own.
386
+ - **Without it, `bundle exec rspec` still passes — on Fiddle.** The examples under "native
387
+ backend" report themselves pending with `BACKEND=fiddle`; that line, or
388
+ `ruby -Ilib -rhyperuuid -e 'p HyperUuid::BACKEND'`, is how to tell which backend a run
389
+ exercised. Deleting `lib/hyperuuid/<minor>/` goes back to Fiddle.
390
+
391
+ The task covers Linux and macOS. The Windows extensions need the `gnullvm` targets and linker
392
+ flags in the forge's `build-magnus.sh`, which is also what CI uses on every platform.
393
+
394
+ The musl build has no host to run on outside a container. With the core built for musl at
395
+ `rust/target/musl/linux-musl-x64/libhyperuuid.so`, this runs the Fiddle suite against it on
396
+ Alpine, with the checkout mounted read-only:
397
+
398
+ ```sh
399
+ docker run --rm -v "$PWD/..":/src:ro ruby:4.0-alpine sh -euc '
400
+ mkdir /work && cp -r /src/ruby /work/ruby
401
+ mkdir -p /work/ruby/lib/hyperuuid/native/linux-musl-x64
402
+ cp /src/rust/target/musl/linux-musl-x64/libhyperuuid.so /work/ruby/lib/hyperuuid/native/linux-musl-x64/
403
+ cd /work/ruby && rm -f Gemfile.lock
404
+ bundle install --quiet --prefer-local
405
+ HYPERUUID_PURE=1 bundle exec rspec'
406
+ ```
407
+
408
+ The floor's test is the same container on `ruby:3.3-alpine`. Bundler would compile a newer
409
+ Fiddle there (see [Install](#install)), so this one goes around Bundler and runs against the
410
+ Fiddle that Ruby 3.3 ships:
411
+
412
+ ```sh
413
+ docker run --rm -v "$PWD/..":/src:ro ruby:3.3-alpine sh -euc '
414
+ mkdir /work && cp -r /src/ruby /work/ruby
415
+ mkdir -p /work/ruby/lib/hyperuuid/native/linux-musl-x64
416
+ cp /src/rust/target/musl/linux-musl-x64/libhyperuuid.so /work/ruby/lib/hyperuuid/native/linux-musl-x64/
417
+ cd /work/ruby && rm -f Gemfile Gemfile.lock
418
+ gem install rspec -v "~> 3.13" --no-document --silent
419
+ HYPERUUID_PURE=1 rspec'
420
+ ```
421
+
422
+ The musl Magnus extension is built the way CI builds it by the forge's
423
+ `ruby-magnus-musl/build-magnus-musl.sh`, which runs on any machine with Docker: from the repo
424
+ root, `build-magnus-musl.sh hyperuuid linux-musl-x64 4.0 . <dir with the musl libhyperuuid.so> <out-dir>`
425
+ compiles it in `ruby:4.0-alpine`, then runs this suite through it on a bare copy of that image.
426
+
427
+ See [the repo root README](../README.md) for the full RFC 9562 coverage table and the state of every other language binding.
428
+
429
+ ## License
430
+
431
+ [MIT](https://github.com/SkunkWerkx/HyperUuid/blob/master/LICENSE)
@@ -0,0 +1,107 @@
1
+ # The hyperuuid-wasm gem's extension: the Magnus extension, linked statically into a ruby.wasm
2
+ # interpreter by `rbwasm build`. ruby.wasm cannot load an extension at runtime, so a consumer
3
+ # puts this gem in the Gemfile they hand to rbwasm and gets one interpreter with HyperUuid
4
+ # compiled in. Only this companion gem ships this file; the hyperuuid gems do not, because an
5
+ # `extensions` entry there would run extconf on every host `gem install`.
6
+ #
7
+ # Bundler installs the gem on the host first, where there is nothing to build. Then rbwasm runs
8
+ # this again with the cross-compiled Ruby's RbConfig, `make clean`, `make static`, and links
9
+ # every *.a left in the build directory. Both paths below end in the same place: the archive
10
+ # saved as hyperuuid_native.prebuilt (not *.a, which `make clean` removes), copied into place
11
+ # by `make static`.
12
+ #
13
+ # * Prebuilt, what a consumer gets: the gem carries one gzipped archive per Ruby minor (Rakefile,
14
+ # wasm:gem), built and attested in CI. No Rust toolchain on the consumer's machine.
15
+ # * From source, how CI makes those archives: HYPERUUID_RUST_DIR names the crate, and
16
+ # HYPERUUID_WASM_OUT, if set, receives a copy of the finished archive.
17
+ require "mkmf"
18
+ require "rbconfig"
19
+ require "zlib"
20
+ require "fileutils"
21
+
22
+ unless RbConfig::CONFIG["host_os"].include?("wasi")
23
+ File.write("Makefile", dummy_makefile($srcdir).join)
24
+ exit
25
+ end
26
+
27
+ # rbwasm reads $target back out to name the extension's Init function in its extinit table.
28
+ $target = "hyperuuid_native"
29
+ minor = "#{RbConfig::CONFIG["MAJOR"]}.#{RbConfig::CONFIG["MINOR"]}"
30
+ prebuilt = "hyperuuid_native.prebuilt"
31
+
32
+ if (rust_dir = ENV["HYPERUUID_RUST_DIR"])
33
+ # rb-sys binds against the target Ruby through RBCONFIG_* variables. Ruby is not installed
34
+ # yet when rbwasm builds extensions, so the two header directories point at the source and
35
+ # build trees instead (rbwasm sets top_srcdir and extout for exactly this).
36
+ RbConfig::CONFIG.merge(
37
+ "rubyhdrdir" => File.join(ENV.fetch("top_srcdir"), "include"),
38
+ "rubyarchhdrdir" => File.join(ENV.fetch("extout"), "include", RbConfig::CONFIG["arch"])
39
+ ).each { |key, value| ENV["RBCONFIG_#{key}"] = value.to_s }
40
+ wasi_sdk = File.dirname(File.dirname(RbConfig::CONFIG["CC"].split.first))
41
+ ENV["BINDGEN_EXTRA_CLANG_ARGS"] = "--sysroot=#{wasi_sdk}/share/wasi-sysroot " \
42
+ "-D_WASI_EMULATED_SIGNAL -D_WASI_EMULATED_PROCESS_CLOCKS -D_WASI_EMULATED_MMAN"
43
+ ENV["CC_wasm32_wasip1"] = "#{wasi_sdk}/bin/clang"
44
+ target_dir = File.expand_path("target")
45
+ system("cargo", "rustc", "--manifest-path", File.join(rust_dir, "Cargo.toml"), "--release",
46
+ "--target", "wasm32-wasip1", "--crate-type", "staticlib", "--features", "ruby",
47
+ "--target-dir", target_dir, exception: true)
48
+ archive = File.binread(File.join(target_dir, "wasm32-wasip1/release/libhyperuuid.a"))
49
+ # The crate links into one object (release LTO), and besides Init_hyperuuid_native and the
50
+ # C ABI it exports three symbols that every Rust extension carrying std defines too:
51
+ # rust_eh_personality, which wasi-vfs (linked into every ruby.wasm) also defines;
52
+ # ruby_abi_version, rb-sys's ABI stamp for a dynamically loaded extension, which a static
53
+ # one never needs; and std's EMPTY_PANIC. Two definitions of any of them stop the link, so
54
+ # each is renamed to one this crate owns, and the HyperCast extension, renamed the same way,
55
+ # can share the interpreter. wasm llvm-objcopy can neither localize nor rename a symbol, so
56
+ # the names are rewritten in place at the same length, which leaves every offset in the
57
+ # archive and its objects valid (and keeps EMPTY_PANIC's v0 mangling well-formed). Nothing
58
+ # calls the first two: a static extension's ABI stamp is never looked up, and wasm32-wasip1
59
+ # builds with panic=abort.
60
+ {
61
+ "rust_eh_personality" => "hyperuuid_eh_person",
62
+ "ruby_abi_version" => "hyperuuid_abiver",
63
+ "9panicking11EMPTY_PANIC" => "9panicking11HUUID_PANIC"
64
+ }.each { |from, to| archive.gsub!(from, to) }
65
+ File.binwrite(prebuilt, archive)
66
+ system("#{wasi_sdk}/bin/llvm-ranlib", prebuilt, exception: true)
67
+ # And a newer Rust that exports another of std's symbols fails here, by name, rather than in
68
+ # the link of an interpreter that also carries the HyperCast extension.
69
+ member = nil
70
+ exported = IO.popen(["#{wasi_sdk}/bin/llvm-nm", "--defined-only", "--extern-only", prebuilt], err: File::NULL, &:read)
71
+ .each_line(chomp: true).filter_map do |line|
72
+ if line.end_with?(".o:")
73
+ member = line
74
+ next
75
+ end
76
+ line.split.last if member&.start_with?("hyperuuid-") && line.match?(/\A\h+ [A-Z] /)
77
+ end
78
+ shared = exported.grep(/\A(_R|_ZN|rust_|__rust|ruby_abi_version\z)/).grep_v(/HUUID_PANIC\z/)
79
+ shared.empty? or abort "hyperuuid-wasm: the extension exports #{shared.join(", ")}, which " \
80
+ "another Rust extension in the same interpreter would define too"
81
+ if (out = ENV["HYPERUUID_WASM_OUT"])
82
+ FileUtils.mkdir_p(File.join(out, minor))
83
+ FileUtils.cp(prebuilt, File.join(out, minor, "hyperuuid_native.a"))
84
+ end
85
+ else
86
+ gz = File.join(__dir__, minor, "hyperuuid_native.a.gz")
87
+ unless File.exist?(gz)
88
+ shipped = Dir[File.join(__dir__, "*", "hyperuuid_native.a.gz")].map { |f| File.basename(File.dirname(f)) }
89
+ abort "hyperuuid-wasm: no prebuilt extension for Ruby #{minor} (this gem carries #{shipped.sort.join(", ")})"
90
+ end
91
+ File.binwrite(prebuilt, Zlib::GzipReader.open(gz, &:read))
92
+ end
93
+
94
+ # install-so is rbwasm's dynamic-linking path (a pic target, for the component model), which
95
+ # a static archive cannot serve.
96
+ File.write("Makefile", <<~MAKE)
97
+ all: static
98
+ static:
99
+ \tcp #{prebuilt} hyperuuid_native.a
100
+ install-so:
101
+ \t@echo "hyperuuid-wasm links statically: build a wasm32-unknown-wasip1 interpreter" >&2; exit 1
102
+ install-rb:
103
+ \t@true
104
+ clean:
105
+ \trm -f hyperuuid_native.a
106
+ .PHONY: all static install-so install-rb clean
107
+ MAKE
@@ -0,0 +1,12 @@
1
+ module HyperUuid
2
+ # Raised when the operating system's random source fails while a UUID is being minted —
3
+ # the one way `new_v4`, `new_v6`, `new_v7` and their batch forms can fail once their
4
+ # arguments are valid. Carries the same message on every backend.
5
+ class RandomSourceError < StandardError; end
6
+
7
+ # Raised when a timestamp cannot be embedded in the UUID being minted: a version 7 value
8
+ # past RFC 9562's 48-bit millisecond field, a version 6 value past its 60-bit field, or
9
+ # any negative one (a `Time` before the Unix epoch included). Carries the same message on
10
+ # every backend.
11
+ class TimestampOutOfRangeError < StandardError; end
12
+ end
@@ -0,0 +1,13 @@
1
+ module HyperUuid
2
+ # Well-known namespace UUIDs defined in RFC 9562 Section 6.6.
3
+ module Namespaces
4
+ # The DNS namespace UUID.
5
+ DNS = Uuid.parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
6
+ # The URL namespace UUID.
7
+ URL = Uuid.parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")
8
+ # The ISO OID namespace UUID.
9
+ OID = Uuid.parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")
10
+ # The X.500 DN namespace UUID.
11
+ X500 = Uuid.parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ module HyperUuid
2
+ # Maps the running RUBY_PLATFORM to the RID-style directory (matching the other bindings'
3
+ # runtimes/{rid}/native/ / native/{rid}/ convention) and native library filename to load.
4
+ module NativePlatform
5
+ class UnsupportedPlatformError < StandardError; end
6
+
7
+ # +platform+ is a parameter only so the specs can walk the whole table from one host;
8
+ # every real caller takes the default.
9
+ def self.rid_and_library_name(platform = RUBY_PLATFORM)
10
+ arch = platform.match?(/arm64|aarch64/) ? "arm64" : "x64"
11
+
12
+ case platform
13
+ when /mingw|mswin|windows/
14
+ ["win-#{arch}", "hyperuuid.dll"]
15
+ when /darwin/
16
+ ["osx-#{arch}", "libhyperuuid.dylib"]
17
+ when /linux/
18
+ # Two C libraries, two builds: a glibc-linked library cannot be relied on to dlopen
19
+ # into a musl process (Alpine), so musl gets RIDs of its own. Ruby names the libc
20
+ # in its platform string there ("x86_64-linux-musl") and leaves it off on glibc.
21
+ os = platform.include?("musl") ? "linux-musl" : "linux"
22
+ ["#{os}-#{arch}", "libhyperuuid.so"]
23
+ else
24
+ raise UnsupportedPlatformError, "hyperuuid: unsupported platform RUBY_PLATFORM=#{platform}"
25
+ end
26
+ end
27
+ end
28
+ end