hyperuuid 0.2.1 → 0.4.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 +331 -88
- data/lib/hyperuuid/errors.rb +12 -0
- 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/wasm32-wasip1/hyperuuid.wasm +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/native_platform.rb +13 -7
- data/lib/hyperuuid/runtime.rb +70 -20
- data/lib/hyperuuid/uuid.rb +32 -13
- data/lib/hyperuuid/wasm_runtime.rb +259 -0
- data/lib/hyperuuid.rb +166 -35
- metadata +19 -23
- data/lib/hyperuuid/native/README.md +0 -6
data/lib/hyperuuid/runtime.rb
CHANGED
|
@@ -12,8 +12,18 @@ module HyperUuid
|
|
|
12
12
|
# files are already plain files on disk once installed — native/{rid}/{lib} can be
|
|
13
13
|
# dlopen'd directly, no extraction step needed.
|
|
14
14
|
module Runtime
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
# The package's two exceptions live on HyperUuid itself (errors.rb). These are the names
|
|
16
|
+
# they had through 0.3.0, kept as aliases of the very same classes so a
|
|
17
|
+
# `rescue HyperUuid::Runtime::TimestampOutOfRangeError` written against an earlier
|
|
18
|
+
# release still catches.
|
|
19
|
+
RandomSourceError = HyperUuid::RandomSourceError
|
|
20
|
+
TimestampOutOfRangeError = HyperUuid::TimestampOutOfRangeError
|
|
21
|
+
|
|
22
|
+
# One copy of each out-of-range message for the Fiddle and wasm backends and for the
|
|
23
|
+
# doors' own range check in hyperuuid.rb; the Magnus extension (rust/src/ruby_ext.rs)
|
|
24
|
+
# carries the same text, and spec/native_backend_spec.rb pins that the two agree.
|
|
25
|
+
V6_TIMESTAMP_OUT_OF_RANGE = "unix_millis does not fit the 60-bit v6 timestamp field"
|
|
26
|
+
V7_TIMESTAMP_OUT_OF_RANGE = "unix_millis must fit within the RFC 9562 48-bit field"
|
|
17
27
|
|
|
18
28
|
NATIVE_DIR = File.join(__dir__, "native")
|
|
19
29
|
|
|
@@ -24,17 +34,18 @@ module HyperUuid
|
|
|
24
34
|
def new_v4
|
|
25
35
|
out = scratch
|
|
26
36
|
rc = functions[:new_v4].call(out)
|
|
27
|
-
raise
|
|
37
|
+
raise random_source_failure("uuid_new_v4") unless rc.zero?
|
|
28
38
|
out[0, 16]
|
|
29
39
|
end
|
|
30
40
|
|
|
31
41
|
def new_v5(namespace_bytes, name_bytes)
|
|
32
42
|
out = scratch
|
|
33
|
-
# Fiddle passes a String's bytes for void*
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
# Fiddle passes a String's own bytes for void* (read-only, no copy of them) — the
|
|
44
|
+
# same crossing every other input here uses. An empty name crosses as it is, a
|
|
45
|
+
# valid pointer with length 0, never as nil: NULL is not part of this export's
|
|
46
|
+
# contract.
|
|
47
|
+
rc = functions[:new_v5].call(namespace_bytes, name_bytes, name_bytes.bytesize, out)
|
|
48
|
+
raise random_source_failure("uuid_new_v5") unless rc.zero?
|
|
38
49
|
out[0, 16]
|
|
39
50
|
end
|
|
40
51
|
|
|
@@ -43,8 +54,8 @@ module HyperUuid
|
|
|
43
54
|
rc = functions[:new_v6].call(unix_millis, out)
|
|
44
55
|
case rc
|
|
45
56
|
when 0 then out[0, 16]
|
|
46
|
-
when 2 then raise TimestampOutOfRangeError,
|
|
47
|
-
else raise
|
|
57
|
+
when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
|
|
58
|
+
else raise random_source_failure("uuid_new_v6")
|
|
48
59
|
end
|
|
49
60
|
end
|
|
50
61
|
|
|
@@ -58,8 +69,8 @@ module HyperUuid
|
|
|
58
69
|
rc = functions[:new_v6_batch].call(unix_millis, count, out)
|
|
59
70
|
case rc
|
|
60
71
|
when 0 then out[0, count * 16]
|
|
61
|
-
when 2 then raise TimestampOutOfRangeError,
|
|
62
|
-
else raise
|
|
72
|
+
when 2 then raise TimestampOutOfRangeError, V6_TIMESTAMP_OUT_OF_RANGE
|
|
73
|
+
else raise random_source_failure("uuid_new_v6_batch")
|
|
63
74
|
end
|
|
64
75
|
end
|
|
65
76
|
|
|
@@ -68,8 +79,8 @@ module HyperUuid
|
|
|
68
79
|
rc = functions[:new_v7].call(unix_millis, out)
|
|
69
80
|
case rc
|
|
70
81
|
when 0 then out[0, 16]
|
|
71
|
-
when 2 then raise TimestampOutOfRangeError,
|
|
72
|
-
else raise
|
|
82
|
+
when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
|
|
83
|
+
else raise random_source_failure("uuid_new_v7")
|
|
73
84
|
end
|
|
74
85
|
end
|
|
75
86
|
|
|
@@ -83,8 +94,8 @@ module HyperUuid
|
|
|
83
94
|
rc = functions[:new_v7_batch].call(unix_millis, count, out)
|
|
84
95
|
case rc
|
|
85
96
|
when 0 then out[0, count * 16]
|
|
86
|
-
when 2 then raise TimestampOutOfRangeError,
|
|
87
|
-
else raise
|
|
97
|
+
when 2 then raise TimestampOutOfRangeError, V7_TIMESTAMP_OUT_OF_RANGE
|
|
98
|
+
else raise random_source_failure("uuid_new_v7_batch")
|
|
88
99
|
end
|
|
89
100
|
end
|
|
90
101
|
|
|
@@ -104,8 +115,44 @@ module HyperUuid
|
|
|
104
115
|
rewrite(:v6_to_rfc_order, bytes)
|
|
105
116
|
end
|
|
106
117
|
|
|
118
|
+
# The loaded core's version word, major << 16 | minor << 8 | patch, straight from the
|
|
119
|
+
# library's zero-argument hyperuuid_version export. HyperUuid.native_version unpacks
|
|
120
|
+
# it; like every method here, each backend replaces this one in place.
|
|
121
|
+
def packed_version
|
|
122
|
+
functions[:version].call
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The one failure every generating export shares, with the one message every backend
|
|
126
|
+
# raises for it: the export's name, and that the system's random source failed.
|
|
127
|
+
def random_source_failure(export)
|
|
128
|
+
RandomSourceError.new("#{export}: the system random source failed")
|
|
129
|
+
end
|
|
130
|
+
|
|
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
|
+
|
|
107
141
|
private
|
|
108
142
|
|
|
143
|
+
# The shared library to dlopen: this install's native/{rid}/{lib}, or — the
|
|
144
|
+
# development loop — the in-repo cargo build, exactly what the other bindings' local
|
|
145
|
+
# staging does (HyperCast's runtime has had this since its first release). Nil when
|
|
146
|
+
# neither exists.
|
|
147
|
+
def library_path
|
|
148
|
+
rid, lib_name = NativePlatform.rid_and_library_name
|
|
149
|
+
path = File.join(NATIVE_DIR, rid, lib_name)
|
|
150
|
+
return path if File.exist?(path)
|
|
151
|
+
|
|
152
|
+
repo_build = File.expand_path(File.join(__dir__, "../../../rust/target/release", lib_name))
|
|
153
|
+
File.exist?(repo_build) ? repo_build : nil
|
|
154
|
+
end
|
|
155
|
+
|
|
109
156
|
# One 16-byte scratch allocation per thread, reused by every single-item call —
|
|
110
157
|
# Fiddle::Pointer.malloc(..., RUBY_FREE) registers a GC finalizer per call, measured
|
|
111
158
|
# (in HyperCast, same mechanism) as the dominant per-call cost by an order of
|
|
@@ -132,11 +179,12 @@ module HyperUuid
|
|
|
132
179
|
end
|
|
133
180
|
|
|
134
181
|
def load_functions
|
|
135
|
-
|
|
136
|
-
path
|
|
137
|
-
|
|
182
|
+
path = library_path
|
|
183
|
+
if path.nil?
|
|
184
|
+
rid, lib_name = NativePlatform.rid_and_library_name
|
|
138
185
|
raise LoadError,
|
|
139
|
-
"hyperuuid: #{
|
|
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)"
|
|
140
188
|
end
|
|
141
189
|
|
|
142
190
|
handle = Fiddle.dlopen(path)
|
|
@@ -197,6 +245,8 @@ module HyperUuid
|
|
|
197
245
|
[Fiddle::TYPE_VOIDP],
|
|
198
246
|
Fiddle::TYPE_VOID
|
|
199
247
|
),
|
|
248
|
+
# The one export that mints nothing: the zero-argument version probe.
|
|
249
|
+
version: Fiddle::Function.new(handle["hyperuuid_version"], [], Fiddle::TYPE_UINT32_T),
|
|
200
250
|
}
|
|
201
251
|
end
|
|
202
252
|
end
|
data/lib/hyperuuid/uuid.rb
CHANGED
|
@@ -9,12 +9,22 @@ module HyperUuid
|
|
|
9
9
|
# The UUID's 16 raw bytes in RFC 9562 (big-endian) order.
|
|
10
10
|
attr_reader :bytes
|
|
11
11
|
|
|
12
|
-
# Wraps a raw 16-byte RFC 9562 (big-endian) UUID value.
|
|
12
|
+
# Wraps a raw 16-byte RFC 9562 (big-endian) UUID value. The bytes are copied, so the
|
|
13
|
+
# String passed in stays the caller's.
|
|
14
|
+
#
|
|
15
|
+
# +owned+ is for this gem's own use: it says +bytes+ is a String the native core has
|
|
16
|
+
# just produced — sixteen bytes, already ASCII-8BIT, held by nothing else — so there is
|
|
17
|
+
# nothing to check, copy or re-tag, and that copy is most of what constructing one costs.
|
|
13
18
|
#
|
|
14
19
|
# @raise [ArgumentError] if +bytes+ isn't exactly 16 bytes.
|
|
15
|
-
def initialize(bytes)
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
def initialize(bytes, owned = false)
|
|
21
|
+
if owned
|
|
22
|
+
@bytes = bytes.freeze
|
|
23
|
+
else
|
|
24
|
+
raise ArgumentError, "bytes must be exactly 16 bytes" unless bytes.bytesize == 16
|
|
25
|
+
|
|
26
|
+
@bytes = bytes.dup.force_encoding(Encoding::BINARY).freeze
|
|
27
|
+
end
|
|
18
28
|
end
|
|
19
29
|
|
|
20
30
|
# The RFC 9562 §5.9 Nil UUID — all 128 bits zero.
|
|
@@ -23,13 +33,22 @@ module HyperUuid
|
|
|
23
33
|
# The RFC 9562 §5.10 Max UUID — all 128 bits one.
|
|
24
34
|
MAX = new(("\xFF" * 16).b).freeze
|
|
25
35
|
|
|
26
|
-
#
|
|
36
|
+
# The one text shape .parse accepts: 8-4-4-4-12 hex digits, either case, the four hyphens
|
|
37
|
+
# exactly where #to_s puts them.
|
|
38
|
+
HYPHENATED = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
|
|
39
|
+
private_constant :HYPHENATED
|
|
40
|
+
|
|
41
|
+
# Parses an 8-4-4-4-12 hyphenated hex UUID string — exactly the shape #to_s produces, in
|
|
42
|
+
# either case. Nothing else parses: not the bare 32 hex digits, not hyphens anywhere but
|
|
43
|
+
# those four positions, not braces or a `urn:uuid:` prefix.
|
|
27
44
|
#
|
|
28
|
-
# @raise [ArgumentError] if +string+ isn't a
|
|
45
|
+
# @raise [ArgumentError] if +string+ isn't a String in that shape.
|
|
29
46
|
def self.parse(string)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
unless string.is_a?(String) && string.match?(HYPHENATED)
|
|
48
|
+
raise ArgumentError, "invalid UUID string: #{string.inspect}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
new([string.delete("-")].pack("H*"))
|
|
33
52
|
end
|
|
34
53
|
|
|
35
54
|
# The RFC 9562 version nibble (bits 48-51, the high nibble of octet 6).
|
|
@@ -100,8 +119,8 @@ module HyperUuid
|
|
|
100
119
|
# Meaningful only for a genuine version 6 or 7 UUID.
|
|
101
120
|
def to_sql_order
|
|
102
121
|
case version
|
|
103
|
-
when 7 then self.class.new(Runtime.v7_to_sql_order(bytes))
|
|
104
|
-
when 6 then self.class.new(Runtime.v6_to_sql_order(bytes))
|
|
122
|
+
when 7 then self.class.new(Runtime.v7_to_sql_order(bytes), true)
|
|
123
|
+
when 6 then self.class.new(Runtime.v6_to_sql_order(bytes), true)
|
|
105
124
|
else raise ArgumentError, "to_sql_order is only defined for version 6 or 7 UUIDs, got version #{version}"
|
|
106
125
|
end
|
|
107
126
|
end
|
|
@@ -127,9 +146,9 @@ module HyperUuid
|
|
|
127
146
|
octet8_version = (bytes.getbyte(8) >> 4) & 0x0F
|
|
128
147
|
octet7_version = (bytes.getbyte(7) >> 4) & 0x0F
|
|
129
148
|
if octet8_version == 6
|
|
130
|
-
self.class.new(Runtime.v6_to_rfc_order(bytes))
|
|
149
|
+
self.class.new(Runtime.v6_to_rfc_order(bytes), true)
|
|
131
150
|
elsif octet7_version == 7
|
|
132
|
-
self.class.new(Runtime.v7_to_rfc_order(bytes))
|
|
151
|
+
self.class.new(Runtime.v7_to_rfc_order(bytes), true)
|
|
133
152
|
else
|
|
134
153
|
raise ArgumentError, "from_sql_order: not a recognized version 6 or 7 SQL-ordered UUID"
|
|
135
154
|
end
|
|
@@ -0,0 +1,259 @@
|
|
|
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
|