hypercast 0.6.0-aarch64-linux-gnu → 0.7.0-aarch64-linux-gnu
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 +109 -18
- data/lib/hypercast/3.4/hypercast_native.so +0 -0
- data/lib/hypercast/4.0/hypercast_native.so +0 -0
- data/lib/hypercast/interop.rb +118 -0
- data/lib/hypercast/native_platform.rb +8 -6
- data/lib/hypercast/runtime.rb +10 -9
- data/lib/hypercast.rb +80 -63
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a51eb014879c6e776f088c09629a5b840f179fe35013b70c348e88092e3c2c2f
|
|
4
|
+
data.tar.gz: ad2bcce5500a8c90234391676efbceadaa42d5129ce9bd55174fc6fcd3133b20
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9872d130f99bea56c7cac42a2f68ce8f3b4f6fe3cc95fbdc180c48a590cb256f7738102db7aadebd7fb0db8a8212b00f5a93adf4f9f01e1c7c1dc68c765f818d
|
|
7
|
+
data.tar.gz: 36553a1e5cca71b530facf345ecda209b432d0435708a134f96cde1dbfe6fab93989cf4f063b4346af69a77e6fb35d6f47af4befa5911bf667d6fd54602a7d61
|
data/README.md
CHANGED
|
@@ -50,6 +50,10 @@ midnight, durations come back as exact `Rational` seconds across the core's whol
|
|
|
50
50
|
| `datetime(text, order)` | zone-less `DateTime`, exact `Rational` seconds | a field order, as `date` |
|
|
51
51
|
| `time(text)` | `Integer` nanoseconds since midnight | — |
|
|
52
52
|
| `duration(text)` | exact `Rational` seconds | — |
|
|
53
|
+
| `decimal_from_float(value)` | `HyperCast::Decimal`, the shortest that names the number (`0.1` is one tenth) | — |
|
|
54
|
+
| `excel_serial_from_float(value, epoch)` | zone-less `DateTime`, as `datetime` | `:y1900` / `:y1904` |
|
|
55
|
+
| `excel_time(value)` | `Integer` nanoseconds since midnight | — |
|
|
56
|
+
| `excel_duration(value)` | exact `Rational` seconds | — |
|
|
53
57
|
|
|
54
58
|
Every door returns a `Success` or a `Fault`; `HyperCast.optional(verdict)` folds `:empty`
|
|
55
59
|
to `nil`. Beside the doors, `native_version` returns the loaded core's
|
|
@@ -156,6 +160,38 @@ Across the ABI a `NumFormat` is a 32-byte struct — the two separators as code
|
|
|
156
160
|
flags, and the symbol's length and UTF-8 bytes held inline — packed once per format object
|
|
157
161
|
and memoized by identity on every backend, so declaring a currency costs a cast nothing.
|
|
158
162
|
|
|
163
|
+
## Interop: building on HyperCast's C ABI
|
|
164
|
+
|
|
165
|
+
For a gem that carries HyperCast's verdicts across a C ABI of its own, as HyperTabular does:
|
|
166
|
+
it reads the core's out-values out of its own buffers and needs the values the doors would
|
|
167
|
+
have returned. `HyperCast::Interop` is the code the Fiddle backend's doors decode with, so
|
|
168
|
+
the two cannot drift. It is loaded on every backend.
|
|
169
|
+
|
|
170
|
+
- `SCALARS` — the `String#unpack` directive for one value of each scalar door (`:i32` is
|
|
171
|
+
`"l<"`; a bool is the byte 0 or 1).
|
|
172
|
+
- `RECORDS` — for each record door (`:decimal`, `:uuid`, `:timestamp`, `:date`, …), the
|
|
173
|
+
directive that unpacks one value, how many fields that yields, and a lambda
|
|
174
|
+
`(fields, at)` that builds the Ruby value from them. To decode a column, append `"*"` and
|
|
175
|
+
step that many fields at a time.
|
|
176
|
+
- `VALUE_BYTES` — the bytes one value of each door takes.
|
|
177
|
+
- `decode(door, bytes)` — one value, from the bytes the core wrote for it.
|
|
178
|
+
- `fault(code, offset, length)` — the `Fault` a nonzero verdict code names; a code that names
|
|
179
|
+
no reason is a `KeyError`.
|
|
180
|
+
- `characters(bytes, offset, length)` — the core's byte span as the character span
|
|
181
|
+
`String#[]` slices by.
|
|
182
|
+
- `version(word)` — a packed `*_version` word as `"major.minor.patch"`.
|
|
183
|
+
- `library_path(library, native_dir, repo_root)` — the shared library to `dlopen`: the
|
|
184
|
+
staged `native/{rid}/` build, else the in-repo cargo build, else `nil`.
|
|
185
|
+
|
|
186
|
+
`NativePlatform.rid_and_library_name(library: "hypertabular")` gives the `{rid}` directory
|
|
187
|
+
and file name for any SkunkWerkx core; `library:` defaults to `"hypercast"`.
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
HyperCast::Interop.decode(:i32, [-7].pack("l<")) # => -7
|
|
191
|
+
HyperCast::Interop.fault(2, 3, 1) # => Fault, reason :malformed
|
|
192
|
+
HyperCast::Interop.version(0x000700) # => "0.7.0"
|
|
193
|
+
```
|
|
194
|
+
|
|
159
195
|
## Why not `Integer()` / `Time.iso8601` / `Float()`?
|
|
160
196
|
|
|
161
197
|
1. **Verdicts, not exceptions** — bad data is the expected case for untrusted text; a
|
|
@@ -169,12 +205,12 @@ and memoized by identity on every backend, so declaring a currency costs a cast
|
|
|
169
205
|
boundary).
|
|
170
206
|
4. **Faster than the stdlib on the Magnus backend, where the carrier is cheap** —
|
|
171
207
|
benchmark-ips (`ruby benchmark/cast_benchmark.rb`, linux-x64 on an Intel Core
|
|
172
|
-
i9-11900H, Ruby 4.0.7): timestamp **
|
|
208
|
+
i9-11900H, Ruby 4.0.7): timestamp **404 ns vs 2.70 µs `Time.iso8601`** (6.7x) — while
|
|
173
209
|
returning exact `Rational` durations on the duration door. The Fiddle fallback lands at
|
|
174
|
-
|
|
210
|
+
2.85 µs: a little behind `Time.iso8601`, sitting on Fiddle's per-call marshalling floor.
|
|
175
211
|
|
|
176
212
|
Separator detection is nearly free here: `1.234.567,89` under `NumFormat::DETECT` runs
|
|
177
|
-
at
|
|
213
|
+
at 154 ns against 145 ns for the same text under a declared eurozone format. Both cost
|
|
178
214
|
nearly twice that in 0.1.0, for a reason that had nothing to do with parsing: every format other than `INVARIANT` paid three method dispatches and two
|
|
179
215
|
`String` allocations per call to read its separators back out of the `Data`. `DETECT`
|
|
180
216
|
is now identity-matched like `INVARIANT`, and any other format is resolved once per
|
|
@@ -213,14 +249,14 @@ both backends:
|
|
|
213
249
|
|
|
214
250
|
| Door | Magnus | Fiddle |
|
|
215
251
|
|---|---:|---:|
|
|
216
|
-
| `bool` |
|
|
217
|
-
| `i32` |
|
|
218
|
-
| `f64` |
|
|
219
|
-
| `uuid` |
|
|
220
|
-
| `timestamp` |
|
|
221
|
-
| `datetime` (`1/7/2026 3:04 PM`) |
|
|
222
|
-
| `duration` (ISO) |
|
|
223
|
-
| `i32`, a fault |
|
|
252
|
+
| `bool` | 109 ns | 2.30 µs |
|
|
253
|
+
| `i32` | 120 ns | 2.30 µs |
|
|
254
|
+
| `f64` | 134 ns | 2.44 µs |
|
|
255
|
+
| `uuid` | 209 ns | 3.28 µs |
|
|
256
|
+
| `timestamp` | 404 ns | 2.85 µs |
|
|
257
|
+
| `datetime` (`1/7/2026 3:04 PM`) | 877 ns | 3.71 µs |
|
|
258
|
+
| `duration` (ISO) | 578 ns | 2.72 µs |
|
|
259
|
+
| `i32`, a fault | 202 ns | 2.88 µs |
|
|
224
260
|
|
|
225
261
|
A lean door on the Magnus backend is little more than the native call: the extension builds
|
|
226
262
|
the `Success` or `Fault` it returns directly — allocated, its members stored, frozen —
|
|
@@ -230,7 +266,7 @@ rather than through `Data.new`, whose keyword handling alone cost more than the
|
|
|
230
266
|
|
|
231
267
|
| `HyperCast::BACKEND` | What runs | Chosen when |
|
|
232
268
|
|---|---|---|
|
|
233
|
-
| `:native` | the core linked into a Magnus extension | a precompiled platform gem is installed — every one carries an extension for each Ruby it installs on; see [Install](#install) |
|
|
269
|
+
| `:native` | the core linked into a Magnus extension | a precompiled platform gem is installed — every one carries an extension for each Ruby it installs on; see [Install](#install) — or the `hypercast-wasm` gem is linked into a ruby.wasm interpreter; see [Ruby in the browser](#ruby-in-the-browser) |
|
|
234
270
|
| `:fiddle` | `libhypercast` for this platform, `dlopen`ed through Fiddle | no extension loads: the universal gem, on a Ruby or platform no platform gem covers; also the answer when nothing loads at all |
|
|
235
271
|
|
|
236
272
|
Selection happens once, at `require`, in that order.
|
|
@@ -256,8 +292,8 @@ concurrent-callers example runs under both.
|
|
|
256
292
|
|
|
257
293
|
## Verifying provenance
|
|
258
294
|
|
|
259
|
-
Every gem RubyGems.org serves — the universal fallback
|
|
260
|
-
platform gems — carries its own GitHub build-provenance attestation, signed directly by
|
|
295
|
+
Every gem RubyGems.org serves — the universal fallback, each of the seven precompiled
|
|
296
|
+
platform gems and `hypercast-wasm` — carries its own GitHub build-provenance attestation, signed directly by
|
|
261
297
|
this repo's own `release.yml` (the `rubygems-publish` job attests `ruby/pkg/*.gem` right
|
|
262
298
|
before the push), so plain `--repo` verifies any of them:
|
|
263
299
|
|
|
@@ -268,8 +304,9 @@ gh attestation verify hypercast-X.Y.Z-<platform>.gem --repo SkunkWerkx/HyperCast
|
|
|
268
304
|
|
|
269
305
|
That's the release's second layer of checking, not the only one: before any gem gets built,
|
|
270
306
|
the same job verifies every native binary it packs — the Fiddle libraries, the Magnus
|
|
271
|
-
extensions (one per Ruby ABI per platform) — against *their own*
|
|
272
|
-
attestations — those are signed from `SkunkWerkx/.github` by `hyper-build-native.yml
|
|
307
|
+
extensions (one per Ruby ABI per platform), the `wasm32-wasip1` archives — against *their own*
|
|
308
|
+
attestations — those are signed from `SkunkWerkx/.github` by `hyper-build-native.yml` and
|
|
309
|
+
`hyper-build-wasm.yml`, so
|
|
273
310
|
that check needs `--signer-repo SkunkWerkx/.github` added — and refuses to proceed on an
|
|
274
311
|
unverified one.
|
|
275
312
|
RubyGems.org has no unpublish and no duplicate-version overwrite, so this all happens while a
|
|
@@ -285,7 +322,7 @@ more on why `--signer-repo` is needed for some artifacts here and not others.
|
|
|
285
322
|
gem install hypercast
|
|
286
323
|
```
|
|
287
324
|
|
|
288
|
-
|
|
325
|
+
Nine gems are published per release. This section is about eight of them: seven precompiled Magnus platform gems that
|
|
289
326
|
`gem install` and `bundle` auto-select when they match — `x86_64-linux-gnu`,
|
|
290
327
|
`aarch64-linux-gnu`, `x86_64-linux-musl`, `aarch64-linux-musl`, `arm64-darwin`,
|
|
291
328
|
`x64-mingw-ucrt` and `aarch64-mingw-ucrt` — and one universal `ruby`-platform gem. A platform
|
|
@@ -294,7 +331,7 @@ universal gem is the last resort, Fiddle with every platform's native library bu
|
|
|
294
331
|
is what RubyGems resolves for a Ruby the platform gems do not cover (3.3, or a Ruby newer than
|
|
295
332
|
the release, such as 4.1 before a release ships for it) and on a platform no platform gem is
|
|
296
333
|
built for — Intel macOS among them, which runs on Fiddle. No extra configuration needed either
|
|
297
|
-
way.
|
|
334
|
+
way. The ninth, `hypercast-wasm`, is only for ruby.wasm; see [Ruby in the browser](#ruby-in-the-browser).
|
|
298
335
|
|
|
299
336
|
Selection has **two** axes here, unlike every other binding in this repo. A Magnus extension
|
|
300
337
|
is bound to one Ruby minor ABI — there is no `abi3` equivalent to collapse the version axis
|
|
@@ -358,6 +395,60 @@ the shipped extension small. The build script, and the two flags that are load-b
|
|
|
358
395
|
the ARM leg (a static libunwind, and a clang-spelled `--target` for bindgen), live in the
|
|
359
396
|
forge's `ruby-magnus` action (`build-magnus.sh`), shared with every other Hyper* repo.
|
|
360
397
|
|
|
398
|
+
## Ruby in the browser
|
|
399
|
+
|
|
400
|
+
ruby.wasm cannot load an extension at runtime. `rbwasm build` cross-compiles the extension of
|
|
401
|
+
every gem in a Gemfile and links them all into the one interpreter it builds, so the browser
|
|
402
|
+
gets a gem of its own: `hypercast-wasm`, the same library and the same Magnus extension,
|
|
403
|
+
prebuilt for `wasm32-wasip1`. List it **instead of** `hypercast` in the Gemfile you build the
|
|
404
|
+
interpreter from (the two carry the same `lib/` files):
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
source "https://rubygems.org"
|
|
408
|
+
|
|
409
|
+
gem "hypercast-wasm"
|
|
410
|
+
gem "js" # JavaScript interop, which a browser app almost always wants
|
|
411
|
+
gem "bigdecimal" # only for Decimal#to_d; see below
|
|
412
|
+
|
|
413
|
+
group :development do
|
|
414
|
+
gem "ruby_wasm"
|
|
415
|
+
end
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
```sh
|
|
419
|
+
bundle install
|
|
420
|
+
bundle exec rbwasm build --ruby-version 4.0 -o ruby.wasm
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Load `ruby.wasm` with [`@ruby/wasm-wasi`](https://www.npmjs.com/package/@ruby/wasm-wasi)
|
|
424
|
+
(`DefaultRubyVM` in a browser, `RubyVM.instantiateModule` under Node), then
|
|
425
|
+
`require "/bundle/setup"` and `require "hypercast"` as anywhere else. `HyperCast::BACKEND` is
|
|
426
|
+
`:native`, and every door works as it does on a host Ruby.
|
|
427
|
+
|
|
428
|
+
- **Ruby 3.4 and 4.0**, one archive each, the same minors the platform gems cover, picked by
|
|
429
|
+
`--ruby-version`. Any other minor stops the build with a message naming the ones the gem
|
|
430
|
+
carries. Built and tested against ruby_wasm 2.10.
|
|
431
|
+
- **No Rust toolchain.** The gem's `extconf.rb` only hands rbwasm the prebuilt archive, and
|
|
432
|
+
rbwasm downloads its own wasi-sdk. The first `rbwasm build` compiles Ruby itself and takes
|
|
433
|
+
15–20 minutes; later builds reuse it.
|
|
434
|
+
- **`Decimal#to_d` needs `gem "bigdecimal"` in that Gemfile.** bigdecimal is a bundled gem
|
|
435
|
+
with a C extension, and ruby.wasm builds a bundled gem's extension only when the Gemfile
|
|
436
|
+
names it; without it, `to_d` raises `LoadError`. `to_s` and `to_r` need nothing.
|
|
437
|
+
- **Static linking only,** into ruby.wasm's default `wasm32-unknown-wasip1` interpreter.
|
|
438
|
+
rbwasm's dynamic-linking build (a pic target, for the component model) is not supported.
|
|
439
|
+
- **Size.** rbwasm packs every gem's files into the interpreter, the archives included, so the
|
|
440
|
+
gem adds about 2 MB to `ruby.wasm` (both archives are gzipped).
|
|
441
|
+
- **With HyperUuid.** `hyperuuid-wasm` links into the same interpreter. Every Rust extension
|
|
442
|
+
that carries std defines a few of the same symbols (`rust_eh_personality`, which ruby.wasm's
|
|
443
|
+
own wasi-vfs defines too, rb-sys's `ruby_abi_version`, and one of std's), and both gems
|
|
444
|
+
rename them to names of their own when CI builds the archive, which also fails if a newer
|
|
445
|
+
Rust starts exporting another.
|
|
446
|
+
|
|
447
|
+
What stands behind it: CI builds each minor's archive from the commit, packs the gem the way
|
|
448
|
+
it ships, links it into a fresh interpreter and runs [`wasm-smoke/test.rb`](wasm-smoke/test.rb)
|
|
449
|
+
under Node and in headless Chrome (the forge's `hyper-build-wasm.yml`). The published gem is
|
|
450
|
+
packed around those attested archives.
|
|
451
|
+
|
|
361
452
|
## Development
|
|
362
453
|
|
|
363
454
|
Everything below runs from a checkout, with `rust/` and `corpus/` beside `ruby/`; none of it
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module HyperCast
|
|
2
|
+
# The native core's C ABI values, presented — for a gem that carries HyperCast's verdicts
|
|
3
|
+
# across a C ABI of its own (HyperTabular does): how each door's out-value unpacks and what
|
|
4
|
+
# builds the Ruby value from it, the verdict code and span of a fault, the packed version
|
|
5
|
+
# word, and where a gem's native library is. The Fiddle backend's doors decode with exactly
|
|
6
|
+
# these, so a value read out of another library's buffer is the value the door would have.
|
|
7
|
+
#
|
|
8
|
+
# Every directive below unpacks one value. A gem decoding a whole column appends "*" and
|
|
9
|
+
# steps +fields+ at a time.
|
|
10
|
+
module Interop
|
|
11
|
+
# The doors whose value is one String#unpack directive away: the directive for one value.
|
|
12
|
+
# A bool is the byte 0 or 1, unpacked as "C" and compared with zero.
|
|
13
|
+
SCALARS = {
|
|
14
|
+
bool: "C", i8: "c", i16: "s<", i32: "l<", i64: "q<", u8: "C", u16: "S<", u32: "L<",
|
|
15
|
+
u64: "Q<", f32: "e", f64: "E", time: "Q<"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
# The doors whose value is a record: the directive that unpacks one, how many fields that
|
|
19
|
+
# yields, and what builds the door's Ruby value from them, starting at field +at+.
|
|
20
|
+
RECORDS = {
|
|
21
|
+
decimal: ["Q<L<CCx2", 4, ->(fields, at) { decimal(fields[at], fields[at + 1], fields[at + 2], fields[at + 3]) }],
|
|
22
|
+
uuid: ["H8H4H4H4H12", 5, ->(fields, at) { fields[at, 5].join("-") }],
|
|
23
|
+
timestamp: ["q<l<x4", 2, ->(fields, at) { instant(fields[at], fields[at + 1]) }],
|
|
24
|
+
date: ["S<CC", 3, ->(fields, at) { Date.new(fields[at], fields[at + 1], fields[at + 2]) }],
|
|
25
|
+
datetime: ["S<CCx4Q<", 4, ->(fields, at) { civil(fields[at], fields[at + 1], fields[at + 2], fields[at + 3]) }],
|
|
26
|
+
duration: ["q<l<x4", 2, ->(fields, at) { duration(fields[at], fields[at + 1]) }]
|
|
27
|
+
}.tap do |records|
|
|
28
|
+
records[:unix] = records[:excel_serial] = records[:timestamp]
|
|
29
|
+
records[:date_ordered] = records[:date]
|
|
30
|
+
end.freeze
|
|
31
|
+
|
|
32
|
+
# The bytes one value of each door takes.
|
|
33
|
+
VALUE_BYTES = {
|
|
34
|
+
bool: 1, i8: 1, u8: 1, i16: 2, u16: 2, i32: 4, u32: 4, f32: 4, date: 4, date_ordered: 4,
|
|
35
|
+
i64: 8, u64: 8, f64: 8, time: 8,
|
|
36
|
+
decimal: 16, uuid: 16, timestamp: 16, unix: 16, excel_serial: 16, datetime: 16, duration: 16
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# Encodings whose bytes already are the UTF-8 (or byte-identical) form the core reads.
|
|
40
|
+
BYTE_COMPATIBLE = [Encoding::UTF_8, Encoding::US_ASCII, Encoding::ASCII_8BIT].freeze
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# One value of +door+, from the bytes the core wrote for it.
|
|
45
|
+
def decode(door, bytes)
|
|
46
|
+
if (directive = SCALARS[door])
|
|
47
|
+
value = bytes.unpack1(directive)
|
|
48
|
+
door == :bool ? value != 0 : value
|
|
49
|
+
else
|
|
50
|
+
directive, _fields, build = RECORDS.fetch(door)
|
|
51
|
+
build.call(bytes.unpack(directive), 0)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# A UTC Time from the core's protobuf-shaped {seconds, nanos} pair, exactly — the
|
|
56
|
+
# timestamp, unix and excel_serial doors' value. (Time.at then #utc is the same Time as
|
|
57
|
+
# Time.at(..., in: "UTC"), at half the cost: the zone's name is not read each time.)
|
|
58
|
+
def instant(seconds, nanos)
|
|
59
|
+
Time.at(seconds, nanos, :nanosecond).utc
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The zone-less DateTime a civil date-time names, with exact Rational seconds — the
|
|
63
|
+
# datetime door's value. Its +00:00 offset is a carrier artifact, not data.
|
|
64
|
+
def civil(year, month, day, nanos_of_day)
|
|
65
|
+
second_of_day, frac = nanos_of_day.divmod(1_000_000_000)
|
|
66
|
+
hour, rest = second_of_day.divmod(3600)
|
|
67
|
+
minute, second = rest.divmod(60)
|
|
68
|
+
DateTime.new(year, month, day, hour, minute, second + Rational(frac, 1_000_000_000))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Exact Rational seconds from the core's same-signed {seconds, nanos} pair — the duration
|
|
72
|
+
# door's value.
|
|
73
|
+
def duration(seconds, nanos)
|
|
74
|
+
Rational(seconds * 1_000_000_000 + nanos, 1_000_000_000)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A Decimal from the core's {lo, hi, scale, negative} — the decimal door's value.
|
|
78
|
+
def decimal(lo, hi, scale, negative)
|
|
79
|
+
Decimal.new(magnitude: (hi << 64) | lo, scale: scale, negative: negative != 0)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The Fault a nonzero verdict code and its span name. A code that names no reason is a
|
|
83
|
+
# binding bug, not data: a KeyError.
|
|
84
|
+
def fault(code, offset, length)
|
|
85
|
+
Fault.new(reason: REASONS.fetch(code), offset: offset, length: length)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The core's byte span over +bytes+, in the units String#[] slices by: an identity for a
|
|
89
|
+
# binary String (its characters are its bytes) and for ASCII text (`ascii_only?` reads the
|
|
90
|
+
# cached coderange — no scan), a byte-to-character remap otherwise. Character counts
|
|
91
|
+
# survive transcoding, so a span mapped on the UTF-8 form indexes the caller's own String
|
|
92
|
+
# whatever encoding it arrived in.
|
|
93
|
+
def characters(bytes, offset, length)
|
|
94
|
+
return [offset, length] if bytes.encoding == Encoding::ASCII_8BIT || bytes.ascii_only?
|
|
95
|
+
|
|
96
|
+
[bytes.byteslice(0, offset).length, bytes.byteslice(offset, length).length]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A native library's packed version word — major << 16 | minor << 8 | patch, as a
|
|
100
|
+
# *_version export returns it — as "major.minor.patch".
|
|
101
|
+
def version(word)
|
|
102
|
+
"#{word >> 16}.#{(word >> 8) & 0xFF}.#{word & 0xFF}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The shared library to dlopen for +library+ ("hypercast" for libhypercast.so): the
|
|
106
|
+
# gem's native/{rid}/{file} under +native_dir+, or — the development loop — the cargo
|
|
107
|
+
# build under +repo_root+/rust/target/release, exactly what the other bindings' local
|
|
108
|
+
# staging does. Nil when neither exists.
|
|
109
|
+
def library_path(library, native_dir, repo_root)
|
|
110
|
+
rid, file = NativePlatform.rid_and_library_name(library: library)
|
|
111
|
+
path = File.join(native_dir, rid, file)
|
|
112
|
+
return path if File.exist?(path)
|
|
113
|
+
|
|
114
|
+
repo_build = File.join(repo_root, "rust", "target", "release", file)
|
|
115
|
+
File.exist?(repo_build) ? repo_build : nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
module HyperCast
|
|
2
2
|
# Maps the running RUBY_PLATFORM to the RID-style directory (matching the other bindings'
|
|
3
|
-
# runtimes/{rid}/native/ / native/{rid}/ convention) and
|
|
3
|
+
# runtimes/{rid}/native/ / native/{rid}/ convention) and file name of a native library.
|
|
4
|
+
# Every SkunkWerkx core ships the same way, so +library+ names whose: "hypercast" for this
|
|
5
|
+
# gem's libhypercast, "hypertabular" for HyperTabular's.
|
|
4
6
|
module NativePlatform
|
|
5
7
|
class UnsupportedPlatformError < StandardError; end
|
|
6
8
|
|
|
7
9
|
# +platform+ is a parameter only so the specs can walk the whole table from one host;
|
|
8
10
|
# every real caller takes the default.
|
|
9
|
-
def self.rid_and_library_name(platform = RUBY_PLATFORM)
|
|
11
|
+
def self.rid_and_library_name(platform = RUBY_PLATFORM, library: "hypercast")
|
|
10
12
|
arch = platform.match?(/arm64|aarch64/) ? "arm64" : "x64"
|
|
11
13
|
|
|
12
14
|
case platform
|
|
13
15
|
when /mingw|mswin|windows/
|
|
14
|
-
["win-#{arch}", "
|
|
16
|
+
["win-#{arch}", "#{library}.dll"]
|
|
15
17
|
when /darwin/
|
|
16
|
-
["osx-#{arch}", "
|
|
18
|
+
["osx-#{arch}", "lib#{library}.dylib"]
|
|
17
19
|
when /linux/
|
|
18
20
|
# Two C libraries, two builds: a glibc-linked library cannot be relied on to dlopen
|
|
19
21
|
# into a musl process (Alpine), so musl gets RIDs of its own. Ruby names the libc
|
|
20
22
|
# in its platform string there ("x86_64-linux-musl") and leaves it off on glibc.
|
|
21
23
|
os = platform.include?("musl") ? "linux-musl" : "linux"
|
|
22
|
-
["#{os}-#{arch}", "
|
|
24
|
+
["#{os}-#{arch}", "lib#{library}.so"]
|
|
23
25
|
else
|
|
24
|
-
raise UnsupportedPlatformError, "
|
|
26
|
+
raise UnsupportedPlatformError, "#{library}: unsupported platform RUBY_PLATFORM=#{platform}"
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
29
|
end
|
data/lib/hypercast/runtime.rb
CHANGED
|
@@ -17,7 +17,9 @@ module HyperCast
|
|
|
17
17
|
# Each door's C signature, by shape — resolved to Fiddle types only in load_functions,
|
|
18
18
|
# so nothing here touches Fiddle until the Fiddle backend actually runs: plain is
|
|
19
19
|
# (text, len, out, fault), numeric adds the packed NumFormat pointer before out, and
|
|
20
|
-
# declared a caller-declared u32 (precision, epoch, field order) in the same place.
|
|
20
|
+
# declared a caller-declared u32 (precision, epoch, field order) in the same place. The
|
|
21
|
+
# typed doors take a double instead of the text and its length: typed is (value, out,
|
|
22
|
+
# fault), typed_declared adds the u32 after the value.
|
|
21
23
|
DOORS = {
|
|
22
24
|
cast_bool: :plain,
|
|
23
25
|
cast_i8: :numeric, cast_i16: :numeric, cast_i32: :numeric, cast_i64: :numeric,
|
|
@@ -26,7 +28,9 @@ module HyperCast
|
|
|
26
28
|
cast_uuid: :plain,
|
|
27
29
|
cast_timestamp: :plain, cast_unix: :declared, cast_excel_serial: :declared,
|
|
28
30
|
cast_date: :plain, cast_date_ordered: :declared, cast_datetime: :declared,
|
|
29
|
-
cast_time: :plain, cast_duration: :plain
|
|
31
|
+
cast_time: :plain, cast_duration: :plain,
|
|
32
|
+
cast_decimal_from_f64: :typed, cast_excel_serial_from_f64: :typed_declared,
|
|
33
|
+
cast_excel_time: :typed, cast_excel_duration: :typed
|
|
30
34
|
}.freeze
|
|
31
35
|
|
|
32
36
|
# The one export that is not a door: the zero-argument version probe, returning the
|
|
@@ -60,12 +64,7 @@ module HyperCast
|
|
|
60
64
|
# development loop — the in-repo cargo build, exactly what the other bindings' local
|
|
61
65
|
# staging does. Nil when neither exists.
|
|
62
66
|
def library_path
|
|
63
|
-
|
|
64
|
-
path = File.join(NATIVE_DIR, rid, lib_name)
|
|
65
|
-
return path if File.exist?(path)
|
|
66
|
-
|
|
67
|
-
repo_build = File.expand_path(File.join(__dir__, "../../../rust/target/release", lib_name))
|
|
68
|
-
File.exist?(repo_build) ? repo_build : nil
|
|
67
|
+
Interop.library_path("hypercast", NATIVE_DIR, File.expand_path("../../..", __dir__))
|
|
69
68
|
end
|
|
70
69
|
|
|
71
70
|
# Why Fiddle found nothing to load. A precompiled platform gem is the one install where
|
|
@@ -114,7 +113,9 @@ module HyperCast
|
|
|
114
113
|
signatures = {
|
|
115
114
|
plain: plain,
|
|
116
115
|
numeric: plain.dup.insert(2, Fiddle::TYPE_VOIDP),
|
|
117
|
-
declared: plain.dup.insert(2, Fiddle::TYPE_UINT32_T)
|
|
116
|
+
declared: plain.dup.insert(2, Fiddle::TYPE_UINT32_T),
|
|
117
|
+
typed: [Fiddle::TYPE_DOUBLE, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP],
|
|
118
|
+
typed_declared: [Fiddle::TYPE_DOUBLE, Fiddle::TYPE_UINT32_T, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP]
|
|
118
119
|
}
|
|
119
120
|
functions = DOORS.to_h do |name, shape|
|
|
120
121
|
[name, Fiddle::Function.new(handle[name.to_s], signatures.fetch(shape), Fiddle::TYPE_INT32_T)]
|
data/lib/hypercast.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "date"
|
|
2
2
|
require_relative "hypercast/native_platform"
|
|
3
|
+
require_relative "hypercast/interop"
|
|
3
4
|
require_relative "hypercast/runtime"
|
|
4
5
|
|
|
5
6
|
# Allocation-lean scalar casts — booleans, numerics, exact decimals, UUIDs, temporals —
|
|
@@ -29,7 +30,7 @@ require_relative "hypercast/runtime"
|
|
|
29
30
|
module HyperCast
|
|
30
31
|
# This gem's own version — kept in lockstep with hypercast.gemspec by the
|
|
31
32
|
# prepare-release workflow, so the two can never drift apart again.
|
|
32
|
-
VERSION = "0.
|
|
33
|
+
VERSION = "0.7.0"
|
|
33
34
|
|
|
34
35
|
# The success case of a verdict: a cast value.
|
|
35
36
|
Success = Data.define(:value)
|
|
@@ -216,13 +217,12 @@ module HyperCast
|
|
|
216
217
|
# (t/f, yes/no, y/n, 1/0, on/off, enabled/disabled, active/inactive,
|
|
217
218
|
# checked/unchecked, in/out), ASCII case-insensitive.
|
|
218
219
|
def bool(text)
|
|
219
|
-
plain(:cast_bool, text, 1) { |out|
|
|
220
|
+
plain(:cast_bool, text, 1) { |out| Interop.decode(:bool, out) }
|
|
220
221
|
end
|
|
221
222
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
size = sizes.fetch(unpack)
|
|
223
|
+
%i[i8 i16 i32 i64 u8 u16 u32 u64].each do |door|
|
|
224
|
+
unpack = Interop::SCALARS.fetch(door)
|
|
225
|
+
size = Interop::VALUE_BYTES.fetch(door)
|
|
226
226
|
# Resolved once here, not inside the method: interpolating a Symbol per call built a
|
|
227
227
|
# String and interned it on every integer cast.
|
|
228
228
|
symbol = :"cast_#{door}"
|
|
@@ -237,12 +237,12 @@ module HyperCast
|
|
|
237
237
|
# Casts real text to an IEEE single (widened losslessly on the way out): finite values
|
|
238
238
|
# only, declared separators and grouping, parens, exponent, and trailing percent.
|
|
239
239
|
def f32(text, format)
|
|
240
|
-
numeric(:cast_f32, text, format, 4) { |out|
|
|
240
|
+
numeric(:cast_f32, text, format, 4) { |out| Interop.decode(:f32, out) }
|
|
241
241
|
end
|
|
242
242
|
|
|
243
243
|
# Casts real text to an IEEE double. Notation rules as f32.
|
|
244
244
|
def f64(text, format)
|
|
245
|
-
numeric(:cast_f64, text, format, 8) { |out|
|
|
245
|
+
numeric(:cast_f64, text, format, 8) { |out| Interop.decode(:f64, out) }
|
|
246
246
|
end
|
|
247
247
|
|
|
248
248
|
# Casts decimal text to an exact Decimal — the real doors' grammar (declared separators
|
|
@@ -252,32 +252,27 @@ module HyperCast
|
|
|
252
252
|
# A magnitude past 2**96 - 1, or more fractional precision than 28 places can hold, is
|
|
253
253
|
# an :out_of_range Fault — the door never rounds.
|
|
254
254
|
def decimal(text, format)
|
|
255
|
-
numeric(:cast_decimal, text, format, 16)
|
|
256
|
-
lo, hi, scale, negative = out.unpack("Q<L<CCx2")
|
|
257
|
-
Decimal.new(magnitude: (hi << 64) | lo, scale: scale, negative: negative != 0)
|
|
258
|
-
end
|
|
255
|
+
numeric(:cast_decimal, text, format, 16) { |out| Interop.decode(:decimal, out) }
|
|
259
256
|
end
|
|
260
257
|
|
|
261
258
|
# Casts UUID text — all five .NET Guid formats (D/N/B/P/X) plus urn:uuid:/GUID:/UUID:
|
|
262
259
|
# prefixes — to Ruby's UUID lingua franca: the lowercase hyphenated String (the same
|
|
263
260
|
# shape SecureRandom.uuid returns).
|
|
264
261
|
def uuid(text)
|
|
265
|
-
plain(:cast_uuid, text, 16)
|
|
266
|
-
out.unpack("H8H4H4H4H12").join("-")
|
|
267
|
-
end
|
|
262
|
+
plain(:cast_uuid, text, 16) { |out| Interop.decode(:uuid, out) }
|
|
268
263
|
end
|
|
269
264
|
|
|
270
265
|
# Casts an RFC 3339 instant — zone mandatory — to a UTC Time at full nanosecond
|
|
271
266
|
# fidelity across the whole 0001-9999 window.
|
|
272
267
|
def timestamp(text)
|
|
273
|
-
plain(:cast_timestamp, text, 16) { |out|
|
|
268
|
+
plain(:cast_timestamp, text, 16) { |out| Interop.decode(:timestamp, out) }
|
|
274
269
|
end
|
|
275
270
|
|
|
276
271
|
# Casts an integer Unix-epoch value under a caller-declared unit Symbol
|
|
277
272
|
# (:seconds/:milliseconds/:microseconds/:nanoseconds) to a UTC Time. An unknown unit
|
|
278
273
|
# is a caller bug (KeyError), never a verdict.
|
|
279
274
|
def unix(text, precision)
|
|
280
|
-
declared(:cast_unix, text, UNIX_PRECISIONS.fetch(precision), 16) { |out|
|
|
275
|
+
declared(:cast_unix, text, UNIX_PRECISIONS.fetch(precision), 16) { |out| Interop.decode(:unix, out) }
|
|
281
276
|
end
|
|
282
277
|
|
|
283
278
|
# Casts an Excel date serial under a caller-declared epoch Symbol (:y1900/:y1904) to a
|
|
@@ -291,7 +286,7 @@ module HyperCast
|
|
|
291
286
|
# the text "1900-02-29" — so every serial above it is shifted one day against a naive
|
|
292
287
|
# count. An unknown epoch is a caller bug (KeyError), never a verdict.
|
|
293
288
|
def excel_serial(text, epoch)
|
|
294
|
-
declared(:cast_excel_serial, text, EXCEL_EPOCHS.fetch(epoch), 16) { |out|
|
|
289
|
+
declared(:cast_excel_serial, text, EXCEL_EPOCHS.fetch(epoch), 16) { |out| Interop.decode(:excel_serial, out) }
|
|
295
290
|
end
|
|
296
291
|
|
|
297
292
|
# Casts a calendar date to a Date. With no order declared: the strict ISO 8601
|
|
@@ -301,15 +296,9 @@ module HyperCast
|
|
|
301
296
|
# caller bug (KeyError), never a verdict.
|
|
302
297
|
def date(text, order = nil)
|
|
303
298
|
if order.nil?
|
|
304
|
-
plain(:cast_date, text, 4)
|
|
305
|
-
year, month, day = out.unpack("S<CC")
|
|
306
|
-
Date.new(year, month, day)
|
|
307
|
-
end
|
|
299
|
+
plain(:cast_date, text, 4) { |out| Interop.decode(:date, out) }
|
|
308
300
|
else
|
|
309
|
-
declared(:cast_date_ordered, text, DATE_ORDERS.fetch(order), 4)
|
|
310
|
-
year, month, day = out.unpack("S<CC")
|
|
311
|
-
Date.new(year, month, day)
|
|
312
|
-
end
|
|
301
|
+
declared(:cast_date_ordered, text, DATE_ORDERS.fetch(order), 4) { |out| Interop.decode(:date_ordered, out) }
|
|
313
302
|
end
|
|
314
303
|
end
|
|
315
304
|
|
|
@@ -322,29 +311,51 @@ module HyperCast
|
|
|
322
311
|
# fusing a real zone is the caller's job, and timestamp stays the strict RFC 3339
|
|
323
312
|
# instant door. An unknown order is a caller bug (KeyError).
|
|
324
313
|
def datetime(text, order)
|
|
325
|
-
declared(:cast_datetime, text, DATE_ORDERS.fetch(order), 16)
|
|
326
|
-
year, month, day, nanos = out.unpack("S<CCx4Q<")
|
|
327
|
-
second_of_day, frac = nanos.divmod(1_000_000_000)
|
|
328
|
-
hour, rest = second_of_day.divmod(3600)
|
|
329
|
-
minute, second = rest.divmod(60)
|
|
330
|
-
DateTime.new(year, month, day, hour, minute, second + Rational(frac, 1_000_000_000))
|
|
331
|
-
end
|
|
314
|
+
declared(:cast_datetime, text, DATE_ORDERS.fetch(order), 16) { |out| Interop.decode(:datetime, out) }
|
|
332
315
|
end
|
|
333
316
|
|
|
334
317
|
# Casts an ISO 24-hour time-of-day to an exact Integer of nanoseconds since midnight
|
|
335
318
|
# (Ruby has no time-of-day type; the integer keeps every digit).
|
|
336
319
|
def time(text)
|
|
337
|
-
plain(:cast_time, text, 8) { |out|
|
|
320
|
+
plain(:cast_time, text, 8) { |out| Interop.decode(:time, out) }
|
|
338
321
|
end
|
|
339
322
|
|
|
340
323
|
# Casts a duration (ISO 8601 fixed components, invariant colon form, or protobuf JSON
|
|
341
324
|
# seconds) to exact Rational seconds — full fidelity across the core's ±10,000-year
|
|
342
325
|
# window, no wrapping and no truncation.
|
|
343
326
|
def duration(text)
|
|
344
|
-
plain(:cast_duration, text, 16)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
327
|
+
plain(:cast_duration, text, 16) { |out| Interop.decode(:duration, out) }
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Reads a number a caller already holds — an Integer, Float or Rational, the way Ruby's
|
|
331
|
+
# own Float conversion takes it; anything else, a String included, is a TypeError — as
|
|
332
|
+
# the exact Decimal it names: the shortest decimal that rounds back to the Float, the
|
|
333
|
+
# digits a spreadsheet writes for it. 0.1 is magnitude 1, scale 1, not the binary
|
|
334
|
+
# fraction nearest it; 0.1 + 0.2 is 0.30000000000000004. NaN is :malformed; an infinity,
|
|
335
|
+
# a magnitude past 2**96 - 1 or more than 28 places is :out_of_range. A typed door's
|
|
336
|
+
# Fault has no span: offset and length are 0.
|
|
337
|
+
def decimal_from_float(value)
|
|
338
|
+
typed(:cast_decimal_from_f64, float(value), 16) { |out| Interop.decode(:decimal, out) }
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Reads an Excel serial number under a declared epoch Symbol (:y1900/:y1904) as the
|
|
342
|
+
# zone-less DateTime it names — the twin of excel_serial for a number a workbook reader
|
|
343
|
+
# already holds, on the same carrier datetime uses. The 1900 system's phantom serial 60
|
|
344
|
+
# is :out_of_range; a negative, NaN or infinite serial is :malformed.
|
|
345
|
+
def excel_serial_from_float(value, epoch)
|
|
346
|
+
number = float(value)
|
|
347
|
+
typed(:cast_excel_serial_from_f64, number, 16, EXCEL_EPOCHS.fetch(epoch)) { |out| Interop.decode(:datetime, out) }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Reads the fraction of an Excel serial number as an exact Integer of nanoseconds since
|
|
351
|
+
# midnight, as time does; 0.75 and 45292.75 are both 18:00.
|
|
352
|
+
def excel_time(value)
|
|
353
|
+
typed(:cast_excel_time, float(value), 8) { |out| Interop.decode(:time, out) }
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Reads a number of days as exact Rational seconds, as duration does: 1.5 is 129600.
|
|
357
|
+
def excel_duration(value)
|
|
358
|
+
typed(:cast_excel_duration, float(value), 16) { |out| Interop.decode(:duration, out) }
|
|
348
359
|
end
|
|
349
360
|
|
|
350
361
|
# The version of the native core actually loaded, as "major.minor.patch" — read from
|
|
@@ -353,15 +364,11 @@ module HyperCast
|
|
|
353
364
|
# not). The cheapest possible probe that the backend resolved at all: takes nothing,
|
|
354
365
|
# cannot fail.
|
|
355
366
|
def native_version
|
|
356
|
-
|
|
357
|
-
"#{word >> 16}.#{(word >> 8) & 0xFF}.#{word & 0xFF}"
|
|
367
|
+
Interop.version(packed_version)
|
|
358
368
|
end
|
|
359
369
|
|
|
360
370
|
private
|
|
361
371
|
|
|
362
|
-
# Encodings whose bytes already are the UTF-8 (or byte-identical) form the core reads.
|
|
363
|
-
BYTE_COMPATIBLE = [Encoding::UTF_8, Encoding::US_ASCII, Encoding::ASCII_8BIT].freeze
|
|
364
|
-
|
|
365
372
|
# Presents the input as UTF-8 bytes: already-compatible text crosses as-is (Fiddle
|
|
366
373
|
# passes a String's own bytes for void* — no copy of them); only foreign encodings pay
|
|
367
374
|
# a transcode, and text that cannot be transcoded raises String#encode's own
|
|
@@ -376,7 +383,7 @@ module HyperCast
|
|
|
376
383
|
raise TypeError, "no implicit conversion of #{text.class} into String"
|
|
377
384
|
text = converted
|
|
378
385
|
end
|
|
379
|
-
BYTE_COMPATIBLE.include?(text.encoding) ? text : text.encode(Encoding::UTF_8)
|
|
386
|
+
Interop::BYTE_COMPATIBLE.include?(text.encoding) ? text : text.encode(Encoding::UTF_8)
|
|
380
387
|
end
|
|
381
388
|
|
|
382
389
|
# Fiddle spells a null pointer as nil — the core's contract for empty input.
|
|
@@ -408,22 +415,10 @@ module HyperCast
|
|
|
408
415
|
elsif rc == -1
|
|
409
416
|
raise "hypercast: libhypercast reported a contract violation — a binding bug, please report it"
|
|
410
417
|
else
|
|
411
|
-
|
|
412
|
-
Fault.new(reason: REASONS.fetch(rc), offset: offset, length: length)
|
|
418
|
+
Interop.fault(rc, *Interop.characters(bytes, *fault[0, 8].unpack("L<L<")))
|
|
413
419
|
end
|
|
414
420
|
end
|
|
415
421
|
|
|
416
|
-
# The core's byte span in the units String#[] slices by: an identity for a binary
|
|
417
|
-
# String (its characters are its bytes) and for ASCII text (`ascii_only?` reads the
|
|
418
|
-
# cached coderange — no scan), a byte-to-character remap otherwise. Character counts
|
|
419
|
-
# survive transcoding, so a span mapped on the UTF-8 form indexes the caller's own
|
|
420
|
-
# String whatever encoding it arrived in. Failure path only: a Success never pays.
|
|
421
|
-
def characters(bytes, offset, length)
|
|
422
|
-
return [offset, length] if bytes.encoding == Encoding::ASCII_8BIT || bytes.ascii_only?
|
|
423
|
-
|
|
424
|
-
[bytes.byteslice(0, offset).length, bytes.byteslice(offset, length).length]
|
|
425
|
-
end
|
|
426
|
-
|
|
427
422
|
# The shared body of every format-free door: one native call over the scratch buffers.
|
|
428
423
|
def plain(symbol, text, out_size)
|
|
429
424
|
bytes = utf8(text)
|
|
@@ -452,6 +447,34 @@ module HyperCast
|
|
|
452
447
|
verdict(rc, fault, bytes) { yield(out[0, out_size]) }
|
|
453
448
|
end
|
|
454
449
|
|
|
450
|
+
# A typed door's number, converted the way the extension's argument is (C's
|
|
451
|
+
# rb_num2dbl), with the same exception and message: a Numeric converts (a Complex with an
|
|
452
|
+
# imaginary part is Complex#to_f's RangeError), anything else is a TypeError — a String
|
|
453
|
+
# among them, which Kernel#Float would have parsed.
|
|
454
|
+
def float(value)
|
|
455
|
+
case value
|
|
456
|
+
when Float then value
|
|
457
|
+
when Numeric then Float(value)
|
|
458
|
+
when String then raise TypeError, "no implicit conversion to float from string"
|
|
459
|
+
when nil, true, false then raise TypeError, "no implicit conversion to float from #{value.inspect}"
|
|
460
|
+
else raise TypeError, "can't convert #{value.class} into Float"
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# The shared body of the typed doors: a double instead of the text, then the declared
|
|
465
|
+
# u32 when the door takes one.
|
|
466
|
+
def typed(symbol, number, out_size, *declared)
|
|
467
|
+
out, fault = scratch
|
|
468
|
+
rc = Runtime.function(symbol).call(number, *declared, out, fault)
|
|
469
|
+
if rc.zero?
|
|
470
|
+
Success.new(value: yield(out[0, out_size]))
|
|
471
|
+
elsif rc == -1
|
|
472
|
+
raise "hypercast: libhypercast reported a contract violation — a binding bug, please report it"
|
|
473
|
+
else
|
|
474
|
+
Interop.fault(rc, 0, 0)
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
455
478
|
# The loaded core's version word, major << 16 | minor << 8 | patch, straight from the
|
|
456
479
|
# library's zero-argument hypercast_version export.
|
|
457
480
|
def packed_version
|
|
@@ -482,12 +505,6 @@ module HyperCast
|
|
|
482
505
|
cache[format] = pointer
|
|
483
506
|
end.compare_by_identity
|
|
484
507
|
end
|
|
485
|
-
|
|
486
|
-
# Builds a UTC Time from the core's protobuf-shaped {seconds, nanos} pair, exactly.
|
|
487
|
-
def instant(bytes)
|
|
488
|
-
seconds, nanos = bytes.unpack("q<l<")
|
|
489
|
-
Time.at(seconds, nanos, :nanosecond, in: "UTC")
|
|
490
|
-
end
|
|
491
508
|
end
|
|
492
509
|
end
|
|
493
510
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hypercast
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: aarch64-linux-gnu
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Buvinghausen
|
|
@@ -53,6 +53,7 @@ files:
|
|
|
53
53
|
- lib/hypercast.rb
|
|
54
54
|
- lib/hypercast/3.4/hypercast_native.so
|
|
55
55
|
- lib/hypercast/4.0/hypercast_native.so
|
|
56
|
+
- lib/hypercast/interop.rb
|
|
56
57
|
- lib/hypercast/native_platform.rb
|
|
57
58
|
- lib/hypercast/runtime.rb
|
|
58
59
|
homepage: https://github.com/SkunkWerkx/HyperCast
|