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.
@@ -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
- class RandomSourceError < StandardError; end
16
- class TimestampOutOfRangeError < StandardError; end
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 RandomSourceError, "uuid_new_v4 failed with code #{rc}" unless rc.zero?
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* directly (read-only) — no Pointer
34
- # wrapper, no copy — the same zero-copy crossing every other input here uses.
35
- name = name_bytes.empty? ? nil : name_bytes
36
- rc = functions[:new_v5].call(namespace_bytes, name, name_bytes.bytesize, out)
37
- raise RandomSourceError, "uuid_new_v5 failed with code #{rc}" unless rc.zero?
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, "unix_millis does not fit the 60-bit v6 timestamp field"
47
- else raise RandomSourceError, "uuid_new_v6 failed with code #{rc}"
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, "unix_millis does not fit the 60-bit v6 timestamp field"
62
- else raise RandomSourceError, "uuid_new_v6_batch failed with code #{rc}"
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, "unix_millis must fit within the RFC 9562 48-bit field"
72
- else raise RandomSourceError, "uuid_new_v7 failed with code #{rc}"
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, "unix_millis must fit within the RFC 9562 48-bit field"
87
- else raise RandomSourceError, "uuid_new_v7_batch failed with code #{rc}"
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
- rid, lib_name = NativePlatform.rid_and_library_name
136
- path = File.join(NATIVE_DIR, rid, lib_name)
137
- unless File.exist?(path)
182
+ path = library_path
183
+ if path.nil?
184
+ rid, lib_name = NativePlatform.rid_and_library_name
138
185
  raise LoadError,
139
- "hyperuuid: #{path} not found (unsupported platform, or this gem was built without a native library for it)"
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
@@ -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
- raise ArgumentError, "bytes must be exactly 16 bytes" unless bytes.bytesize == 16
17
- @bytes = bytes.dup.force_encoding(Encoding::BINARY).freeze
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
- # Parses an 8-4-4-4-12 hyphenated hex UUID string.
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 valid UUID string.
45
+ # @raise [ArgumentError] if +string+ isn't a String in that shape.
29
46
  def self.parse(string)
30
- hex = string.delete("-")
31
- raise ArgumentError, "invalid UUID string: #{string.inspect}" unless hex.match?(/\A[0-9a-fA-F]{32}\z/)
32
- new([hex].pack("H*"))
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