hyperuuid 0.5.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 +4 -4
- data/README.md +63 -116
- data/lib/hyperuuid/native/linux-arm64/libhyperuuid.so +0 -0
- data/lib/hyperuuid/native/linux-musl-arm64/libhyperuuid.so +0 -0
- data/lib/hyperuuid/native/linux-musl-x64/libhyperuuid.so +0 -0
- data/lib/hyperuuid/native/linux-x64/libhyperuuid.so +0 -0
- data/lib/hyperuuid/native/osx-arm64/libhyperuuid.dylib +0 -0
- data/lib/hyperuuid/native/osx-x64/libhyperuuid.dylib +0 -0
- data/lib/hyperuuid/native/win-arm64/hyperuuid.dll +0 -0
- data/lib/hyperuuid/native/win-x64/hyperuuid.dll +0 -0
- data/lib/hyperuuid/runtime.rb +38 -26
- data/lib/hyperuuid/uuid.rb +1 -1
- data/lib/hyperuuid.rb +26 -52
- metadata +5 -8
- data/lib/hyperuuid/native/wasm32-wasip1/hyperuuid.wasm +0 -0
- data/lib/hyperuuid/wasm_runtime.rb +0 -259
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9469a1d25a10c66127020801f9773f8dc64325fb3e9567c76ab50d3182641ca9
|
|
4
|
+
data.tar.gz: bf59070f36ce9d2fef96080649dcd0392a01d3e0b18b6311804d2839ed465bfc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
[`Fiddle`](https://docs.ruby-lang.org/en/master/Fiddle.html) — dlopen/dlsym plus
|
|
17
|
-
call, no runtime bridge.
|
|
18
|
-
|
|
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
|
|
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`
|
|
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
|
|
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.
|
|
174
|
+
Selection happens once, at `require`, in that order.
|
|
178
175
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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.
|
|
193
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
`
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
needed either
|
|
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 |
|
|
298
|
-
| --- | --- | --- |
|
|
299
|
-
|
|
|
300
|
-
|
|
|
301
|
-
| 3.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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.**
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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,7 +297,7 @@ 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
|
|
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.
|
|
@@ -361,14 +305,12 @@ built for the Ruby you are running and put where `require` looks — and that is
|
|
|
361
305
|
```sh
|
|
362
306
|
cd rust
|
|
363
307
|
cargo cdylib # libhyperuuid, what the Fiddle backend loads
|
|
364
|
-
cargo wasm-module # hyperuuid.wasm, what the wasm 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
|
-
|
|
407
|
-
HYPERUUID_PURE=1
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/hyperuuid/runtime.rb
CHANGED
|
@@ -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
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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
|
-
#
|
|
11
|
-
#
|
|
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
|
|
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
|
|
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
|
{
|
data/lib/hyperuuid/uuid.rb
CHANGED
|
@@ -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
|
|
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
|
|
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)
|
|
12
|
-
#
|
|
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.
|
|
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
|
|
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
|
|
190
|
-
#
|
|
191
|
-
#
|
|
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
|
|
209
|
-
#
|
|
210
|
-
#
|
|
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
|
-
#
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
#
|
|
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["
|
|
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
|
-
|
|
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
|
+
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.
|
|
57
|
-
selected automatically and
|
|
58
|
-
platform gem matches, stdlib Fiddle everywhere else
|
|
59
|
-
|
|
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
|
|
Binary file
|
|
@@ -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
|