Repository navigation
v0.2.0
The native call was never the bottleneck. Every binding already made exactly one call into the Rust core per cast — what cost real time was what each language wrapped around it: a copied input, heap scratch for a 16-byte out-value, an object built to be thrown away. This release strips that carrier back in every binding and measures each one before and after on one machine in one session. The core is untouched; no door changed its verdict on any input, and the shared corpus still replays byte-identical through all eight packages.
Alongside that: the seventh Ruby gem (aarch64-mingw-ucrt), per-platform Native AOT receipts, provenance instructions in every README, and the release pipeline brought in step with what HyperUuid 0.2.x learned.
The scoreboard, again
The RFC 3339 timestamp door against each platform's own parser, linux-arm64, crossing and transcode included in every HyperCast figure, with 0.1.0's number beside it. Full receipts and methodology in the root README and each binding's own.
| Language | 0.1.0 | 0.2.0 | Platform parser, same run | |
|---|---|---|---|---|
| Swift | 281 ns, 3 mallocs | 55 ns, 0 mallocs | 817 ns Date.ISO8601FormatStyle |
14.9x faster |
| Java | 62.9 ns | 52.5 ns (46.2 from byte[]) |
596 ns Instant.parse |
11.4x faster |
| C# | 71 ns | 76.9 ns from string, 51.2 ns from UTF-8 |
282 ns DateTimeOffset.TryParse |
3.7x / 5.5x faster |
| Ruby (Magnus) | 713 ns | 771 ns | 3.32 µs Time.iso8601 |
4.3x faster |
| PHP | 487 ns | 567 ns | 1.57 µs DateTimeImmutable |
2.8x faster |
| Go | 174 ns, 2 allocs | 112 ns, 0 allocs | 67 ns time.Parse(RFC3339Nano) |
slower — still on purpose, now without an allocation |
Where the carrier was the cost, the door moved. Where it wasn't — Ruby and PHP, whose numeric doors are what changed — the timestamp row is within noise of 0.1.0 and is printed as such.
Swift: zero mallocs on every door
Every String door copied the input into a fresh [UInt8], and every door then allocated three more heap arrays for the out-value, the fault span and the format — four mallocs before the native call. The input now crosses as a view of the string's own UTF-8 (withUTF8), the scratch is a tuple of fixed-width integers on the stack, and the library handle is a class reference rather than a 21-field struct copied out of a Result per call.
| Door | 0.1.0 | 0.2.0 | mallocs |
|---|---|---|---|
Cast.timestamp |
281 ns | 55 ns | 3 → 0 |
Cast.uuid |
222 ns | 37 ns | 3 → 0 |
Cast.f64 |
354 ns | 45 ns | 4 → 0 |
Cast.i32 |
349 ns | 33 ns | 4 → 0 |
Cast.dateTime (messy civil) |
330 ns | 129 ns | 3 → 0 |
The Foundation controls in the same run are unchanged between the two tapes, which is what makes the comparison a receipt. Every door also gained an UnsafeRawBufferPointer overload — the primitive the String and [UInt8] forms now wrap.
Go: the verdict comes back by value
0.1.0's doors passed &out and &fault into the foreign call, and any Go pointer handed to cgo escapes to the heap. The README called that "a floor for this call shape" — it was a floor for that shape, not for the ABI. The C shims now declare the out-value, fault span and format on their own stack and return one struct, so no Go pointer crosses at all:
| Door | 0.1.0 | 0.2.0 |
|---|---|---|
Bool |
111 ns, 1 alloc | 78 ns, 0 allocs |
I32 |
172 ns, 3 allocs | 87 ns, 0 allocs |
F64 |
218 ns, 3 allocs | 106 ns, 0 allocs |
Uuid |
155 ns, 2 allocs | 99 ns, 0 allocs |
Timestamp |
174 ns, 2 allocs | 112 ns, 0 allocs |
DateTime (1/7/2026 3:04 PM) |
173 ns, 2 allocs | 122 ns, 0 allocs — parity with time.Parse now |
purego is unchanged within noise: its allocations are the trampoline's own argument boxing, not the out-params. Go still loses per-call to its stdlib on the plain doors, and still says so.
Java: the input no longer copies
Every downcall is linked Linker.Option.critical(true), so the caller's byte[] crosses as a pinned heap segment instead of being copied into a per-thread native staging buffer — that buffer and its arena are gone. Sound because every door is a short, non-blocking parse over those bytes that never calls back into Java, which is the profile the option exists for. reachability-metadata.json registers it, and the GraalVM Native Image smoke test passes on it, so a consumer's own native-image build inherits it with no configuration.
Every door gained a MemorySegment overload: slice one buffer holding many values — a mapped file, a direct buffer, one line of a CSV — and cast a value out of it with nothing copied. That is the shape round three's chunk layer will hand down. The UUID door reads its sixteen bytes as two big-endian longs instead of one at a time, which turned the one row that lost in 0.1.0 into a win:
| Door | 0.1.0 | 0.2.0 | JDK |
|---|---|---|---|
Cast.timestamp |
62.9 ns | 52.5 ns | 596 ns Instant.parse |
Cast.uuid |
53.8 ns | 38.3 ns | 45.9 ns UUID.fromString |
Cast.time |
66.4 ns | 45.1 ns | 393 ns LocalTime.parse |
Cast.dateTime (messy civil) |
81.8 ns | 64.9 ns | 386 ns DateTimeFormatter |
Full-length JMH, 2 forks × (5 warmup + 10 measurement), -prof gc.
Python, Ruby, PHP, C#
- Python —
cast_uuidbuildsuuid.UUIDthe way HyperUuid pinned:UUID.__new__plusobject.__setattr__of theintandis_safeslots, skipping an__init__whose validation the core had already done. 1.18 µs → 730 ns, ahead ofuuid.UUID()'s 979 ns. The door used to sit at parity because both sides were bounded by__init__. - Ruby (Magnus) — the three reason Symbols and the option-symbol tables are cached,
DETECTis identity-matched likeINVARIANT, and any other format is resolved once per thread and memoized by identity, anchored in a thread-variable so the key can never be a recycled address. The numeric doors under a declared eurozone format orDETECTdrop from ~990 to ~580 ns; every format other thanINVARIANTused to pay three method dispatches and twoStringallocations per call to read its separators back out. - Ruby (Fiddle) — ~20% off the numeric doors (
i323.37 → 2.62 µs) by not building per call what never changes: the interpolated:cast_*Symbol, the splat dispatcher, the 12-byte format copy into scratch. Still the zero-compile fallback, still on Fiddle's 1.6 µs floor. - PHP — the eight integer doors are flat (one literal FFI call each, no dynamic symbol lookup and string match per call), and
Cast::uuidBytesreturns the sixteen RFC-ordered octets for aBINARY(16)bind. Within noise on phpbench; recorded as structure, not speed. - C# — the
stringdoors try the stack buffer first and rent from the pool only when the encoder says the text did not fit. New UTF-8 benchmark rows measure theReadOnlySpan<byte>doors without the transcode everystringrow includes:Cast.Uuid36.9 ns againstGuid.TryParse's 51.2, which the string row had reported as a wash.
Ruby: the seventh gem
aarch64-mingw-ucrt joins the platform gems, with the Magnus extension for both Ruby ABIs, so Windows-on-ARM leaves the Fiddle fallback like every other mainstream platform. Both Windows extensions build the gnullvm targets. This one is CI's receipt so far — built and tested on the win-arm64 leg — and no Magnus-versus-Fiddle numbers have been taken on Windows-on-ARM hardware for this crate yet. gem install hypercast resolves the right one; anything outside the grid still gets the universal zero-compile gem.
Provenance, and the receipts behind it
Every README now has a Verifying provenance section: which artifact, which signer, and why --signer-repo SkunkWerkx/.github is needed on some and not others.
# A package — signed by release.yml, which lives in this repo.
gh attestation verify hypercast-0.2.0-aarch64-mingw-ucrt.gem --repo SkunkWerkx/HyperCast
# A native library, or a Magnus extension — signed by the shared forge workflow,
# so the signer's own repo has to be named too.
gh attestation verify libhypercast.so \
--repo SkunkWerkx/HyperCast --signer-repo SkunkWerkx/.githubCI also publishes HyperCast.AotSmokeTest under Native AOT on all six RIDs, fails on any trim diagnostic, runs the binary, and uploads each leg's log as aot-report-{rid} — the AOT claim as a per-platform receipt rather than a README sentence. The crate is packaged and attested before its irreversible push, and publishes tokenless through Trusted Publishing.
Not in this release
A batch door. docs/roadmap.md places it in round three as new exports beside the scalar ABI; a binding-level loop over N crossings would be a fake win, and every change above is the per-call shape that layer will inherit. Two platform controls moved between toolchains and are reported as measured rather than carried over: Swift's DateFormatter read 810 ns on 0.1.0's tape and 28 µs on Swift 6.3.3, and the JDK's ISO_OFFSET_DATE_TIME control was unstable in the full-length run and is not quoted.
Install
| Language | ||
|---|---|---|
| Rust | cargo add hypercast |
crates.io |
| C# | dotnet add package HyperCast |
nuget.org |
| Java | io.github.skunkwerkx:hypercast:0.2.0 |
Maven Central |
| Python | pip install hypercast |
PyPI |
| Ruby | gem install hypercast |
RubyGems |
| PHP | composer require skunkwerkx/hypercast |
Packagist |
| Swift | .package(url: "https://github.com/SkunkWerkx/HyperCast", from: "0.2.0") |
git tag |
| Go | go get github.com/SkunkWerkx/HyperCast/go@go/v0.2.0 |
git tag |
Drop-in from 0.1.0 for every binding: nothing is removed or renamed, every new door is an overload or a sibling beside the existing surface, and the verdict types are untouched. Full entry in CHANGELOG.md.