hyperuuid-wasm 0.7.0 → 0.9.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 +113 -45
- data/ext/hyperuuid_native/3.4/hyperuuid_native.a.gz +0 -0
- data/ext/hyperuuid_native/4.0/hyperuuid_native.a.gz +0 -0
- data/lib/hyperuuid/errors.rb +4 -5
- data/lib/hyperuuid/runtime.rb +91 -68
- data/lib/hyperuuid/uuid.rb +119 -77
- data/lib/hyperuuid.rb +44 -52
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b35d26b737a64df74f7a10783112899a0386bbd7dd7b7aa6387b4cbd5fb3c09
|
|
4
|
+
data.tar.gz: c37cd5f4d1bc6ad5eef8d181aa263ac61212c73f4f73640724f63e53a9797f61
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1580f8edd7863896d8b646db61e5918aa7efcaa22db88995e2c6aa2d4456f8dd2698e1200e214191830b984aee8fcdd56f22f5da51985a48305401bf10b84201
|
|
7
|
+
data.tar.gz: e5a86147bafa4f1e9bfb266656e073f135286f07750dbb930cf6a8b10e9a3b45060e5a8daf73cd6c7162ee060a75f3e4f02c3a609c63fc8b94bbb26a0a46c653
|
data/README.md
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
[](https://rubygems.org/gems/hyperuuid)
|
|
5
5
|
[](https://github.com/SkunkWerkx/HyperUuid/blob/master/LICENSE)
|
|
6
6
|
|
|
7
|
-
**Ruby's own stdlib stops at `SecureRandom.uuid` — random v4,
|
|
7
|
+
**Ruby's own stdlib stops at `SecureRandom.uuid` and `SecureRandom.uuid_v7` — random v4 and v7, one at a time. No v5, no batches, no timestamp read-back, no SQL Server order. This gem fills that in, with zero gem dependency beyond `Fiddle` (which ships with every Ruby install) — and it's faster than `SecureRandom.uuid` too.**
|
|
8
8
|
|
|
9
|
-
RFC 9562 UUID v4 (random), v5 (deterministic)
|
|
10
|
-
two backends sharing one public surface. Ruby 3.3 is the floor. The fast path is a native
|
|
9
|
+
RFC 9562 UUID v4 (random), v5 (deterministic) and v7 (time-sortable) generation, with a
|
|
10
|
+
version 8 SQL Server order for v7 and 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
|
|
@@ -22,12 +22,13 @@ require "hyperuuid"
|
|
|
22
22
|
|
|
23
23
|
id = HyperUuid.new_v4
|
|
24
24
|
id2 = HyperUuid.new_v5(HyperUuid::Namespaces::DNS, "example.com")
|
|
25
|
-
id3 = HyperUuid.
|
|
26
|
-
id4 = HyperUuid.new_v7
|
|
25
|
+
id3 = HyperUuid.new_v7
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
id3.timestamp # recover the embedded UTC Time
|
|
28
|
+
id3.timestamp(raise_on_mismatch: false) # nil instead of raising if id3 isn't an RFC 9562 v7
|
|
29
|
+
id3.rfc?(7) # the RFC 9562 variant and version 7, the guard before trusting its fields
|
|
30
|
+
sql = id3.to_sql_order # a version 8 laid out so SQL Server's uniqueidentifier sorts by creation order
|
|
31
|
+
sql.timestamp(layout: :sql_server) # read straight from SQL order, no round trip
|
|
31
32
|
|
|
32
33
|
# One native call, one random-bytes fetch, one counter reservation for the whole batch:
|
|
33
34
|
batch = HyperUuid.new_v7_batch(1000)
|
|
@@ -39,9 +40,9 @@ batch = HyperUuid.new_v7_batch(1000)
|
|
|
39
40
|
|---|---|---|
|
|
40
41
|
| `new_v4` | `Uuid` | — |
|
|
41
42
|
| `new_v5(namespace, name)` | `Uuid`, the same one for the same pair | a `Uuid` namespace (`Namespaces::DNS`/`URL`/`OID`/`X500`, or your own) and a `String` name — text is hashed as UTF-8, a binary String as its bytes, and it may be empty |
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
43
|
+
| `new_v7(time = nil)` | `Uuid` | nothing (now), a `Time`, or an Integer of Unix-epoch milliseconds |
|
|
44
|
+
| `new_v7_batch(count, time = nil)` | `Array` of `Uuid` | a count (at most `MAX_V7_BATCH`), and the same time forms |
|
|
45
|
+
| `new_v7_batch_bytes(count, time = nil)` | one binary `String`, 16 bytes per UUID | as `new_v7_batch` — see [Bulk generation into bytes](#bulk-generation-into-bytes) |
|
|
45
46
|
| `native_version` | the loaded core's `"major.minor.patch"` | — |
|
|
46
47
|
| `available?` | `true`/`false`, never raising | — |
|
|
47
48
|
|
|
@@ -53,29 +54,94 @@ runtime dependency on the `uuid` gem:
|
|
|
53
54
|
| `Uuid.parse(text)` | the 8-4-4-4-12 hyphenated form `#to_s` produces, either case, and nothing else — an `ArgumentError` otherwise |
|
|
54
55
|
| `Uuid.new(bytes)` | wraps 16 raw RFC 9562-ordered bytes |
|
|
55
56
|
| `Uuid::NIL` `Uuid::MAX` | the RFC 9562 §5.9/§5.10 special values |
|
|
56
|
-
| `#bytes` `#to_s`
|
|
57
|
-
| `#
|
|
58
|
-
| `#
|
|
57
|
+
| `#bytes` `#to_s` | the frozen binary String, the hyphenated text |
|
|
58
|
+
| `#version(layout: :rfc9562)` | the version in that layout: the nibble, 0-15, in RFC order; in `:sql_server`, 7, the version a SQL-ordered value was converted from, and 0 when the bytes aren't one |
|
|
59
|
+
| `#variant` | the RFC 9562 §4.1 variant, as a Symbol: `:rfc9562` (every UUID this gem mints, and the only variant with versions), `:ncs` (Nil among them), `:microsoft`, `:future` (Max among them) |
|
|
60
|
+
| `#rfc?(version, layout: :rfc9562)` | whether it is an RFC 9562 UUID of that version: the variant and the version together. A version outside 0-15 is `false` |
|
|
61
|
+
| `#timestamp(raise_on_mismatch: true, layout: :rfc9562)` | the UTC `Time` embedded in an RFC 9562 version 7 UUID held in that layout, in one native call that checks the variant as well as the version (a 7 nibble under another variant has no timestamp, and nor does a v1 or v6 minted elsewhere); `nil` instead of an `ArgumentError` for anything that isn't one when passed `false` |
|
|
62
|
+
| `#to_sql_order` `#from_sql_order` | to and from the layout SQL Server's `uniqueidentifier` sorts by, refusing what doesn't fit it |
|
|
59
63
|
| `==` `eql?` `hash` `<=>` | value equality and byte-order comparison (`Comparable`) |
|
|
60
64
|
|
|
61
|
-
`#to_sql_order`/`#from_sql_order` convert a version
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
`#to_sql_order`/`#from_sql_order` convert a version 7 UUID to and from the layout SQL Server's
|
|
66
|
+
`uniqueidentifier` needs on the wire to sort by creation order — computed once in the native
|
|
67
|
+
Rust core rather than reimplemented in Ruby, and verified there (and independently against the
|
|
68
|
+
real `System.Data.SqlTypes.SqlGuid` comparator in the C# binding's test suite). The converted
|
|
69
|
+
value is an RFC 9562 version 8: the 8 and the RFC variant sit where SQL Server's text form and
|
|
70
|
+
a .NET `Guid` show them, and the v7's 122 other bits (timestamp, counter, random) fill the rest
|
|
71
|
+
in the order SQL Server compares them. Every v7 converts, and the conversion back is exact.
|
|
72
|
+
Either direction refuses a value of the wrong kind with an `ArgumentError`: anything that isn't
|
|
73
|
+
an RFC 9562 v7 for `#to_sql_order`, anything without the layout's 8 and variant for
|
|
74
|
+
`#from_sql_order`.
|
|
75
|
+
|
|
76
|
+
### Layouts
|
|
77
|
+
|
|
78
|
+
`#version`, `#rfc?` and `#timestamp` take the byte order the value is held in as `layout:`, a
|
|
79
|
+
Symbol: `:rfc9562` (the default, what every other method takes and returns) or `:sql_server`
|
|
80
|
+
(what `#to_sql_order` returns, a version 8 holding a converted v7). `Uuid::LAYOUTS` lists them.
|
|
81
|
+
Anything else is an `ArgumentError`, never a guess. With the default, `#version` and `#rfc?`
|
|
82
|
+
read the value as RFC 9562 order, which looks at the wrong bytes of a SQL-ordered value (one
|
|
83
|
+
from `#to_sql_order`, or a `uniqueidentifier` read back from SQL Server), so pass
|
|
84
|
+
`layout: :sql_server` at every call site that can see one. There is deliberately no
|
|
85
|
+
layout-agnostic check: whoever holds the value knows its order.
|
|
86
|
+
|
|
87
|
+
`#variant` reads RFC 9562 order only and has no layout form. The SQL Server layout keeps the
|
|
88
|
+
variant in the same place, so a SQL-ordered value reads `:rfc9562` there, but that says nothing
|
|
89
|
+
about whether it holds a converted v7; to validate one, or a `uniqueidentifier` read-back,
|
|
90
|
+
`#rfc?(7, layout: :sql_server)` checks the layout's 8 and variant.
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
sql = HyperUuid.new_v7.to_sql_order
|
|
94
|
+
sql.version(layout: :sql_server) # => 7
|
|
95
|
+
sql.rfc?(7, layout: :sql_server) # => true
|
|
96
|
+
sql.timestamp(layout: :sql_server) # the same Time as sql.from_sql_order.timestamp
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The bytes alone cannot say which order they are in, so the layout is yours to know: an
|
|
100
|
+
RFC-ordered value can happen to carry the layout's 8 and variant (a random v4 does one time in
|
|
101
|
+
16), and `#from_sql_order` then converts it rather than refusing it. The same goes the other
|
|
102
|
+
way: a value already in SQL order carries a 7 where `#to_sql_order` looks for the version about
|
|
103
|
+
one time in 16, and is then converted a second time rather than refused.
|
|
104
|
+
|
|
105
|
+
### Binding to SQL Server
|
|
106
|
+
|
|
107
|
+
Send `#to_sql_order`'s result as its raw `#bytes`, not as UUID text. SQL Server stores a UUID
|
|
108
|
+
string's first three groups byte-reversed, which loses the creation order
|
|
109
|
+
([details](https://github.com/SkunkWerkx/HyperUuid#sql-server-ordering)); tiny_tds 3.4 and an ActiveRecord `:uuid` attribute both send text. A binary
|
|
110
|
+
literal lands byte for byte:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
sql = HyperUuid.new_v7.to_sql_order
|
|
114
|
+
client.execute("INSERT INTO orders (id) VALUES (0x#{sql.bytes.unpack1('H*')})").do
|
|
115
|
+
|
|
116
|
+
row = client.execute("SELECT CAST(id AS binary(16)) AS id FROM orders").first
|
|
117
|
+
sql = HyperUuid::Uuid.new(row["id"]) # read with layout: :sql_server
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
In ActiveRecord, map the column as `:binary`, which quotes as `0x…`. Reading needs the `CAST`
|
|
121
|
+
too: tiny_tds returns a `uniqueidentifier` as text with those groups reversed.
|
|
122
|
+
|
|
123
|
+
### The version 7 batch limit
|
|
124
|
+
|
|
125
|
+
`HyperUuid::MAX_V7_BATCH` is 67,108,864 (2²⁶), the size of the counter that orders version 7
|
|
126
|
+
UUIDs within a millisecond, and the most one `new_v7_batch` or `new_v7_batch_bytes` call
|
|
127
|
+
mints. A larger count is an `ArgumentError` naming the limit, raised before anything is
|
|
128
|
+
allocated. Every batch is in strictly increasing order: the counter is one process-wide
|
|
129
|
+
sequence, so a batch can straddle the point where it wraps back to 0, and the UUIDs from there
|
|
130
|
+
on carry a timestamp one millisecond later than the one supplied rather than sorting before the
|
|
131
|
+
ones ahead of them.
|
|
132
|
+
|
|
133
|
+
That roll-forward orders one batch, not the stream. The next batch or single `new_v7` in the
|
|
134
|
+
same real millisecond starts its counter just past the wrap and carries the supplied timestamp,
|
|
135
|
+
so it sorts before the previous batch's tail, which was stamped a millisecond later. Two single
|
|
136
|
+
`new_v7` calls either side of the wrap in one millisecond sort in reverse the same way. It
|
|
137
|
+
happens at most once per 2²⁶ UUIDs the process mints.
|
|
71
138
|
|
|
72
139
|
### Errors
|
|
73
140
|
|
|
74
141
|
Two exceptions are this gem's own, and both carry the same message on every backend:
|
|
75
142
|
|
|
76
|
-
- `HyperUuid::TimestampOutOfRangeError` — the time cannot be embedded
|
|
77
|
-
48-bit millisecond field,
|
|
78
|
-
epoch included).
|
|
143
|
+
- `HyperUuid::TimestampOutOfRangeError` — the time cannot be embedded in a version 7: past
|
|
144
|
+
its 48-bit millisecond field, or negative (a `Time` before the Unix epoch included).
|
|
79
145
|
- `HyperUuid::RandomSourceError` — the operating system's random source failed.
|
|
80
146
|
|
|
81
147
|
Through 0.3.0 these were reachable only as `HyperUuid::Runtime::TimestampOutOfRangeError` and
|
|
@@ -84,8 +150,11 @@ Through 0.3.0 these were reachable only as `HyperUuid::Runtime::TimestampOutOfRa
|
|
|
84
150
|
Everything else is a caller bug and raises Ruby's own error for it, checked once in the shared
|
|
85
151
|
doors rather than left to whichever backend is live: a `TypeError` for a time that is not a
|
|
86
152
|
`Time`, an Integer or `nil`, for a count that is not an Integer, for a namespace that is not a
|
|
87
|
-
`Uuid` or a name that is not a `String
|
|
88
|
-
0
|
|
153
|
+
`Uuid` or a name that is not a `String`, for a version passed to `#rfc?` that is not an
|
|
154
|
+
Integer; an `ArgumentError` for a count outside 0..`MAX_V7_BATCH`, for a layout other than
|
|
155
|
+
`:rfc9562`/`:sql_server`, and for a batch too large for the platform to address (a 32-bit
|
|
156
|
+
target such as ruby.wasm). The SQL Server conversions' one refusal (a value of the wrong
|
|
157
|
+
version) comes from the core, as an `ArgumentError` worded the same on both backends.
|
|
89
158
|
|
|
90
159
|
### Is the native core there?
|
|
91
160
|
|
|
@@ -93,6 +162,8 @@ doors rather than left to whichever backend is live: a `TypeError` for a time th
|
|
|
93
162
|
raising — for a consumer with a fallback of its own:
|
|
94
163
|
|
|
95
164
|
```ruby
|
|
165
|
+
require "securerandom"
|
|
166
|
+
|
|
96
167
|
id = HyperUuid.available? ? HyperUuid.new_v7.to_s : SecureRandom.uuid
|
|
97
168
|
```
|
|
98
169
|
|
|
@@ -105,18 +176,18 @@ looked for, or `HyperUuid::NativePlatform::UnsupportedPlatformError` naming the
|
|
|
105
176
|
|
|
106
177
|
## Why not `SecureRandom.uuid`?
|
|
107
178
|
|
|
108
|
-
`SecureRandom.uuid`
|
|
179
|
+
`SecureRandom.uuid` gives you a random v4 UUID and `SecureRandom.uuid_v7` a v7 (Ruby 3.3 and later, so every Ruby this gem supports). Ruby's stdlib has no v5, nothing to read a timestamp back out, no batch form, and no SQL Server ordering. If you need more than that, the choice is really "which gem":
|
|
109
180
|
|
|
110
|
-
1. **
|
|
181
|
+
1. **The RFC 9562 versions new systems use, one gem, zero extra dependency.** v4/v5/v7 plus batch generation plus `Nil`/`Max`, and nothing added to your `Gemfile.lock` beyond `Fiddle` — Ruby's own bundled FFI layer, not a third-party C extension to compile — and on the precompiled platform gems not even that.
|
|
111
182
|
2. **No native-extension compile step.** Third-party UUID gems that go beyond v4 are typically pure Ruby or wrap a C extension compiled at install time; this gem ships its fast path as a prebuilt platform-gem extension and its fallback as a `dlopen`ed prebuilt library — either way, the gem itself compiles nothing at install time ([Install](#install) has the one caveat, which is Fiddle's own).
|
|
112
183
|
3. **Batch generation.** `new_v7_batch(1000)` shares one timestamp capture, one random-bytes fetch, and one counter reservation across the whole batch instead of paying per-item overhead a thousand times over.
|
|
113
184
|
4. **Cross-language consistency.** The same Rust core mints v5 namespace UUIDs for Python, Go, C#, and every other binding in this repo — verified in CI to match Python's own `uuid.uuid5` byte-for-byte. If your system isn't Ruby-only, no Ruby-only gem can offer that.
|
|
114
185
|
|
|
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
|
|
186
|
+
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 one v4 or v7 at a time is all you need, `SecureRandom.uuid` and `SecureRandom.uuid_v7` are simpler and already in stdlib — that's a completely reasonable choice.
|
|
116
187
|
|
|
117
188
|
## Bulk generation into bytes
|
|
118
189
|
|
|
119
|
-
`
|
|
190
|
+
`new_v7_batch_bytes` returns the batch as one binary `String` of raw RFC 9562-ordered bytes — 16 per UUID — instead of an Array of `Uuid` objects:
|
|
120
191
|
|
|
121
192
|
```ruby
|
|
122
193
|
bytes = HyperUuid.new_v7_batch_bytes(1000)
|
|
@@ -141,9 +212,7 @@ Real numbers, `benchmark-ips` on Ruby 4.0.7, linux-x64 on an Intel Core i9-11900
|
|
|
141
212
|
|---|---:|---:|
|
|
142
213
|
| `SecureRandom.uuid` | 943,917 | baseline |
|
|
143
214
|
| `HyperUuid.new_v4` | 5,236,000 | **5.5x faster** |
|
|
144
|
-
| `HyperUuid.new_v6` (explicit ms) | 3,811,000 | **4.0x faster** |
|
|
145
215
|
| `HyperUuid.new_v7` (explicit ms) | 3,785,000 | **4.0x faster** |
|
|
146
|
-
| `HyperUuid.new_v6` (current time) | 3,677,000 | **3.9x faster** |
|
|
147
216
|
| `HyperUuid.new_v7` (current time) | 3,574,000 | **3.8x faster** |
|
|
148
217
|
| `HyperUuid.new_v5` | 1,942,000 | 2.1x faster |
|
|
149
218
|
|
|
@@ -151,18 +220,16 @@ A `HyperUuid.new_v4` — real entropy, correct version/variant bits, minted by t
|
|
|
151
220
|
|
|
152
221
|
The "current time" rows deserve a footnote, because they will not look like this everywhere. Here they land beside the explicit-ms rows: the only difference between the two is one `Process.clock_gettime(CLOCK_REALTIME)` wall-clock read, and on this machine that read is too cheap to see. On a machine with a slow clock it is the whole story — where a virtualized clock defeats the vDSO fast path the same read costs ~1µs, which puts both current-time rows at parity with `SecureRandom.uuid` while the explicit-ms rows stay well ahead of it. `SecureRandom.uuid` never reads a clock — random v4 is the only thing it does. If your clock is slow and you are minting many, read it once and pass the timestamp, or use the batch doors.
|
|
153
222
|
|
|
154
|
-
The Fiddle fallback (`HYPERUUID_PURE=1`, and any platform without a prebuilt extension) keeps its own diet — a reused thread-local scratch buffer instead of two GC-finalizer-registering mallocs per call, zero-copy `String` passes for read-only inputs, an unsynchronized fast path past the load mutex — landing at 1.16x slower than `SecureRandom.uuid` for v4 (1.24 µs against 1.07 µs in its own run) and about 1.3x slower for
|
|
223
|
+
The Fiddle fallback (`HYPERUUID_PURE=1`, and any platform without a prebuilt extension) keeps its own diet — a reused thread-local scratch buffer instead of two GC-finalizer-registering mallocs per call, zero-copy `String` passes for read-only inputs, an unsynchronized fast path past the load mutex — landing at 1.16x slower than `SecureRandom.uuid` for v4 (1.24 µs against 1.07 µs in its own run) and about 1.3x slower for v7, with the same structural story as before: `Fiddle`'s interpreted marshalling is the floor, and the batch doors are how you amortize it.
|
|
155
224
|
|
|
156
225
|
Batch generation still amortizes per-call cost on both backends — one native call for the whole batch:
|
|
157
226
|
|
|
158
227
|
| Call | i/s (Magnus backend) |
|
|
159
228
|
|---|---:|
|
|
160
|
-
| `new_v6` × 1000 (individual) | 3,761 |
|
|
161
|
-
| `new_v6_batch(1000)` | 4,857 (**1.3x**) |
|
|
162
229
|
| `new_v7` × 1000 (individual) | 3,742 |
|
|
163
230
|
| `new_v7_batch(1000)` | 4,850 (**1.3x**) |
|
|
164
231
|
|
|
165
|
-
The multiplier is small on this backend for the best reason available: the individual calls are cheap, so there is little waste left to amortize, and what `new_v7_batch` spends its 206 µs on is building a thousand `Uuid` objects — the byte form above does the same native work in 5 µs. On the Fiddle backend, where each call costs 1.4 µs, the same batch is 6.7x the loop. If you need v5/
|
|
232
|
+
The multiplier is small on this backend for the best reason available: the individual calls are cheap, so there is little waste left to amortize, and what `new_v7_batch` spends its 206 µs on is building a thousand `Uuid` objects — the byte form above does the same native work in 5 µs. On the Fiddle backend, where each call costs 1.4 µs, the same batch is 6.7x the loop. If you need v5/v7, need many at once, or need this Ruby service's IDs to agree byte-for-byte with a Go or Python service's, that's what this gem is for — and now it's the fast option too, not just the capable one.
|
|
166
233
|
|
|
167
234
|
## Backends
|
|
168
235
|
|
|
@@ -248,8 +315,7 @@ one at `require` time:
|
|
|
248
315
|
| 3.4 (until its EOL 2028-03-31) | Magnus, `BACKEND == :native` | platform |
|
|
249
316
|
| 3.3 (the floor, until its EOL 2027-03-31) | Fiddle | universal |
|
|
250
317
|
|
|
251
|
-
What stands behind each cell: CI runs the whole suite through each Magnus extension on every
|
|
252
|
-
push — Ruby 3.4 and 4.0 on all seven platform-gem platforms, the two musl ones inside each
|
|
318
|
+
What stands behind each cell: CI runs the whole suite through each Magnus extension — Ruby 4.0 on every pull request and Ruby 3.4 in the weekly and release builds, on all seven platform-gem platforms, the two musl ones inside each
|
|
253
319
|
Ruby's own Alpine image — and the Fiddle suite on Ruby 4.0 on every one of them, plus on Ruby
|
|
254
320
|
3.3 inside Alpine. Intel macOS has no CI leg: its library is cross-built and tested at the
|
|
255
321
|
core, and Ruby there runs the universal gem's Fiddle backend over it. Anywhere else — a platform with no native build at
|
|
@@ -326,7 +392,7 @@ bundle exec rbwasm build --ruby-version 4.0 -o ruby.wasm
|
|
|
326
392
|
Load `ruby.wasm` with [`@ruby/wasm-wasi`](https://www.npmjs.com/package/@ruby/wasm-wasi)
|
|
327
393
|
(`DefaultRubyVM` in a browser, `RubyVM.instantiateModule` under Node), then
|
|
328
394
|
`require "/bundle/setup"` and `require "hyperuuid"` as anywhere else. `HyperUuid::BACKEND` is
|
|
329
|
-
`:native`: every door, the batch and raw-bytes forms, and `
|
|
395
|
+
`:native`: every door, the batch and raw-bytes forms, and `new_v7` with no argument,
|
|
330
396
|
which read the clock through WASI.
|
|
331
397
|
|
|
332
398
|
- **Ruby 3.4 and 4.0**, one archive each, the same minors the platform gems cover, picked by
|
|
@@ -367,6 +433,7 @@ bundle install
|
|
|
367
433
|
bundle exec rake native:dev # build the Magnus extension for this Ruby and stage it
|
|
368
434
|
bundle exec rspec # BACKEND == :native
|
|
369
435
|
HYPERUUID_PURE=1 bundle exec rspec # BACKEND == :fiddle
|
|
436
|
+
HYPERUUID_BIG_BATCH=1 bundle exec rspec spec/inspection_spec.rb # adds the 1 GiB MAX_V7_BATCH batch
|
|
370
437
|
bundle exec rake docs:check # every public object carries a doc comment
|
|
371
438
|
ruby benchmark/uuid_benchmark.rb # prints the backend it measured
|
|
372
439
|
```
|
|
@@ -393,11 +460,12 @@ flags in the forge's `build-magnus.sh`, which is also what CI uses on every plat
|
|
|
393
460
|
|
|
394
461
|
The musl build has no host to run on outside a container. With the core built for musl at
|
|
395
462
|
`rust/target/musl/linux-musl-x64/libhyperuuid.so`, this runs the Fiddle suite against it on
|
|
396
|
-
Alpine, with the checkout mounted read-only:
|
|
463
|
+
Alpine, with the checkout mounted read-only (`corpus/` comes along: `spec/corpus_spec.rb` finds
|
|
464
|
+
it by walking up from the spec):
|
|
397
465
|
|
|
398
466
|
```sh
|
|
399
467
|
docker run --rm -v "$PWD/..":/src:ro ruby:4.0-alpine sh -euc '
|
|
400
|
-
mkdir /work && cp -r /src/ruby /work/
|
|
468
|
+
mkdir /work && cp -r /src/ruby /src/corpus /work/
|
|
401
469
|
mkdir -p /work/ruby/lib/hyperuuid/native/linux-musl-x64
|
|
402
470
|
cp /src/rust/target/musl/linux-musl-x64/libhyperuuid.so /work/ruby/lib/hyperuuid/native/linux-musl-x64/
|
|
403
471
|
cd /work/ruby && rm -f Gemfile.lock
|
|
@@ -411,7 +479,7 @@ Fiddle that Ruby 3.3 ships:
|
|
|
411
479
|
|
|
412
480
|
```sh
|
|
413
481
|
docker run --rm -v "$PWD/..":/src:ro ruby:3.3-alpine sh -euc '
|
|
414
|
-
mkdir /work && cp -r /src/ruby /work/
|
|
482
|
+
mkdir /work && cp -r /src/ruby /src/corpus /work/
|
|
415
483
|
mkdir -p /work/ruby/lib/hyperuuid/native/linux-musl-x64
|
|
416
484
|
cp /src/rust/target/musl/linux-musl-x64/libhyperuuid.so /work/ruby/lib/hyperuuid/native/linux-musl-x64/
|
|
417
485
|
cd /work/ruby && rm -f Gemfile Gemfile.lock
|
|
Binary file
|
|
Binary file
|
data/lib/hyperuuid/errors.rb
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
module HyperUuid
|
|
2
2
|
# Raised when the operating system's random source fails while a UUID is being minted —
|
|
3
|
-
# the one way `new_v4`, `
|
|
3
|
+
# the one way `new_v4`, `new_v7` and its batch forms can fail once their
|
|
4
4
|
# arguments are valid. Carries the same message on every backend.
|
|
5
5
|
class RandomSourceError < StandardError; end
|
|
6
6
|
|
|
7
|
-
# Raised when a timestamp cannot be embedded in the UUID being minted:
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# every backend.
|
|
7
|
+
# Raised when a timestamp cannot be embedded in the version 7 UUID being minted: one past
|
|
8
|
+
# RFC 9562's 48-bit millisecond field, or a negative one (a `Time` before the Unix epoch
|
|
9
|
+
# included). Carries the same message on every backend.
|
|
11
10
|
class TimestampOutOfRangeError < StandardError; end
|
|
12
11
|
end
|
data/lib/hyperuuid/runtime.rb
CHANGED
|
@@ -22,12 +22,27 @@ module HyperUuid
|
|
|
22
22
|
RandomSourceError = HyperUuid::RandomSourceError
|
|
23
23
|
TimestampOutOfRangeError = HyperUuid::TimestampOutOfRangeError
|
|
24
24
|
|
|
25
|
-
# One copy of
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
V6_TIMESTAMP_OUT_OF_RANGE = "unix_millis does not fit the 60-bit v6 timestamp field"
|
|
25
|
+
# One copy of the out-of-range message for the Fiddle backend and for the doors' own
|
|
26
|
+
# range check in hyperuuid.rb; the Magnus extension (rust/src/ruby_ext.rs) carries the
|
|
27
|
+
# same text, and spec/native_backend_spec.rb pins that the two agree.
|
|
29
28
|
V7_TIMESTAMP_OUT_OF_RANGE = "unix_millis must fit within the RFC 9562 48-bit field"
|
|
30
29
|
|
|
30
|
+
# The two batch-size refusals, shared the same way: a version 7 batch past the 26-bit
|
|
31
|
+
# counter space (the core's return code 4, which the doors in hyperuuid.rb refuse before
|
|
32
|
+
# either backend sees it), and a batch whose byte length the platform cannot address
|
|
33
|
+
# (code 3, reachable only on a 32-bit target such as ruby.wasm). Both are argument
|
|
34
|
+
# errors, never a random source failure.
|
|
35
|
+
V7_BATCH_TOO_LARGE = "a single version 7 batch takes at most 67108864 UUIDs (the 26-bit counter space)"
|
|
36
|
+
BATCH_TOO_LARGE_TO_ADDRESS = "the batch is too large to address on this platform"
|
|
37
|
+
|
|
38
|
+
# The SQL Server order conversions' one refusal, in the core's own words (the Magnus
|
|
39
|
+
# extension raises its SqlOrderError's text): a value that isn't a UUID of the
|
|
40
|
+
# conversion's version (code 5), an ArgumentError. Every version 7 converts.
|
|
41
|
+
SQL_ORDER_WRONG_VERSION = "the value is not a UUID of the version this conversion takes"
|
|
42
|
+
|
|
43
|
+
# The core's v7::MAX_BATCH, which hyperuuid.rb publishes as HyperUuid::MAX_V7_BATCH.
|
|
44
|
+
MAX_V7_BATCH = 1 << 26
|
|
45
|
+
|
|
31
46
|
NATIVE_DIR = File.join(__dir__, "native")
|
|
32
47
|
|
|
33
48
|
@mutex = Mutex.new
|
|
@@ -54,32 +69,6 @@ module HyperUuid
|
|
|
54
69
|
out[0, 16]
|
|
55
70
|
end
|
|
56
71
|
|
|
57
|
-
def new_v6(unix_millis)
|
|
58
|
-
out = scratch
|
|
59
|
-
rc = functions[:new_v6].call(unix_millis, out)
|
|
60
|
-
case rc
|
|
61
|
-
when 0 then out[0, 16]
|
|
62
|
-
when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
|
|
63
|
-
else raise random_source_failure("uuid_new_v6")
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def v6_unix_millis(bytes)
|
|
68
|
-
functions[:v6_unix_millis].call(bytes)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def new_v6_batch(count, unix_millis)
|
|
72
|
-
return "" if count.zero?
|
|
73
|
-
|
|
74
|
-
out = buffer(count * 16)
|
|
75
|
-
rc = functions[:new_v6_batch].call(unix_millis, count, out)
|
|
76
|
-
case rc
|
|
77
|
-
when 0 then out[0, count * 16]
|
|
78
|
-
when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
|
|
79
|
-
else raise random_source_failure("uuid_new_v6_batch")
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
72
|
def new_v7(unix_millis)
|
|
84
73
|
out = scratch
|
|
85
74
|
rc = functions[:new_v7].call(unix_millis, out)
|
|
@@ -96,37 +85,65 @@ module HyperUuid
|
|
|
96
85
|
|
|
97
86
|
def new_v7_batch(count, unix_millis)
|
|
98
87
|
return "" if count.zero?
|
|
88
|
+
# Before the buffer: a count past the counter space would otherwise allocate up to
|
|
89
|
+
# 64 GiB only for the core to refuse it with code 4.
|
|
90
|
+
raise ArgumentError, V7_BATCH_TOO_LARGE if count > MAX_V7_BATCH
|
|
99
91
|
|
|
100
92
|
out = buffer(count * 16)
|
|
101
93
|
rc = functions[:new_v7_batch].call(unix_millis, count, out)
|
|
102
94
|
case rc
|
|
103
95
|
when 0 then out[0, count * 16]
|
|
104
96
|
when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
|
|
97
|
+
when 3 then raise ArgumentError, BATCH_TOO_LARGE_TO_ADDRESS
|
|
98
|
+
when 4 then raise ArgumentError, V7_BATCH_TOO_LARGE
|
|
105
99
|
else raise random_source_failure("uuid_new_v7_batch")
|
|
106
100
|
end
|
|
107
101
|
end
|
|
108
102
|
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
# The inspection exports. +layout+ is the core's layout code (1 RFC 9562, 2 SQL Server),
|
|
104
|
+
# which Uuid has already validated; +bytes+ are 16, held in that layout's order.
|
|
105
|
+
def version(bytes, layout)
|
|
106
|
+
functions[:version].call(bytes, layout)
|
|
111
107
|
end
|
|
112
108
|
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
# The core's variant code: 1 NCS, 2 RFC 9562, 3 Microsoft, 4 future (Uuid#variant maps
|
|
110
|
+
# it to a Symbol, for both backends).
|
|
111
|
+
def variant(bytes)
|
|
112
|
+
functions[:variant].call(bytes)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# +version+ is 0-15 here: Uuid#rfc? answers false for anything else without asking.
|
|
116
|
+
def is_rfc(bytes, version, layout)
|
|
117
|
+
functions[:is_rfc].call(bytes, version, layout) == 1
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The Unix ms of an RFC 9562 version 7 UUID held in +layout+'s order, or nil for
|
|
121
|
+
# anything else — another version (a version 6 or 1 included), another variant. One
|
|
122
|
+
# core call; the version it answers with is not needed above Runtime, so it is dropped
|
|
123
|
+
# here.
|
|
124
|
+
def get_timestamp(bytes, layout)
|
|
125
|
+
out = millis_scratch
|
|
126
|
+
version = functions[:get_timestamp].call(bytes, layout, out)
|
|
127
|
+
version.zero? ? nil : out[0, 8].unpack1("Q")
|
|
115
128
|
end
|
|
116
129
|
|
|
117
|
-
def
|
|
118
|
-
|
|
130
|
+
def v7_unix_millis_in(bytes, layout)
|
|
131
|
+
functions[:v7_unix_millis_in].call(bytes, layout)
|
|
119
132
|
end
|
|
120
133
|
|
|
121
|
-
def
|
|
122
|
-
rewrite(:
|
|
134
|
+
def v7_to_sql_order(bytes)
|
|
135
|
+
rewrite(:v7_to_sql_order, bytes)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def v7_to_rfc_order(bytes)
|
|
139
|
+
rewrite(:v7_to_rfc_order, bytes)
|
|
123
140
|
end
|
|
124
141
|
|
|
125
142
|
# The loaded core's version word, major << 16 | minor << 8 | patch, straight from the
|
|
126
143
|
# library's zero-argument hyperuuid_version export. HyperUuid.native_version unpacks
|
|
127
144
|
# it; like every method here, each backend replaces this one in place.
|
|
128
145
|
def packed_version
|
|
129
|
-
functions[:
|
|
146
|
+
functions[:packed_version].call
|
|
130
147
|
end
|
|
131
148
|
|
|
132
149
|
# The one failure every generating export shares, with the one message every backend
|
|
@@ -187,6 +204,12 @@ module HyperUuid
|
|
|
187
204
|
Thread.current[:hyperuuid_scratch] ||= buffer(16)
|
|
188
205
|
end
|
|
189
206
|
|
|
207
|
+
# The u64 get_timestamp writes, per thread for the same reason as #scratch, and apart from
|
|
208
|
+
# it so the UUID bytes passed in are never the buffer being written.
|
|
209
|
+
def millis_scratch
|
|
210
|
+
Thread.current[:hyperuuid_millis_scratch] ||= buffer(8)
|
|
211
|
+
end
|
|
212
|
+
|
|
190
213
|
# Every Fiddle allocation goes through here, and loads the library first: that is what
|
|
191
214
|
# keeps a missing library reported as missing_library_message instead of as whatever
|
|
192
215
|
# touching Fiddle raises first. The scratch buffer pays for it once per thread.
|
|
@@ -196,12 +219,16 @@ module HyperUuid
|
|
|
196
219
|
end
|
|
197
220
|
|
|
198
221
|
# The in-place byte-order rewrites are the one shape that must copy in: the native
|
|
199
|
-
# call genuinely mutates the buffer, and the input String is frozen.
|
|
222
|
+
# call genuinely mutates the buffer, and the input String is frozen. A refusal leaves
|
|
223
|
+
# the buffer as it was and raises what the Magnus extension raises for it.
|
|
200
224
|
def rewrite(symbol, bytes)
|
|
201
225
|
buf = scratch
|
|
202
226
|
buf[0, 16] = bytes
|
|
203
|
-
functions[symbol].call(buf)
|
|
204
|
-
buf[0, 16]
|
|
227
|
+
case (rc = functions[symbol].call(buf))
|
|
228
|
+
when 0 then buf[0, 16]
|
|
229
|
+
when 5 then raise ArgumentError, SQL_ORDER_WRONG_VERSION
|
|
230
|
+
else raise "uuid_#{symbol}: unexpected return code #{rc}"
|
|
231
|
+
end
|
|
205
232
|
end
|
|
206
233
|
|
|
207
234
|
# Loaded lazily and exactly once, mirroring Java's class initializer / Swift's lazy
|
|
@@ -224,21 +251,6 @@ module HyperUuid
|
|
|
224
251
|
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
|
|
225
252
|
Fiddle::TYPE_INT
|
|
226
253
|
),
|
|
227
|
-
new_v6: Fiddle::Function.new(
|
|
228
|
-
handle["uuid_new_v6"],
|
|
229
|
-
[Fiddle::TYPE_UINT64_T, Fiddle::TYPE_VOIDP],
|
|
230
|
-
Fiddle::TYPE_INT
|
|
231
|
-
),
|
|
232
|
-
v6_unix_millis: Fiddle::Function.new(
|
|
233
|
-
handle["uuid_v6_unix_millis"],
|
|
234
|
-
[Fiddle::TYPE_VOIDP],
|
|
235
|
-
Fiddle::TYPE_UINT64_T
|
|
236
|
-
),
|
|
237
|
-
new_v6_batch: Fiddle::Function.new(
|
|
238
|
-
handle["uuid_new_v6_batch"],
|
|
239
|
-
[Fiddle::TYPE_UINT64_T, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
|
|
240
|
-
Fiddle::TYPE_INT
|
|
241
|
-
),
|
|
242
254
|
new_v7: Fiddle::Function.new(
|
|
243
255
|
handle["uuid_new_v7"],
|
|
244
256
|
[Fiddle::TYPE_UINT64_T, Fiddle::TYPE_VOIDP],
|
|
@@ -257,25 +269,36 @@ module HyperUuid
|
|
|
257
269
|
v7_to_sql_order: Fiddle::Function.new(
|
|
258
270
|
handle["uuid_v7_to_sql_order"],
|
|
259
271
|
[Fiddle::TYPE_VOIDP],
|
|
260
|
-
Fiddle::
|
|
272
|
+
Fiddle::TYPE_INT
|
|
261
273
|
),
|
|
262
274
|
v7_to_rfc_order: Fiddle::Function.new(
|
|
263
275
|
handle["uuid_v7_to_rfc_order"],
|
|
264
276
|
[Fiddle::TYPE_VOIDP],
|
|
265
|
-
Fiddle::
|
|
277
|
+
Fiddle::TYPE_INT
|
|
266
278
|
),
|
|
267
|
-
|
|
268
|
-
handle["
|
|
269
|
-
[Fiddle::TYPE_VOIDP],
|
|
270
|
-
Fiddle::
|
|
279
|
+
version: Fiddle::Function.new(
|
|
280
|
+
handle["uuid_version"],
|
|
281
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T],
|
|
282
|
+
Fiddle::TYPE_UINT32_T
|
|
271
283
|
),
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
[
|
|
275
|
-
Fiddle::
|
|
284
|
+
variant: Fiddle::Function.new(handle["uuid_variant"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_UINT32_T),
|
|
285
|
+
is_rfc: Fiddle::Function.new(
|
|
286
|
+
handle["uuid_is_rfc"],
|
|
287
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_UINT32_T],
|
|
288
|
+
Fiddle::TYPE_UINT32_T
|
|
289
|
+
),
|
|
290
|
+
get_timestamp: Fiddle::Function.new(
|
|
291
|
+
handle["uuid_get_timestamp"],
|
|
292
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP],
|
|
293
|
+
Fiddle::TYPE_UINT32_T
|
|
294
|
+
),
|
|
295
|
+
v7_unix_millis_in: Fiddle::Function.new(
|
|
296
|
+
handle["uuid_v7_unix_millis_in"],
|
|
297
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_UINT32_T],
|
|
298
|
+
Fiddle::TYPE_UINT64_T
|
|
276
299
|
),
|
|
277
300
|
# The one export that mints nothing: the zero-argument version probe.
|
|
278
|
-
|
|
301
|
+
packed_version: Fiddle::Function.new(handle["hyperuuid_version"], [], Fiddle::TYPE_UINT32_T),
|
|
279
302
|
}
|
|
280
303
|
end
|
|
281
304
|
end
|
data/lib/hyperuuid/uuid.rb
CHANGED
|
@@ -38,6 +38,17 @@ module HyperUuid
|
|
|
38
38
|
HYPHENATED = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
|
|
39
39
|
private_constant :HYPHENATED
|
|
40
40
|
|
|
41
|
+
# The byte orders a Uuid can hold, as the +layout:+ keyword of #version, #rfc? and
|
|
42
|
+
# #timestamp takes them, mapped to the native core's layout codes. +:rfc9562+ is RFC 9562
|
|
43
|
+
# order, what every other method here takes and returns. +:sql_server+ is the layout
|
|
44
|
+
# #to_sql_order returns, a version 8 that holds a converted version 7.
|
|
45
|
+
LAYOUTS = { rfc9562: 1, sql_server: 2 }.freeze
|
|
46
|
+
|
|
47
|
+
# The Symbols #variant returns, indexed by the native core's variant code (1-4; 0 is
|
|
48
|
+
# reserved and never returned). The one place that code becomes a Symbol, for both backends.
|
|
49
|
+
VARIANTS = [nil, :ncs, :rfc9562, :microsoft, :future].freeze
|
|
50
|
+
private_constant :VARIANTS
|
|
51
|
+
|
|
41
52
|
# Parses an 8-4-4-4-12 hyphenated hex UUID string — exactly the shape #to_s produces, in
|
|
42
53
|
# either case. Nothing else parses: not the bare 32 hex digits, not hyphens anywhere but
|
|
43
54
|
# those four positions, not braces or a `urn:uuid:` prefix.
|
|
@@ -51,14 +62,57 @@ module HyperUuid
|
|
|
51
62
|
new([string.delete("-")].pack("H*"))
|
|
52
63
|
end
|
|
53
64
|
|
|
54
|
-
# The
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
# The version of this UUID, read by the native core in +layout+'s byte order.
|
|
66
|
+
#
|
|
67
|
+
# In +:rfc9562+ (the default) it is the version nibble (bits 48-51, the high nibble of
|
|
68
|
+
# octet 6), 0 through 15: Nil reads as 0 and Max as 15. It says nothing about the variant;
|
|
69
|
+
# #rfc? checks both. In +:sql_server+, the layout #to_sql_order returns, it is the version
|
|
70
|
+
# the value was converted from, 7, when the bytes carry the layout's version 8 and RFC
|
|
71
|
+
# variant, and 0 when they don't. It reads the bytes as given: an
|
|
72
|
+
# RFC-ordered value can happen to carry them (a random v4 does one time in 16), so the
|
|
73
|
+
# layout is the caller's to know.
|
|
74
|
+
#
|
|
75
|
+
# With the default layout, the value is read as RFC 9562 order. A SQL-ordered value — from
|
|
76
|
+
# #to_sql_order, or a +uniqueidentifier+ read back from SQL Server — needs
|
|
77
|
+
# +layout: :sql_server+, because the RFC-order read looks at the wrong bytes there. There
|
|
78
|
+
# is deliberately no layout-agnostic check: whoever holds the value knows its order.
|
|
79
|
+
#
|
|
80
|
+
# @raise [ArgumentError] if +layout+ isn't one of LAYOUTS' keys.
|
|
81
|
+
def version(layout: :rfc9562)
|
|
82
|
+
Runtime.version(bytes, layout_code(layout))
|
|
57
83
|
end
|
|
58
84
|
|
|
59
|
-
# The
|
|
85
|
+
# The variant of this UUID (RFC 9562 §4.1), read by the native core in RFC 9562 order, as a
|
|
86
|
+
# Symbol: +:rfc9562+ (+10xx+, every UUID this gem mints, and the only variant that has
|
|
87
|
+
# versions), +:ncs+ (+0xxx+, Nil among them), +:microsoft+ (+110x+) or +:future+ (+111x+,
|
|
88
|
+
# Max among them).
|
|
89
|
+
#
|
|
90
|
+
# RFC 9562 order only, with no layout form. The SQL Server layout keeps the variant in the
|
|
91
|
+
# same place, so a SQL-ordered value reads +:rfc9562+ here, but that says nothing about
|
|
92
|
+
# which version it holds. To validate one, or a +uniqueidentifier+ read back from SQL
|
|
93
|
+
# Server, use <tt>rfc?(7, layout: :sql_server)</tt>, which checks the layout's version 8
|
|
94
|
+
# and variant.
|
|
60
95
|
def variant
|
|
61
|
-
(
|
|
96
|
+
VARIANTS.fetch(Runtime.variant(bytes))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Whether this is an RFC 9562 UUID of version +version+, read in +layout+'s byte order —
|
|
100
|
+
# the RFC 9562 variant and that version together, the guard to run before trusting a
|
|
101
|
+
# value's version-specific fields. In +:sql_server+ +version+ is the version the value was
|
|
102
|
+
# converted from, so only 7 can be true. A version outside 0-15 is simply false.
|
|
103
|
+
#
|
|
104
|
+
# With the default layout, the value is read as RFC 9562 order. A SQL-ordered value — from
|
|
105
|
+
# #to_sql_order, or a +uniqueidentifier+ read back from SQL Server — needs
|
|
106
|
+
# +layout: :sql_server+, because the RFC-order read looks at the wrong bytes there. There
|
|
107
|
+
# is deliberately no layout-agnostic check: whoever holds the value knows its order.
|
|
108
|
+
#
|
|
109
|
+
# @raise [TypeError] if +version+ isn't an Integer.
|
|
110
|
+
# @raise [ArgumentError] if +layout+ isn't one of LAYOUTS' keys.
|
|
111
|
+
def rfc?(version, layout: :rfc9562)
|
|
112
|
+
raise TypeError, "version must be an Integer; got #{version.class}" unless version.is_a?(Integer)
|
|
113
|
+
|
|
114
|
+
code = layout_code(layout)
|
|
115
|
+
version.between?(0, 15) && Runtime.is_rfc(bytes, version, code)
|
|
62
116
|
end
|
|
63
117
|
|
|
64
118
|
# The 8-4-4-4-12 hyphenated hex string representation.
|
|
@@ -68,92 +122,61 @@ module HyperUuid
|
|
|
68
122
|
end
|
|
69
123
|
alias_method :to_str, :to_s
|
|
70
124
|
|
|
71
|
-
# The UTC timestamp embedded in
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
125
|
+
# The UTC timestamp embedded in an RFC 9562 version 7 UUID, held in +layout+'s byte order:
|
|
126
|
+
# +:rfc9562+ (the default), or +:sql_server+ for a value #to_sql_order returned (the
|
|
127
|
+
# timestamp of the version 7 it was converted from). One native call that checks the
|
|
128
|
+
# variant as well as the version: a 7 in the version nibble under any variant but
|
|
129
|
+
# +:rfc9562+ is not a version 7 UUID, so it has no timestamp (#rfc? is the same check on
|
|
130
|
+
# its own). Every other version has none either, a version 6 or 1 from another generator
|
|
131
|
+
# included.
|
|
132
|
+
#
|
|
133
|
+
# Raises by default for anything that isn't an RFC 9562 version 7 UUID; pass
|
|
134
|
+
# `raise_on_mismatch: false` to get `nil` back instead — for a caller that doesn't already
|
|
135
|
+
# know (or want to separately check) whether this UUID is time-based.
|
|
75
136
|
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
millis
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
else
|
|
85
|
-
raise ArgumentError,
|
|
86
|
-
"timestamp is only defined for version 6 or 7 UUIDs, got version #{version}" if raise_on_mismatch
|
|
87
|
-
|
|
88
|
-
return nil
|
|
89
|
-
end
|
|
90
|
-
Time.at(millis / 1000, millis % 1000, :millisecond).utc
|
|
137
|
+
# @raise [ArgumentError] if this isn't an RFC 9562 version 7 UUID in +layout+ (unless
|
|
138
|
+
# +raise_on_mismatch+ is false), or +layout+ isn't one of LAYOUTS' keys.
|
|
139
|
+
def timestamp(raise_on_mismatch: true, layout: :rfc9562)
|
|
140
|
+
millis = Runtime.get_timestamp(bytes, layout_code(layout))
|
|
141
|
+
return Time.at(millis / 1000, millis % 1000, :millisecond).utc if millis
|
|
142
|
+
return nil unless raise_on_mismatch
|
|
143
|
+
|
|
144
|
+
raise ArgumentError, timestamp_mismatch(layout)
|
|
91
145
|
end
|
|
92
146
|
|
|
93
|
-
# Converts an RFC 9562
|
|
94
|
-
#
|
|
95
|
-
#
|
|
147
|
+
# Converts an RFC 9562 version 7 UUID to the layout SQL Server's `uniqueidentifier` needs
|
|
148
|
+
# on the wire to sort by creation order: an RFC 9562 version 8 UUID (§5.8, the version for
|
|
149
|
+
# custom layouts, where §6.11 sends a byte order other than the network one).
|
|
96
150
|
#
|
|
97
151
|
# `System.Data.SqlTypes.SqlGuid` comparison — and therefore T-SQL `ORDER BY` on a
|
|
98
152
|
# `uniqueidentifier` column — doesn't compare a GUID's 16 bytes left to right; it uses a
|
|
99
153
|
# fixed, non-sequential byte significance order (octets 10,11,12,13,14,15,8,9,6,7,4,5,
|
|
100
|
-
# 0,1,2,3, most significant first).
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
# the
|
|
154
|
+
# 0,1,2,3, most significant first). The layout writes the version nibble 8 at octet 7 and
|
|
155
|
+
# the RFC variant at octet 8, where SQL Server's text form and a .NET `Guid` show them, and
|
|
156
|
+
# fills the other 122 bits in that comparison's order: the 48-bit millisecond timestamp,
|
|
157
|
+
# the 26-bit counter and the 48 random bits as one intact block. A version 7 has exactly
|
|
158
|
+
# those 122 free bits, so every one converts, and #from_sql_order gives back the exact
|
|
159
|
+
# original. Computed once in the native Rust core and verified there (and independently,
|
|
160
|
+
# against the real SqlGuid comparator, in this project's C# test suite); this binding calls
|
|
161
|
+
# the same native functions rather than reimplementing the bit math.
|
|
104
162
|
#
|
|
105
|
-
#
|
|
106
|
-
# order — into that comparison's most-significant bytes, and moves the trailing entropy,
|
|
107
|
-
# which carries no ordering information, into the least-significant ones as one intact
|
|
108
|
-
# block. For v6, which has no monotonic counter the way v7 does, the only field that
|
|
109
|
-
# determines creation order is the 60-bit timestamp itself, so that moves into the most
|
|
110
|
-
# significant bytes instead, with `clock_seq`/`node` (independently random per call, not
|
|
111
|
-
# a counter, so no ordering value either way) relocated into the rest. v6's much simpler
|
|
112
|
-
# byte layout needs no bit-level repacking to do this — just whole-octet-group
|
|
113
|
-
# relocation — unlike v7's, and its version/variant land at different sql-order offsets
|
|
114
|
-
# as a result (octet 8's top nibble / octet 6's top two bits, not 7/8).
|
|
115
|
-
#
|
|
116
|
-
# **v6-specific caveat, unlike v7:** two version 6 UUIDs minted at the same millisecond
|
|
117
|
-
# have identical timestamp bits, so they aren't guaranteed to sort in creation order any
|
|
118
|
-
# more than plain RFC order already does — a pre-existing RFC 9562 v6 limitation, not one
|
|
119
|
-
# this transform introduces.
|
|
120
|
-
#
|
|
121
|
-
# Meaningful only for a genuine version 6 or 7 UUID.
|
|
163
|
+
# @raise [ArgumentError] if this isn't an RFC 9562 version 7 UUID.
|
|
122
164
|
def to_sql_order
|
|
123
|
-
|
|
124
|
-
when 7 then self.class.new(Runtime.v7_to_sql_order(bytes), true)
|
|
125
|
-
when 6 then self.class.new(Runtime.v6_to_sql_order(bytes), true)
|
|
126
|
-
else raise ArgumentError, "to_sql_order is only defined for version 6 or 7 UUIDs, got version #{version}"
|
|
127
|
-
end
|
|
165
|
+
self.class.new(Runtime.v7_to_sql_order(bytes), true)
|
|
128
166
|
end
|
|
129
167
|
|
|
130
|
-
# Inverse of #to_sql_order — converts a SQL-Server-ordered
|
|
131
|
-
#
|
|
168
|
+
# Inverse of #to_sql_order — converts a SQL-Server-ordered value back to the RFC 9562
|
|
169
|
+
# version 7 UUID it was converted from, exactly. Refuses a value without the layout's
|
|
170
|
+
# version 8 and RFC variant, an RFC-ordered version 7 among them.
|
|
132
171
|
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
172
|
+
# The layout is the caller's to know: bytes alone cannot say which order they are in. A
|
|
173
|
+
# value that was never SQL-ordered can still carry the layout's 8 and variant, and is then
|
|
174
|
+
# converted rather than refused (a random RFC-ordered v4 does one time in 16, since its
|
|
175
|
+
# octet 8 already carries the RFC variant and its octet 7 is random).
|
|
137
176
|
#
|
|
138
|
-
#
|
|
139
|
-
# it's deterministic (top nibble always 0x6, by construction), and for v7-sql-order it's
|
|
140
|
-
# also deterministic but structurally excluded from ever reading 0x6 (its top two bits are
|
|
141
|
-
# the fixed variant `10`, so the nibble only ever lands in 0x8-0xB) — no collision either
|
|
142
|
-
# way. Octet 7, by contrast, is *not* safe to check first: for v7-sql-order it's
|
|
143
|
-
# deterministically 0x7, but for v6-sql-order it holds `clock_seq`'s fully random low
|
|
144
|
-
# byte, which has a real (~1-in-16) chance of a top nibble that also happens to read 0x7 —
|
|
145
|
-
# confirmed by an actual test failure during development, not a hypothetical. Checking
|
|
146
|
-
# octet 8 first rules v6 in or out unambiguously before octet 7's reading can matter.
|
|
177
|
+
# @raise [ArgumentError] if this isn't a SQL-ordered value converted from version 7.
|
|
147
178
|
def from_sql_order
|
|
148
|
-
|
|
149
|
-
octet7_version = (bytes.getbyte(7) >> 4) & 0x0F
|
|
150
|
-
if octet8_version == 6
|
|
151
|
-
self.class.new(Runtime.v6_to_rfc_order(bytes), true)
|
|
152
|
-
elsif octet7_version == 7
|
|
153
|
-
self.class.new(Runtime.v7_to_rfc_order(bytes), true)
|
|
154
|
-
else
|
|
155
|
-
raise ArgumentError, "from_sql_order: not a recognized version 6 or 7 SQL-ordered UUID"
|
|
156
|
-
end
|
|
179
|
+
self.class.new(Runtime.v7_to_rfc_order(bytes), true)
|
|
157
180
|
end
|
|
158
181
|
|
|
159
182
|
# Whether +other+ wraps the same 16 raw bytes.
|
|
@@ -178,5 +201,24 @@ module HyperUuid
|
|
|
178
201
|
def inspect
|
|
179
202
|
"#<HyperUuid::Uuid #{self}>"
|
|
180
203
|
end
|
|
204
|
+
|
|
205
|
+
private
|
|
206
|
+
|
|
207
|
+
# Why #timestamp found none: the message is worked out only once it is going to be raised.
|
|
208
|
+
def timestamp_mismatch(layout)
|
|
209
|
+
return "timestamp: not a SQL-ordered version 7 UUID" if layout == :sql_server
|
|
210
|
+
|
|
211
|
+
found = version
|
|
212
|
+
message = "timestamp is only defined for version 7 UUIDs, got version #{found}"
|
|
213
|
+
found == 7 ? "#{message} with the #{variant.inspect} variant" : message
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# The native core's code for +layout+, the one place a layout is checked: an unknown one
|
|
217
|
+
# is refused here, never passed through to the core.
|
|
218
|
+
def layout_code(layout)
|
|
219
|
+
LAYOUTS.fetch(layout) do
|
|
220
|
+
raise ArgumentError, "layout must be one of #{LAYOUTS.keys.map(&:inspect).join(', ')}; got #{layout.inspect}"
|
|
221
|
+
end
|
|
222
|
+
end
|
|
181
223
|
end
|
|
182
224
|
end
|
data/lib/hyperuuid.rb
CHANGED
|
@@ -4,27 +4,42 @@ require_relative "hyperuuid/namespaces"
|
|
|
4
4
|
require_relative "hyperuuid/native_platform"
|
|
5
5
|
require_relative "hyperuuid/runtime"
|
|
6
6
|
|
|
7
|
-
# RFC 9562 UUID v4 (random), v5 (deterministic)
|
|
8
|
-
# Rust core, reached through whichever of two
|
|
9
|
-
# linked straight into a Magnus native extension
|
|
10
|
-
# or the native libhyperuuid shared library
|
|
11
|
-
# one per supported platform, picked by
|
|
12
|
-
# either, and nothing compiled at install
|
|
13
|
-
# is at the bottom of
|
|
7
|
+
# RFC 9562 UUID v4 (random), v5 (deterministic) and v7 (time-sortable) generation, with a
|
|
8
|
+
# version 8 SQL Server order for v7, from one Rust core, reached through whichever of two
|
|
9
|
+
# backends this install can load: the core linked straight into a Magnus native extension
|
|
10
|
+
# (shipped precompiled in the platform gems), or the native libhyperuuid shared library
|
|
11
|
+
# called through Fiddle (the universal gem bundles one per supported platform, picked by
|
|
12
|
+
# HyperUuid::NativePlatform). No runtime bridge in either, and nothing compiled at install
|
|
13
|
+
# time. HyperUuid::BACKEND names the one that loaded; the selection logic is at the bottom of
|
|
14
|
+
# this file.
|
|
14
15
|
#
|
|
15
16
|
# The doors below — with Uuid, Namespaces and the two exception classes — are the public
|
|
16
17
|
# surface, and are shared by every backend, argument validation included: a caller bug raises
|
|
17
18
|
# the same error whichever backend is live.
|
|
18
19
|
module HyperUuid
|
|
19
|
-
# This gem's own version — distinct from the RFC 9562 UUID *versions* (v4/v5/
|
|
20
|
-
#
|
|
21
|
-
VERSION = "0.
|
|
20
|
+
# This gem's own version — distinct from the RFC 9562 UUID *versions* (v4/v5/v7) the rest
|
|
21
|
+
# of this module generates.
|
|
22
|
+
VERSION = "0.9.0"
|
|
22
23
|
|
|
23
24
|
# The widest batch count, and the longest v5 name in bytes, the native ABI carries (a u32).
|
|
24
25
|
# (The millisecond count is a u64; unix_millis_from refuses anything wider.)
|
|
25
26
|
U32_MAX = 0xFFFF_FFFF
|
|
26
27
|
private_constant :U32_MAX
|
|
27
28
|
|
|
29
|
+
# The most UUIDs one version 7 batch (#new_v7_batch, #new_v7_batch_bytes) mints:
|
|
30
|
+
# 67,108,864, the size of the 26-bit counter that orders UUIDs within a millisecond. A larger
|
|
31
|
+
# count is refused with ArgumentError before anything is allocated.
|
|
32
|
+
#
|
|
33
|
+
# Every batch up to this size is in strictly increasing order: the counter is one
|
|
34
|
+
# process-wide sequence, so a batch can straddle the point where it wraps back to 0, and the
|
|
35
|
+
# UUIDs from there on carry a timestamp one millisecond later than the one supplied.
|
|
36
|
+
# The roll-forward orders one batch, not the stream: the next batch or single #new_v7 in
|
|
37
|
+
# the same real millisecond starts its counter just past the wrap and carries the supplied
|
|
38
|
+
# timestamp, so it sorts before this batch's tail, which was stamped a millisecond later.
|
|
39
|
+
# Two single #new_v7 calls either side of the wrap in one millisecond sort in reverse the
|
|
40
|
+
# same way. It happens at most once per 2**26 UUIDs the process mints.
|
|
41
|
+
MAX_V7_BATCH = Runtime::MAX_V7_BATCH
|
|
42
|
+
|
|
28
43
|
# Creates a random UUID version 4 (RFC 9562 §5.4).
|
|
29
44
|
def self.new_v4
|
|
30
45
|
Uuid.new(Runtime.new_v4, true)
|
|
@@ -53,7 +68,7 @@ module HyperUuid
|
|
|
53
68
|
# Converts +value+ to a Unix-epoch millisecond integer: +nil+ becomes the current time, a
|
|
54
69
|
# +Time+ is converted exactly (via its own Rational seconds, avoiding float rounding), and
|
|
55
70
|
# an Integer millisecond count passes through unchanged. Shared by every
|
|
56
|
-
# `
|
|
71
|
+
# `new_v7`/batch door below so a caller can pass either a `Time` or a raw
|
|
57
72
|
# millisecond count interchangeably.
|
|
58
73
|
#
|
|
59
74
|
# This is also where a caller bug is caught once, for every backend: anything else is a
|
|
@@ -87,29 +102,6 @@ module HyperUuid
|
|
|
87
102
|
count
|
|
88
103
|
end
|
|
89
104
|
|
|
90
|
-
# Creates a time-sortable UUID version 6 (RFC 9562 §5.6), a field-compatible reordering of
|
|
91
|
-
# version 1 for better sort/index locality. Defaults to the current time; pass an explicit
|
|
92
|
-
# `Time` or Unix-epoch millisecond integer to embed a specific time instead. `clock_seq` and
|
|
93
|
-
# `node` are randomly generated on every call — unlike version 7, there is no monotonic
|
|
94
|
-
# counter, so calls within the same millisecond are not guaranteed to sort in creation order.
|
|
95
|
-
#
|
|
96
|
-
# @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
|
|
97
|
-
# @raise [TypeError] if +unix_millis+ isn't a Time, an Integer or nil.
|
|
98
|
-
def self.new_v6(unix_millis = nil)
|
|
99
|
-
Uuid.new(Runtime.new_v6(unix_millis_from(unix_millis, Runtime::V6_TIMESTAMP_OUT_OF_RANGE)), true)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Creates `count` time-sortable version 6 UUIDs sharing one timestamp capture — one FFI call
|
|
103
|
-
# and one random-bytes fetch instead of `count` of each. Defaults to the current time; pass
|
|
104
|
-
# an explicit `Time` or Unix-epoch millisecond integer to embed a specific time instead.
|
|
105
|
-
#
|
|
106
|
-
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
|
|
107
|
-
# @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
|
|
108
|
-
def self.new_v6_batch(count, unix_millis = nil)
|
|
109
|
-
bytes = new_v6_batch_bytes(count, unix_millis)
|
|
110
|
-
Array.new(count) { |i| Uuid.new(bytes[i * 16, 16], true) }
|
|
111
|
-
end
|
|
112
|
-
|
|
113
105
|
# Creates a time-sortable UUID version 7 (RFC 9562 §6.2). Defaults to the current time; pass
|
|
114
106
|
# an explicit `Time` or Unix-epoch millisecond integer (non-negative, fitting in 48 bits) to
|
|
115
107
|
# embed a specific time instead.
|
|
@@ -125,7 +117,18 @@ module HyperUuid
|
|
|
125
117
|
# instead of `count` of each. Defaults to the current time; pass an explicit `Time` or
|
|
126
118
|
# Unix-epoch millisecond integer to embed a specific time instead.
|
|
127
119
|
#
|
|
128
|
-
#
|
|
120
|
+
# Every batch is in strictly increasing order. The counter is one process-wide sequence, so
|
|
121
|
+
# a batch can straddle the point where it wraps back to 0; the UUIDs from there on carry a
|
|
122
|
+
# timestamp one millisecond later than the one supplied rather than sorting before the ones
|
|
123
|
+
# ahead of them. That is also why one batch takes at most MAX_V7_BATCH.
|
|
124
|
+
#
|
|
125
|
+
# The roll-forward orders one batch, not the stream: the next batch or single #new_v7 in
|
|
126
|
+
# the same real millisecond starts its counter just past the wrap and carries the supplied
|
|
127
|
+
# timestamp, so it sorts before this batch's tail, which was stamped a millisecond later.
|
|
128
|
+
# Two single #new_v7 calls either side of the wrap in one millisecond sort in reverse the
|
|
129
|
+
# same way. It happens at most once per 2**26 UUIDs the process mints.
|
|
130
|
+
#
|
|
131
|
+
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and MAX_V7_BATCH.
|
|
129
132
|
# @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
|
|
130
133
|
def self.new_v7_batch(count, unix_millis = nil)
|
|
131
134
|
bytes = new_v7_batch_bytes(count, unix_millis)
|
|
@@ -145,28 +148,17 @@ module HyperUuid
|
|
|
145
148
|
# format, a bulk COPY. If you need Uuid objects, keep using #new_v7_batch — slicing this
|
|
146
149
|
# String into them yourself just moves the same allocations into your own code.
|
|
147
150
|
#
|
|
148
|
-
# Slice it with `bytes[i * 16, 16]`, which is what #new_v7_batch does internally.
|
|
151
|
+
# Slice it with `bytes[i * 16, 16]`, which is what #new_v7_batch does internally. The
|
|
152
|
+
# order, the possible extra millisecond and the MAX_V7_BATCH limit are #new_v7_batch's.
|
|
149
153
|
#
|
|
150
|
-
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and
|
|
154
|
+
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and MAX_V7_BATCH.
|
|
151
155
|
# @raise [TimestampOutOfRangeError] if the time is negative or past the 48-bit v7 field.
|
|
152
156
|
def self.new_v7_batch_bytes(count, unix_millis = nil)
|
|
153
|
-
count
|
|
154
|
-
Runtime.
|
|
155
|
-
end
|
|
157
|
+
# First, so any count past the limit (2**32 and beyond included) names the limit itself.
|
|
158
|
+
raise ArgumentError, Runtime::V7_BATCH_TOO_LARGE if count.is_a?(Integer) && count > MAX_V7_BATCH
|
|
156
159
|
|
|
157
|
-
# Returns `count` version 6 UUIDs as one binary String of raw RFC 9562-ordered bytes,
|
|
158
|
-
# 16 per UUID. The version 6 counterpart to #new_v7_batch_bytes, with the same rationale and
|
|
159
|
-
# the same guidance about when it is the right call.
|
|
160
|
-
#
|
|
161
|
-
# clock_seq and node are independently random per item; unlike version 7 there is no
|
|
162
|
-
# monotonic counter, so items minted in the same millisecond are not guaranteed to sort in
|
|
163
|
-
# creation order.
|
|
164
|
-
#
|
|
165
|
-
# @raise [TypeError, ArgumentError] if +count+ isn't an Integer between 0 and 2**32 - 1.
|
|
166
|
-
# @raise [TimestampOutOfRangeError] if the time is negative or past the 60-bit v6 field.
|
|
167
|
-
def self.new_v6_batch_bytes(count, unix_millis = nil)
|
|
168
160
|
count = batch_count(count)
|
|
169
|
-
Runtime.
|
|
161
|
+
Runtime.new_v7_batch(count, unix_millis_from(unix_millis, Runtime::V7_TIMESTAMP_OUT_OF_RANGE))
|
|
170
162
|
end
|
|
171
163
|
|
|
172
164
|
# The version of the native core actually loaded, as "major.minor.patch" — read from the
|