hyperuuid 0.4.0 → 0.6.0

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: 81bb32234121786655a840eb06f6255dee1a7263cbb51954f1a094b1432a02f0
4
- data.tar.gz: de9b7f6914d7fdc2321450e3dbe2f0e4b0703af044bdff728e1c6435a9ddc2d3
3
+ metadata.gz: 9469a1d25a10c66127020801f9773f8dc64325fb3e9567c76ab50d3182641ca9
4
+ data.tar.gz: bf59070f36ce9d2fef96080649dcd0392a01d3e0b18b6311804d2839ed465bfc
5
5
  SHA512:
6
- metadata.gz: 6b43d3d7cb67c46ada26791f2ef63f24f6d58fcebe60275d9bc16362a2994312a81c7a9a7b37fa91a94254042f2e58e7b8cd6b192af60162dc9f6e7c485cbafc
7
- data.tar.gz: 2b6929f9893c0d8a0cb6403cb724c3aed3d72ec415048b465a361d6236a1e0f93c17a04180b7ee5f422244783568f5d842155b56e324a0d091e327562a2dcc15
6
+ metadata.gz: 978ee6cf29cb949cf22dfca9a02ec2c3c129a66a4a7b9db461e06028d948246eef50ad5d3e326b53ab4dc888836f147b76d9391717c3660369751cc1afd7bdb5
7
+ data.tar.gz: 7606abc5a1ec909559dd1cefc0dedf2272c0bdd46bd113210123cf543d92523668589baf4bf297c302a4b0d56a0dd4c6ccf31f7de687de5192b36f60e6482a78
data/README.md CHANGED
@@ -7,17 +7,15 @@
7
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
8
 
9
9
  RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation, with
10
- three backends sharing one public surface. Ruby 3.3 is the floor. The fast path is a native
10
+ two backends sharing one public surface. Ruby 3.3 is the floor. The fast path is a native
11
11
  extension built with [Magnus](https://github.com/matsadler/magnus) — the Rust core linked
12
12
  directly into the Ruby VM, auto-selected when loadable — which redefines the low-level
13
13
  `Runtime` methods in place on require; everything above them (`Uuid`, the module doors and
14
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).
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).
21
19
 
22
20
  ```ruby
23
21
  require "hyperuuid"
@@ -114,7 +112,7 @@ looked for, or `HyperUuid::NativePlatform::UnsupportedPlatformError` naming the
114
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.
115
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.
116
114
 
117
- 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.
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.
118
116
 
119
117
  ## Bulk generation into bytes
120
118
 
@@ -135,7 +133,7 @@ Slice it with `bytes[i * 16, 16]` — which is exactly what `new_v7_batch` does
135
133
 
136
134
  `ruby benchmark/uuid_benchmark.rb` reproduces every table here and the bytes-versus-objects
137
135
  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.
136
+ run it again under `HYPERUUID_PURE=1` for the other backend.
139
137
 
140
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):
141
139
 
@@ -170,86 +168,33 @@ The multiplier is small on this backend for the best reason available: the indiv
170
168
 
171
169
  | `HyperUuid::BACKEND` | What runs | Chosen when |
172
170
  |---|---|---|
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) |
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) |
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 |
176
173
 
177
- Selection happens once, at `require`, in that order. Two environment variables override it:
174
+ Selection happens once, at `require`, in that order.
178
175
 
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`.
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.
188
185
 
189
186
  **Threads.** Every backend is safe to call from any number of threads. The extension runs
190
187
  under the GVL. Fiddle releases the GVL for the duration of each call, so that is the one
191
188
  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.
189
+ buffer, with the core's v7 counter shared and atomic. `spec/hyperuuid_spec.rb`'s "concurrent
190
+ callers" examples run under both.
195
191
 
196
192
  **Ractors.** Main Ractor only, on every backend: called from another Ractor the doors raise
