Repository navigation
v0.3.0 - A wasm backend in Java, Ruby, Python and Go
The core built as a wasm32-wasip1 module, hyperuuid.wasm, ships beside the native libraries in the jar, the gems and the wheels, and is committed under go/native/. Each binding runs it behind its existing backend switch, with the engine an optional dependency the consumer adds only if they want this path:
- Java — GraalWasm.
-Dhyperuuid.backend=wasm, or automatic when the jar has no native build for the platform.org.graalvm.polyglot:wasmiscompileOnlyand never reaches the POM;UuidGenerator.backend()reports which path won. - Ruby — the wasmtime gem.
HYPERUUID_WASM=1, or automatic when no native library exists for the platform.HyperUuid::BACKENDreports:wasm;spec/wasm_backend_spec.rbpins the outputs byte-for-byte against Fiddle. - Python — wasmtime-py via
pip install hyperuuid[wasm].HYPERUUID_WASM=1, or automatic when the PyO3 extension fails to import.hyperuuid.BACKENDreports"wasm"or"native". - Go — wasmtime-go behind
-tags hyperuuid_wasm. Opt-in only, never selected automatically; the tag compilo throughout, so no win-arm64 build.
Measured on one box, through each shipped binding:
| Binding | new_v7 wasm |
new_v7 native |
1000-UUID byte fill wasm | native |
|---|---|---|---|---|
| Java (GraalVM JIT) | 420 ns | 64 ns | ||
| Java (Native Image) | 181 ns | — | — | — |
| Ruby | 867 ns | ~450 ns | 40.6 µs | 24 µs |
| Python | 6.2 µs | 850 ns | 41 µs | 18. |
| Go | 3.1 µs | 142 ns | 41 µs | 17.6 µs |
On a stock JDK with no GraalVM JIT the modulwV7lands at 3.1 µs. Every wasm call isserialized under a lock, because neither a GraalWasmContextnor a wasmtimeStore` is safe for concurrent use; the
native backends stay lock-free.
One detail was load-bearing rather than tidy: the module exports wasi-libc's malloc/free through two linker flags in rust/.cargo/config.toml. A wasm host cannot hand this library a pointer into its own memory, so every embedder has to ask the guest for a buffer — and a host-picked offset past the data segments collided with dlmalloc's first allocation and corrupted a batch mid-buffer in the spike that led here. The export is what makes the guest's own
allocator the only one in play. Nothing abountract changes on any native target.
CI builds the module on every leg and runs the four suites a second time through it.
The carrier diet: Java, Go, Swift
Java: nothing is copied on the way across. Every door used to open an Arena.ofConfined() per call and copy every
input into it. All twelve downcalls are now l(true), so a caller's byte[]— a v5name, a batch destination, sixteen bytes to reorder in place — is pinned and handed to the native side directly. The single-UUID doors use one per-thread 16-byte in/out scratch, written and read as two big-endian longs.reachability-metadata.json` registers the option and the GraalVM Native Image smoke test passes on it.
| JMH | Before | After |
|---|---|---|
newV4 |
155 ns | 102 ns |
newV5 |
230 ns | 102 ns |
newV6 |
128 ns | 67 ns |
newV7 |
125 ns | 77 ns |
| each, allocation | 112 B/op | 32 B/op |
fillV7(byte[]) |
0 B/op |
Go: the UUID crosses by value. Every single-UUID door handed the core &out[0] of a Go local, and any Go pointer passed to a cgo call escapes to the heap. The C shims now keep the sixteen bytes on their own stack and return them as a struct, and take a UUID argument the same way, so no Go pointer crosses except a caller's own slice. NewV4/NewV6At/NewV7At go 1 → 0 allocs, NewV5String 3 → 1 (the one left is Go's own []byte(name)), V6/V7UnixMillis 75 → 58 ns, 0 allocs. Per-call time on the generators barely moves, because entropy, not the
crossing, is what those doors cost — the REAm is corrected accordingly. purego isunchanged.
Swift: zero mallocs per call. Every doore out-value and another per input, and theString v5 form copied the name into an Array. uuid_t is sixteen RFC-ordered bytes already, so it is now the scratch and the result; the v5 name crosses via withUTF8, the batch object doors fill their result array in place
through the existing fill path, and the librce rather than a 13-field struct copied percall. newV4 1 → 0 mallocs, newV5 3 → 0, newV7Batch(1000) 86 → 17 µs and 1002 → 1 malloc. newV5(namespace:name:) gains an UnsafeRawBufferPointer primitive that the String and [UInt8] forms now wrap.
The wasm module is attested like every native library
hyperuuid.wasm carries the same build-prov native builds, signed by the reusableworkflow in SkunkWerkx/.github, and stage-native-binaries.yml refuses to commit it under go/native/ unless that attestation verifies:
gh attestation verify hyperuuid.wasm \
--repo SkunkWerkx/HyperUuid --signer-repo SkunkWerkx/.githubEvery README now has a Verifying provenance section with the exact command and flags for its artifact.
Ruby: the gemspec declares no wasmtime
The engine is a Gemfile group for this repo's own suite, not a development dependency of the gem, so gem install hyperuuid and bundle install against the gem pull in nothing new.
Upgrade note
Drop-in for every binding. Nothing is removed or renamed. The wasm backends are opt-in and change nothing until asked for: no new runtime dependency in any package — Java's GraalWasm is compileOnly, Ruby's wasmtime a Gemfile group for the suite, Python's an extra, Go's behind a ge: wasmtime-go now appears in go.mod, soit enters a consumer's module graph without entering their binary. The only addition under rust/ is the .cargo/config.toml that exports malloc/free on wasm32-wasip1, which applies to builds run from that directory and to nothing a consumer compiles.