197
- `Ractor::UnsafeError` (`Ractor::IsolationError` under wasm).
198
-
199
- ## WebAssembly (wasmtime)
200
-
201
- The Rust core also ships inside this gem as a `wasm32-wasip1` module
202
- (`lib/hyperuuid/native/wasm32-wasip1/hyperuuid.wasm`), and the
203
- [`wasmtime`](https://rubygems.org/gems/wasmtime) gem can run it in-process. This is the
204
- inverse of ruby.wasm — not Ruby inside a wasm sandbox, but a wasm module inside Ruby — and
205
- it is the one backend that needs no shared library for the platform it runs on: no
206
- `dlopen`, no Magnus extension, nothing compiled against this Ruby's ABI. Everything above
207
- `Runtime` (`Uuid`, the module doors, batch slicing) is the same code the other two backends
208
- run, and `spec/wasm_backend_spec.rb` pins that the outputs agree with the Fiddle backend
209
- byte for byte.
210
-
211
- `wasmtime` is deliberately **not** a dependency of this gem; a consumer who wants this path
212
- installs it:
213
-
214
- ```sh
215
- gem install wasmtime
216
- HYPERUUID_WASM=1 ruby -rhyperuuid -e 'p HyperUuid::BACKEND' # => :wasm
217
- ```
218
-
219
- `HYPERUUID_WASM=1` forces the backend (and raises a `LoadError` naming the gem if it is
220
- missing). Without it, the wasm backend is only ever chosen automatically when there is no
221
- native library for this platform at all — no Magnus extension and no `libhyperuuid` for the
222
- RID — and `wasmtime` happens to be installed. No supported platform's behavior changes just
223
- because this backend exists.
224
-
225
- Two things are different under the sandbox, both by necessity. A wasm guest only sees its own
226
- linear memory, so every buffer the core fills comes from the module's own exported `malloc`
227
- (the same wasi-libc allocator Rust's std uses on that target) and is read back with
228
- `Memory#read` — using the guest's allocator rather than a host-picked offset is what keeps a
229
- batch from being clobbered by the guest's next allocation. And a `Wasmtime::Store` is
230
- single-threaded, so every call is serialized under one Mutex around one shared instance,
231
- which is also what keeps the core's v7 counter (it lives inside the instance) monotonic
232
- across threads and batches, exactly as the one dlopen'd library does natively.
233
-
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:
236
-
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 |
243
-
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.
193
+ `Ractor::UnsafeError`.
249
194
 
250
195
  ## Verifying provenance
251
196
 
252
- Every gem RubyGems.org serves — the universal fallback and each of the five precompiled
197
+ Every gem RubyGems.org serves — the universal fallback and each of the seven precompiled
253
198
  platform gems — carries its own GitHub build-provenance attestation, signed directly by
254
199
  this repo's own `release.yml` (the `rubygems-publish` job attests `ruby/pkg/*.gem` right
255
200
  before the push), so plain `--repo` verifies any of them:
@@ -261,7 +206,7 @@ gh attestation verify hyperuuid-X.Y.Z-<platform>.gem --repo SkunkWerkx/HyperUuid
261
206
 
262
207
  That's the release's second layer of checking, not the only one: before any gem gets built,
263
208
  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*
209
+ extensions (one per Ruby ABI per platform) — against *their own*
265
210
  attestations — those are signed from `SkunkWerkx/.github` by `hyper-build-native.yml`, so
266
211
  that check needs `--signer-repo SkunkWerkx/.github` added — and refuses to proceed on an
267
212
  unverified one. RubyGems.org has no unpublish and no
@@ -278,15 +223,16 @@ more on why `--signer-repo` is needed for some artifacts here and not others.
278
223
  gem install hyperuuid
279
224
  ```
280
225
 
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.
226
+ Eight gems are published per release: seven precompiled Magnus platform gems that
227
+ `gem install` and `bundle` auto-select when they match — `x86_64-linux-gnu`,
228
+ `aarch64-linux-gnu`, `x86_64-linux-musl`, `aarch64-linux-musl`, `arm64-darwin`,
229
+ `x64-mingw-ucrt` and `aarch64-mingw-ucrt` — and one universal `ruby`-platform gem. A platform gem carries its
230
+ Magnus extensions and nothing else native: no Fiddle library at all. The universal gem is
231
+ the last resort, Fiddle with every platform's native library bundled, and it is what
232
+ RubyGems resolves for a Ruby the platform gems do not cover (3.3, or a Ruby newer than the
233
+ release, such as 4.1 before a release ships for it) and on a platform no platform gem is
234
+ built for — Intel macOS among them, which runs on Fiddle. No extra configuration needed either
235
+ way.
290
236
 
291
237
  Selection has **two** axes here, unlike every other binding in this repo. A Magnus extension
292
238
  is bound to one Ruby minor ABI — there's no `abi3` equivalent to collapse the version axis the
@@ -294,20 +240,20 @@ way [the Python binding's](../python/) wheels do — so each platform gem is a "
294
240
  carrying one compiled extension per supported Ruby, under `lib/hyperuuid/<minor>/`, and picks
295
241
  one at `require` time:
296
242
 
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.
243
+ | Ruby | Linux (glibc and musl), Apple silicon macOS, Windows — x64 and arm64 | Gem installed |
244
+ | --- | --- | --- |
245
+ | a newer Ruby than the release covers (4.1+) | Fiddle | universal |
246
+ | 4.0 (primary) | Magnus, `BACKEND == :native` | platform |
247
+ | 3.4 (until its EOL 2028-03-31) | Magnus, `BACKEND == :native` | platform |
248
+ | 3.3 (the floor, until its EOL 2027-03-31) | Fiddle | universal |
249
+
250
+ What stands behind each cell: CI runs the whole suite through each Magnus extension on every
251
+ push — Ruby 3.4 and 4.0 on all seven platform-gem platforms, the two musl ones inside each
252
+ Ruby's own Alpine image — and the Fiddle suite on Ruby 4.0 on every one of them, plus on Ruby
253
+ 3.3 inside Alpine. Intel macOS has no CI leg: its library is cross-built and tested at the
254
+ core, and Ruby there runs the universal gem's Fiddle backend over it. Anywhere else — a platform with no native build at
255
+ all — the universal gem installs but `HyperUuid.available?` answers `false`, and the first
256
+ call raises `HyperUuid::NativePlatform::UnsupportedPlatformError`.
311
257
 
312
258
  The platform gems declare `required_ruby_version >= 3.4, < 4.1` precisely so RubyGems
313
259
  *declines* them outside that range and resolves the universal gem instead — a wrong-ABI
@@ -318,15 +264,13 @@ don't link libruby at all, so one can load successfully against the wrong ABI an
318
264
  later. When 3.4 goes EOL it simply leaves the matrix and its users fall back to Fiddle, which
319
265
  is exactly what the fallback is for.
320
266
 
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`.
267
+ **musl.** Alpine has platform gems of its own, `x86_64-linux-musl` and
268
+ `aarch64-linux-musl`, whose extensions are built inside each Ruby's official `ruby:*-alpine`
269
+ image and need nothing beyond musl's libc. The glibc gems name their libc too
270
+ (`x86_64-linux-gnu`, `aarch64-linux-gnu`), which is what makes both `gem install` and
271
+ Bundler pick the right one on every supported Ruby: next to a plain `x86_64-linux` gem,
272
+ RubyGems before 4.0 resolves that one on Alpine instead, even under
273
+ `--platform x86_64-linux-musl`.
330
274
 
331
275
  **Nothing in this gem is ever compiled, on any platform.** Its one dependency can be:
332
276
  `fiddle` is a bundled gem on Ruby 4.0 and a default gem on 3.3 and 3.4, and `gem install` is
@@ -353,22 +297,20 @@ forge's `ruby-magnus` action (`build-magnus.sh`), shared with every other Hyper*
353
297
  ## Development
354
298
 
355
299
  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
300
+ use the gem. The Fiddle backend finds the in-repo build on its own when nothing
357
301
  is staged under `lib/hyperuuid/native/`. The Magnus backend does not — an extension has to be
358
302
  built for the Ruby you are running and put where `require` looks — and that is what
359
303
  `rake native:dev` is for.
360
304
 
361
305
  ```sh
362
306
  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
307
+ cargo cdylib # libhyperuuid, what the Fiddle backend loads
365
308
 
366
309
  cd ../ruby
367
310
  bundle install
368
311
  bundle exec rake native:dev # build the Magnus extension for this Ruby and stage it
369
312
  bundle exec rspec # BACKEND == :native
370
313
  HYPERUUID_PURE=1 bundle exec rspec # BACKEND == :fiddle
371
- HYPERUUID_WASM=1 bundle exec rspec # BACKEND == :wasm
372
314
  bundle exec rake docs:check # every public object carries a doc comment
373
315
  ruby benchmark/uuid_benchmark.rb # prints the backend it measured
374
316
  ```
@@ -403,8 +345,8 @@ docker run --rm -v "$PWD/..":/src:ro ruby:4.0-alpine sh -euc '
403
345
  mkdir -p /work/ruby/lib/hyperuuid/native/linux-musl-x64
404
346
  cp /src/rust/target/musl/linux-musl-x64/libhyperuuid.so /work/ruby/lib/hyperuuid/native/linux-musl-x64/
405
347
  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'
348
+ bundle install --quiet --prefer-local
349
+ HYPERUUID_PURE=1 bundle exec rspec'
408
350
  ```
409
351
 
410
352
  The floor's test is the same container on `ruby:3.3-alpine`. Bundler would compile a newer
@@ -421,6 +363,11 @@ docker run --rm -v "$PWD/..":/src:ro ruby:3.3-alpine sh -euc '
421
363
  HYPERUUID_PURE=1 rspec'
422
364
  ```
423
365
 
366
+ The musl Magnus extension is built the way CI builds it by the forge's
367
+ `ruby-magnus-musl/build-magnus-musl.sh`, which runs on any machine with Docker: from the repo
368
+ root, `build-magnus-musl.sh hyperuuid linux-musl-x64 4.0 . <dir with the musl libhyperuuid.so> <out-dir>`
369
+ compiles it in `ruby:4.0-alpine`, then runs this suite through it on a bare copy of that image.
370
+
424
371
  See [the repo root README](../README.md) for the full RFC 9562 coverage table and the state of every other language binding.
425
372
 
426
373
  ## License
@@ -2,15 +2,13 @@ require "fiddle"
2
2
 
3
3
  module HyperUuid
4
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.
5
+ # C-ABI call, no runtime bridge (the same "no shim" positioning as the Swift binding's
6
+ # dlopen approach). Fiddle ships with every Ruby install; it's a plain gem dependency here
7
+ # (see hyperuuid.gemspec) rather than a third-party one — mirroring Python's
8
+ # zero-dependency PyO3 wheels.
9
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.
10
+ # A Ruby gem's files are plain files on disk once installed, so native/{rid}/{lib} can be
11
+ # dlopen'd directly, with no extraction step.
14
12
  module Runtime
15
13
  # The package's two exceptions live on HyperUuid itself (errors.rb). These are the names
16
14
  # they had through 0.3.0, kept as aliases of the very same classes so a
@@ -19,7 +17,7 @@ module HyperUuid
19
17
  RandomSourceError = HyperUuid::RandomSourceError
20
18
  TimestampOutOfRangeError = HyperUuid::TimestampOutOfRangeError
21
19
 
22
- # One copy of each out-of-range message for the Fiddle and wasm backends and for the
20
+ # One copy of each out-of-range message for the Fiddle backend and for the
23
21
  # doors' own range check in hyperuuid.rb; the Magnus extension (rust/src/ruby_ext.rs)
24
22
  # carries the same text, and spec/native_backend_spec.rb pins that the two agree.
25
23
  V6_TIMESTAMP_OUT_OF_RANGE = "unix_millis does not fit the 60-bit v6 timestamp field"
@@ -128,16 +126,6 @@ module HyperUuid
128
126
  RandomSourceError.new("#{export}: the system random source failed")
129
127
  end
130
128
 
131
- # Whether this platform has a shared library for Fiddle to dlopen at all: a known RID
132
- # and the file actually present — in this install, or in the in-repo cargo build the
133
- # dev loop falls back to. Backend selection (hyperuuid.rb) asks this before falling
134
- # back to the WebAssembly backend, which needs neither.
135
- def fiddle_library_available?
136
- !library_path.nil?
137
- rescue NativePlatform::UnsupportedPlatformError
138
- false
139
- end
140
-
141
129
  private
142
130
 
143
131
  # The shared library to dlopen: this install's native/{rid}/{lib}, or — the
@@ -153,6 +141,35 @@ module HyperUuid
153
141
  File.exist?(repo_build) ? repo_build : nil
154
142
  end
155
143
 
144
+ # Why Fiddle found nothing to load. A precompiled platform gem is the one install where
145
+ # that is by design rather than a gap: it carries only its Magnus extensions, and the
146
+ # Fiddle backend is reached there only by forcing it (HYPERUUID_PURE) or because none
147
+ # of its extensions loaded — a gem RubyGems matched to a Ruby it was not built for,
148
+ # such as the glibc Linux gem that `gem install` on RubyGems 3.x picks on Alpine. So
149
+ # that case names its fix, the universal gem, which carries every platform's library,
150
+ # instead of a missing path that reads like a packaging bug. Both arguments are
151
+ # parameters only so the specs can ask for every wording.
152
+ def missing_library_message(gem_platform = Gem.loaded_specs["hyperuuid"]&.platform,
153
+ forced = ENV.key?("HYPERUUID_PURE"))
154
+ rid, lib_name = NativePlatform.rid_and_library_name
155
+ missing = File.join(NATIVE_DIR, rid, lib_name)
156
+ if gem_platform && gem_platform.to_s != Gem::Platform::RUBY
157
+ reason =
158
+ if forced
159
+ "HYPERUUID_PURE forces the Fiddle backend (unset it to use the extension)"
160
+ else
161
+ "none of its extensions loads on this Ruby (#{RUBY_VERSION}, #{RUBY_PLATFORM})"
162
+ end
163
+ "hyperuuid: this #{gem_platform} platform gem carries only Magnus extensions, no " \
164
+ "Fiddle library, and #{reason}. The universal gem has the Fiddle backend for every " \
165
+ "platform: `gem install hyperuuid --platform ruby`, or Bundler's force_ruby_platform " \
166
+ "(#{missing} not found)"
167
+ else
168
+ "hyperuuid: #{missing} not found (unsupported platform, or this gem was built " \
169
+ "without a native library for it)"
170
+ end
171
+ end
172
+
156
173
  # One 16-byte scratch allocation per thread, reused by every single-item call —
157
174
  # Fiddle::Pointer.malloc(..., RUBY_FREE) registers a GC finalizer per call, measured
158
175
  # (in HyperCast, same mechanism) as the dominant per-call cost by an order of
@@ -170,7 +187,7 @@ module HyperUuid
170
187
  buf[0, 16]
171
188
  end
172
189
 
173
- # Loaded lazily and exactly once, mirroring the Go binding's sync.Once / Swift's lazy
190
+ # Loaded lazily and exactly once, mirroring Java's class initializer / Swift's lazy
174
191
  # static let — the native library and its function pointers live for the process's
175
192
  # lifetime, same as every other binding (never dlclose'd). The unsynchronized read is
176
193
  # the hot path; the mutex only guards the one-time load (a benign race — idempotent).
@@ -180,12 +197,7 @@ module HyperUuid
180
197
 
181
198
  def load_functions
182
199
  path = library_path
183
- if path.nil?
184
- rid, lib_name = NativePlatform.rid_and_library_name
185
- raise LoadError,
186
- "hyperuuid: #{File.join(NATIVE_DIR, rid, lib_name)} not found (unsupported " \
187
- "platform, or this gem was built without a native library for it)"
188
- end
200
+ raise LoadError, missing_library_message if path.nil?
189
201
 
190
202
  handle = Fiddle.dlopen(path)
191
203
  {
@@ -1,7 +1,7 @@
1
1
  module HyperUuid
2
2
  # A parsed 16-byte RFC 9562 UUID value. Minimal by design — this gem has no runtime
3
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
4
+ # binding's lone google/uuid requirement and the Python binding's dependency-free PyO3
5
5
  # wheels.
6
6
  class Uuid
7
7
  include Comparable
data/lib/hyperuuid.rb CHANGED
@@ -5,12 +5,11 @@ require_relative "hyperuuid/native_platform"
5
5
  require_relative "hyperuuid/runtime"
6
6
 
7
7
  # RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation from one
8
- # Rust core, reached through whichever of three backends this install can load: the core
8
+ # Rust core, reached through whichever of two backends this install can load: the core
9
9
  # linked straight into a Magnus native extension (shipped precompiled in the platform gems),
10
- # the native libhyperuuid shared library called through Fiddle (the universal gem bundles
11
- # one per supported platform, picked by HyperUuid::NativePlatform), or the same core as a
12
- # WebAssembly module inside the wasmtime gem. No runtime bridge in any of them, and nothing
13
- # compiled at install time. HyperUuid::BACKEND names the one that loaded; the selection logic
10
+ # or the native libhyperuuid shared library called through Fiddle (the universal gem bundles
11
+ # one per supported platform, picked by HyperUuid::NativePlatform). No runtime bridge in
12
+ # either, and nothing compiled at install time. HyperUuid::BACKEND names the one that loaded; the selection logic
14
13
  # is at the bottom of this file.
15
14
  #
16
15
  # The doors below — with Uuid, Namespaces and the two exception classes — are the public
@@ -19,7 +18,7 @@ require_relative "hyperuuid/runtime"
19
18
  module HyperUuid
20
19
  # This gem's own version — distinct from the RFC 9562 UUID *versions* (v4/v5/v6/v7) the
21
20
  # rest of this module generates.
22
- VERSION = "0.4.0"
21
+ VERSION = "0.6.0"
23
22
 
24
23
  # The widest batch count, and the longest v5 name in bytes, the native ABI carries (a u32).
25
24
  # (The millisecond count is a u64; unix_millis_from refuses anything wider.)
@@ -62,7 +61,7 @@ module HyperUuid
62
61
  # before the epoch included) or past 64 bits — is the same TimestampOutOfRangeError, with
63
62
  # the same +out_of_range+ message, the core raises for a value it can see but not embed.
64
63
  # Left to the backends those cases diverged: a RangeError from the extension, a wrapped
65
- # value from Fiddle, a silently truncated one from wasm.
64
+ # value from Fiddle.
66
65
  #
67
66
  # The two common arguments go first and cheapest: no argument is the clock, which needs no
68
67
  # range check, and an Integer is checked by its bit length rather than against 2**64 - 1: a
@@ -185,11 +184,10 @@ module HyperUuid
185
184
  # Whether a backend actually loaded and exports the ABI this binding was built against —
186
185
  # what a consumer with a fallback of its own (SecureRandom.uuid, say) checks before
187
186
  # committing to these doors. Probed once (a native_version round trip), cached, and never
188
- # raises: a missing shared library, an unsupported platform, an older core without the
189
- # version export, or a wasm module the wasmtime gem cannot instantiate all answer false.
190
- # The doors themselves keep their own behavior — the first call on an unavailable backend
191
- # raises its precise error — and so does require-time selection under HYPERUUID_WASM=1
192
- # without wasmtime; this only answers the question quietly.
187
+ # raises: a missing shared library, an unsupported platform, or an older core without the
188
+ # version export all answer false. The doors themselves keep their own behavior — the
189
+ # first call on an unavailable backend raises its precise error — this only answers the
190
+ # question quietly.
193
191
  def self.available?
194
192
  return @available unless @available.nil?
195
193
 
@@ -205,31 +203,18 @@ end
205
203
  # above in place (no delegation layer) — Fiddle's measured per-call marshalling floor drops
206
204
  # to an ordinary extension call, while everything above Runtime (Uuid, the module doors,
207
205
  # batch slicing) stays shared byte-for-byte between backends. The pure-Fiddle definitions
208
- # remain the universal zero-compile fallback; precompiled platform gems are how the
209
- # extension ships without ever making a consumer compile anything. Set HYPERUUID_PURE=1 to
210
- # force Fiddle.
206
+ # remain the universal zero-compile fallback — the last resort, for a Ruby or a platform no
207
+ # platform gem covers; precompiled platform gems are how the extension ships without ever
208
+ # making a consumer compile anything, and they carry no Fiddle library at all.
211
209
  #
212
- # The third backend is WebAssembly (lib/hyperuuid/wasm_runtime.rb): the same core as a
213
- # wasm32-wasip1 module, run in-process by the `wasmtime` gem, which is deliberately not a
214
- # runtime dependency of this gem — a consumer who wants it installs it. HYPERUUID_WASM=1
215
- # forces it (and fails loudly if wasmtime is missing); otherwise it is only ever chosen when
216
- # there is no native library for this platform at all and wasmtime happens to be available,
217
- # so no supported platform's behavior changes by its existence.
218
- #
219
- # Both variables are read for presence, not value — set to anything at all, "0" and the
220
- # empty string included, they force their backend — and HYPERUUID_WASM wins when both are
221
- # set.
210
+ # HYPERUUID_PURE forces Fiddle. It is a testing and diagnostic switch — CI runs the whole suite
211
+ # through it, and it is how a suspected extension bug is ruled in or out — not a setting a
212
+ # deployment needs. It is read for presence, not value, so "0" and the empty string force it
213
+ # too. Inside a platform gem there is no library for it to load: BACKEND still reads :fiddle,
214
+ # HyperUuid.available? answers false, and the first door call raises a LoadError naming the
215
+ # universal gem, which is where the Fiddle backend lives.
222
216
  HyperUuid::BACKEND =
223
- if ENV["HYPERUUID_WASM"]
224
- begin
225
- require "wasmtime"
226
- rescue LoadError
227
- raise LoadError,
228
- "hyperuuid: HYPERUUID_WASM=1 needs the wasmtime gem — `gem install wasmtime` (or add it to your Gemfile)"
229
- end
230
- require_relative "hyperuuid/wasm_runtime"
231
- :wasm
232
- elsif ENV["HYPERUUID_PURE"]
217
+ if ENV["HYPERUUID_PURE"]
233
218
  :fiddle
234
219
  else
235
220
  # Two layouts, and both have to work. A released platform gem is a "fat" gem carrying one
@@ -243,7 +228,11 @@ HyperUuid::BACKEND =
243
228
  # other exists.
244
229
  #
245
230
  # A miss on both is not an error: it means this Ruby/platform combination has no
246
- # precompiled extension, which is precisely what the Fiddle backend below is for.
231
+ # precompiled extension, which is precisely what the Fiddle backend below is for. A
232
+ # platform with no shared library either still lands on Fiddle, so the first call raises
233
+ # its own precise error — a "not found" LoadError naming the path, or
234
+ # NativePlatform::UnsupportedPlatformError naming the platform. HyperUuid.available? is
235
+ # the quiet way to ask.
247
236
  begin
248
237
  require "hyperuuid/#{RUBY_VERSION[/\d+\.\d+/]}/hyperuuid_native"
249
238
  :native
@@ -252,22 +241,7 @@ HyperUuid::BACKEND =
252
241
  require "hyperuuid_native"
253
242
  :native
254
243
  rescue LoadError
255
- if HyperUuid::Runtime.fiddle_library_available?
256
- :fiddle
257
- else
258
- # No shared library for this platform either. wasmtime, if the consumer has it,
259
- # is the only backend left that can run here; without it, stay on Fiddle so the
260
- # first call raises its own precise error — a "not found" LoadError naming the
261
- # path, or NativePlatform::UnsupportedPlatformError naming the platform — rather
262
- # than a vaguer one from here. HyperUuid.available? is the quiet way to ask.
263
- begin
264
- require "wasmtime"
265
- require_relative "hyperuuid/wasm_runtime"
266
- :wasm
267
- rescue LoadError
268
- :fiddle
269
- end
270
- end
244
+ :fiddle
271
245
  end
272
246
  end
273
247
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyperuuid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Buvinghausen
@@ -53,11 +53,10 @@ dependencies:
53
53
  version: '0.9'
54
54
  description: |
55
55
  RFC 9562 UUID v4 (random), v5 (deterministic), v6 and v7 (time-sortable) generation, with
56
- batch and raw-bytes forms, from one native Rust core. Three backends behind one surface,
57
- selected automatically and all shipped prebuilt: a Magnus extension where a precompiled
58
- platform gem matches, stdlib Fiddle everywhere else, and the same core as a WebAssembly
59
- module for anyone who installs the wasmtime gem. No runtime bridge, no dependencies
60
- beyond Fiddle.
56
+ batch and raw-bytes forms, from one native Rust core. Two backends behind one surface,
57
+ selected automatically and both shipped prebuilt: a Magnus extension where a precompiled
58
+ platform gem matches, and stdlib Fiddle everywhere else. No runtime bridge, no
59
+ dependencies beyond Fiddle.
61
60
  executables: []
62
61
  extensions: []
63
62
  extra_rdoc_files: []
@@ -73,13 +72,11 @@ files:
73
72
  - lib/hyperuuid/native/linux-x64/libhyperuuid.so
74
73
  - lib/hyperuuid/native/osx-arm64/libhyperuuid.dylib
75
74
  - lib/hyperuuid/native/osx-x64/libhyperuuid.dylib
76
- - lib/hyperuuid/native/wasm32-wasip1/hyperuuid.wasm
77
75
  - lib/hyperuuid/native/win-arm64/hyperuuid.dll
78
76
  - lib/hyperuuid/native/win-x64/hyperuuid.dll
79
77
  - lib/hyperuuid/native_platform.rb
80
78
  - lib/hyperuuid/runtime.rb
81
79
  - lib/hyperuuid/uuid.rb
82
- - lib/hyperuuid/wasm_runtime.rb
83
80
  homepage: https://github.com/SkunkWerkx/HyperUuid
84
81
  licenses:
85
82
  - MIT
@@ -1,259 +0,0 @@
1
- require "wasmtime"
2
-
3
- module HyperUuid
4
- # The WebAssembly backend: the same Rust core, compiled once to a `wasm32-wasip1` module
5
- # (lib/hyperuuid/native/wasm32-wasip1/hyperuuid.wasm) and run inside this process by the
6
- # `wasmtime` gem — no per-platform shared library needed, and nothing dlopen'd. This is the
7
- # inverse of ruby.wasm: not Ruby running inside a wasm sandbox, but a wasm module running
8
- # inside Ruby.
9
- #
10
- # Same integration shape as the Magnus extension: on require (after runtime.rb has defined
11
- # the Fiddle backend) this redefines the `HyperUuid::Runtime` singleton methods **in place**
12
- # — no delegation layer, no second surface. Everything above Runtime (the `Uuid` class, the
13
- # module doors and their argument checks, batch slicing) stays shared byte-for-byte across
14
- # all three backends, and the same `HyperUuid::RandomSourceError` /
15
- # `HyperUuid::TimestampOutOfRangeError` classes carry the same messages — literally the
16
- # same constants and helper runtime.rb defines for the Fiddle backend.
17
- #
18
- # Two things differ from the native backends, both forced by the sandbox:
19
- #
20
- # * A wasm guest only sees its own linear memory, so no Fiddle::Pointer can cross. Every
21
- # buffer the core writes into is obtained from the module's own exported `malloc` (the
22
- # wasi-libc allocator Rust's std already uses on this target) and read back with
23
- # `Memory#read`. Using the guest's allocator rather than a host-picked offset is
24
- # load-bearing: dlmalloc claims the tail of the initial memory on first use, and a batch
25
- # written at a guessed offset was observed corrupted by the guest's next allocation.
26
- # * A `Wasmtime::Store` is single-threaded, so every call is serialized under one Mutex.
27
- # One shared instance per process is also what keeps the core's v7 counter — which lives
28
- # inside the instance — monotonic across threads and batches, exactly as the one dlopen'd
29
- # library does natively.
30
- module Runtime
31
- WASM_MODULE_PATH = File.join(NATIVE_DIR, "wasm32-wasip1", "hyperuuid.wasm")
32
- # Development loop: the in-repo cargo build, the same fallback runtime.rb takes for the
33
- # native library, so `HYPERUUID_WASM=1 bundle exec rspec` needs nothing staged by hand.
34
- WASM_REPO_BUILD_PATH = File.expand_path(
35
- File.join(__dir__, "../../../rust/target/wasm32-wasip1/release/hyperuuid.wasm")
36
- )
37
-
38
- # One instantiated module: the exported functions, the exported memory, and the two
39
- # 16-byte scratch buffers (one in, one out) the single-item doors reuse for the life of
40
- # the process. Batches and v5 names get a `malloc`/`free` pair per call instead — one
41
- # allocation amortized over `count` IDs, the same shape as the Fiddle backend's per-batch
42
- # Pointer.malloc.
43
- class WasmInstance
44
- attr_reader :memory, :in, :out
45
-
46
- EXPORTS = %i[
47
- malloc free
48
- uuid_new_v4 uuid_new_v5 uuid_new_v6 uuid_v6_unix_millis uuid_new_v6_batch
49
- uuid_new_v7 uuid_v7_unix_millis uuid_new_v7_batch
50
- uuid_v7_to_sql_order uuid_v7_to_rfc_order uuid_v6_to_sql_order uuid_v6_to_rfc_order
51
- hyperuuid_version
52
- ].freeze
53
-
54
- def initialize(path)
55
- path = WASM_REPO_BUILD_PATH if !File.exist?(path) && File.exist?(WASM_REPO_BUILD_PATH)
56
- unless File.exist?(path)
57
- raise LoadError,
58
- "hyperuuid: #{path} not found (this gem was built without its WebAssembly module)"
59
- end
60
-
61
- engine = Wasmtime::Engine.new
62
- mod = Wasmtime::Module.from_file(engine, path)
63
- linker = Wasmtime::Linker.new(engine)
64
- # The module imports five WASI preview1 functions (random_get is the one that
65
- # matters: it is the core's only entropy source; the rest are wasi-libc's startup
66
- # plumbing). A default WasiConfig — no stdio, no filesystem, no environment — is all
67
- # any of them need.
68
- Wasmtime::WASI::P1.add_to_linker_sync(linker)
69
- store = Wasmtime::Store.new(engine, wasi_p1_config: Wasmtime::WasiConfig.new)
70
- instance = linker.instantiate(store, mod)
71
-
72
- @memory = instance.export("memory").to_memory
73
- @fn = EXPORTS.to_h do |name|
74
- export = instance.export(name.to_s) or
75
- raise LoadError, "hyperuuid: #{path} does not export #{name} (a module older than this binding)"
76
- [name, export.to_func]
77
- end
78
- @in = malloc(16)
79
- @out = malloc(16)
80
- end
81
-
82
- def fn(name)
83
- @fn.fetch(name)
84
- end
85
-
86
- def malloc(size)
87
- ptr = @fn[:malloc].call(size)
88
- raise NoMemoryError, "hyperuuid: wasm malloc(#{size}) failed" if ptr.zero?
89
- ptr
90
- end
91
-
92
- def free(ptr)
93
- @fn[:free].call(ptr)
94
- end
95
-
96
- # Runs `block` with a `size`-byte guest buffer, freeing it afterwards even on error.
97
- def with_buffer(size)
98
- ptr = malloc(size)
99
- begin
100
- yield ptr
101
- ensure
102
- free(ptr)
103
- end
104
- end
105
- end
106
-
107
- @wasm_mutex = Mutex.new
108
- @wasm = nil
109
-
110
- class << self
111
- def new_v4
112
- wasm do |w|
113
- rc = w.fn(:uuid_new_v4).call(w.out)
114
- raise random_source_failure("uuid_new_v4") unless rc.zero?
115
- w.memory.read(w.out, 16)
116
- end
117
- end
118
-
119
- def new_v5(namespace_bytes, name_bytes)
120
- wasm do |w|
121
- w.memory.write(w.in, namespace_bytes)
122
- # A zero-length name still needs a valid (non-null) pointer inside the guest —
123
- # the `in` scratch, already holding the namespace, serves; nothing reads past
124
- # a zero length.
125
- rc =
126
- if name_bytes.empty?
127
- w.fn(:uuid_new_v5).call(w.in, w.in, 0, w.out)
128
- else
129
- w.with_buffer(name_bytes.bytesize) do |name|
130
- w.memory.write(name, name_bytes)
131
- w.fn(:uuid_new_v5).call(w.in, name, name_bytes.bytesize, w.out)
132
- end
133
- end
134
- raise random_source_failure("uuid_new_v5") unless rc.zero?
135
- w.memory.read(w.out, 16)
136
- end
137
- end
138
-
139
- def new_v6(unix_millis)
140
- wasm do |w|
141
- rc = w.fn(:uuid_new_v6).call(i64(unix_millis), w.out)
142
- case rc
143
- when 0 then w.memory.read(w.out, 16)
144
- when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
145
- else raise random_source_failure("uuid_new_v6")
146
- end
147
- end
148
- end
149
-
150
- def v6_unix_millis(bytes)
151
- wasm do |w|
152
- w.memory.write(w.in, bytes)
153
- u64(w.fn(:uuid_v6_unix_millis).call(w.in))
154
- end
155
- end
156
-
157
- def new_v6_batch(count, unix_millis)
158
- return "" if count.zero?
159
- wasm do |w|
160
- w.with_buffer(count * 16) do |out|
161
- rc = w.fn(:uuid_new_v6_batch).call(i64(unix_millis), count, out)
162
- case rc
163
- when 0 then w.memory.read(out, count * 16)
164
- when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
165
- else raise random_source_failure("uuid_new_v6_batch")
166
- end
167
- end
168
- end
169
- end
170
-
171
- def new_v7(unix_millis)
172
- wasm do |w|
173
- rc = w.fn(:uuid_new_v7).call(i64(unix_millis), w.out)
174
- case rc
175
- when 0 then w.memory.read(w.out, 16)
176
- when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
177
- else raise random_source_failure("uuid_new_v7")
178
- end
179
- end
180
- end
181
-
182
- def v7_unix_millis(bytes)
183
- wasm do |w|
184
- w.memory.write(w.in, bytes)
185
- u64(w.fn(:uuid_v7_unix_millis).call(w.in))
186
- end
187
- end
188
-
189
- def new_v7_batch(count, unix_millis)
190
- return "" if count.zero?
191
- wasm do |w|
192
- w.with_buffer(count * 16) do |out|
193
- rc = w.fn(:uuid_new_v7_batch).call(i64(unix_millis), count, out)
194
- case rc
195
- when 0 then w.memory.read(out, count * 16)
196
- when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
197
- else raise random_source_failure("uuid_new_v7_batch")
198
- end
199
- end
200
- end
201
- end
202
-
203
- def v7_to_sql_order(bytes)
204
- rewrite(:uuid_v7_to_sql_order, bytes)
205
- end
206
-
207
- def v7_to_rfc_order(bytes)
208
- rewrite(:uuid_v7_to_rfc_order, bytes)
209
- end
210
-
211
- def v6_to_sql_order(bytes)
212
- rewrite(:uuid_v6_to_sql_order, bytes)
213
- end
214
-
215
- def v6_to_rfc_order(bytes)
216
- rewrite(:uuid_v6_to_rfc_order, bytes)
217
- end
218
-
219
- def packed_version
220
- wasm { |w| w.fn(:hyperuuid_version).call }
221
- end
222
-
223
- private
224
-
225
- # The in-place byte-order rewrites: copy the caller's frozen bytes into the guest,
226
- # let the core rewrite them there, read the result back.
227
- def rewrite(export, bytes)
228
- wasm do |w|
229
- w.memory.write(w.in, bytes)
230
- w.fn(export).call(w.in)
231
- w.memory.read(w.in, 16)
232
- end
233
- end
234
-
235
- # Every call holds the one lock: the Store is single-threaded, and the lazily created
236
- # instance is shared by every thread for the life of the process (never torn down,
237
- # same as the dlopen'd library natively).
238
- def wasm
239
- @wasm_mutex.synchronize do
240
- @wasm ||= WasmInstance.new(WASM_MODULE_PATH)
241
- yield @wasm
242
- end
243
- end
244
-
245
- # wasm has no unsigned integers: a u64 crosses as an i64. The core's own range checks
246
- # (48-bit v7, 60-bit v6) still see the exact value the caller passed, they just see
247
- # it through a two's-complement reinterpretation on the way in and back. Exact only
248
- # for a value that is a u64 to begin with, which the doors in hyperuuid.rb guarantee
249
- # before any backend is called.
250
- def i64(value)
251
- value >= 2**63 ? value - 2**64 : value
252
- end
253
-
254
- def u64(value)
255
- value.negative? ? value + 2**64 : value
256
- end
257
- end
258
- end
259
- end