ReviewOS

zig-utils/zig-js

ABI: implement external ZigString ownership and finalization

#324
Closed chris opened this 9 days ago ·
9 days ago

Parent: #140 Tracked by: #134, #163, #164 Follow-up to: #199

Pinned scope

Implement the three remaining external-string exports shared by the pinned Home 7ed99c02 and Bun 4982b91e private profiles:

  • ZigString__external — retain the caller buffer and invoke (ctx, ptr, len) exactly once when the JS string releases it.
  • ZigString__toExternalValueWithCallback — the same exact-once lifetime with the pinned null callback context.
  • ZigString__toExternalU16 — transfer a globally allocated UTF-16 buffer and preserve exact UTF-16 code units, including lone surrogates.

The pinned references are src/jsc/bindings/bindings.cpp plus src/jsc/ZigString.zig/ZigString.rs. Copied, atomic, and rope construction are already complete under #199.

Design requirements

  • External storage stays immutable and usable until the last engine reference dies; do not implement a copy followed by an eager callback and call it external ownership.
  • Finalization is exact-once across ordinary GC, duplicate references, context teardown, construction failure, and zero-length strings.
  • Callback execution must be thread-safe, must not occur while GC/object locks are held, and must preserve the supplied pointer, length, and context exactly.
  • Latin-1/tagged UTF-16 decoding and UTF-16/WTF-8 conversion preserve JavaScript code-unit semantics, including astral pairs and lone surrogates.
  • Invalid pointer/length pairs, over-limit lengths, cross-VM misuse, and allocation failure publish the correct pending exception while returning the empty encoded sentinel and releasing transferred ownership exactly once where required.
  • Arena and moving-GC modes have one documented ownership model; if zig-gc needs a general external-cell finalizer hook, implement and test it in ~/Code/Libraries/zig-gc as its own linked small commit.

Acceptance

  • Focused Zig tests cover 8-bit and 16-bit buffers, empty strings, embedded NUL, astral/lone-surrogate values, source lifetime, mutation visibility contract, duplicate references, collection, teardown, callback reentry, callback thread behavior, errors, and injected OOM.
  • The separately compiled Home consumer declares, links, and executes all three signatures.
  • Home and Bun inventories and ABI documentation are regenerated with exact counts.
  • Relevant focused tests and both ABI audits pass; one full CI run is accepted before closure.
  • Parent issues receive completion evidence and updated remaining counts.
9 days ago

Collector audit found a hard prerequisite: current StringCells are arena-permanent primitives and markValue only traces objects. #325 now tracks precise collectible/finalizable StringCells and the external-owner hook required here. #324 remains the ABI/lifetime consumer and must not close until #325 supplies real liveness.

9 days ago

Prerequisite progress: 6d2af84 lands first-class collectible/finalizable runtime StringCells and exact owned-byte accounting under #325. External owner callbacks are intentionally not simulated or eagerly invoked; #324 remains open for the exact-once deferred release contract on top of this real GC lifecycle. Full matrix: https://github.com/zig-utils/zig-js/actions/runs/29700993746

9 days ago

Foundation landed on main in 66fa951b (with zig-gc 579f683 + ordering correction 05f9b6e). External ArrayBuffer owners now enqueue exact-once releases during finalization and drain them through zig-gc’s post-sweep hook only after allocation locks are released and parallel publication is reopened.

The focused C-API witness covers ordinary GC and context teardown, and its deallocator re-enters JSGarbageCollect successfully. This establishes the lock-safe lifecycle the three external ZigString adapters can reuse; the adapters themselves remain open in this issue.

Descendant matrix: https://github.com/zig-utils/zig-js/actions/runs/29701666901

9 days ago

All three pinned exports landed on main in 74b0fe3f:

  • ZigString__external retains Latin-1/UTF-16 input with the supplied pointer, logical length, context, and callback.
  • ZigString__toExternalValueWithCallback uses the same lifecycle with a null callback context.
  • ZigString__toExternalU16 transfers UTF-16 storage and resolves the consumer’s strong ZigString__freeGlobal (weak libc fallback only for standalone links).

StringCells keep canonical WTF-8 internally while retaining the original external allocation until cell death. GC finalizers enqueue without allocation; callbacks drain post-sweep and may re-enter GC. Arena contexts release at teardown. The focused witness covers Latin-1, astral/lone-surrogate UTF-16, reachability, repeat GC, teardown, exact arguments, callback reentry, oversized input, pending-exception preservation, and U16 transfer. Home/Bun audits are green at 323/108 and 314/107, and the separately compiled Home fixture is green at 327/327.

Fresh acceptance matrix: https://github.com/zig-utils/zig-js/actions/runs/29702181895. Leaving open until it is green.

9 days ago

The first acceptance matrix exposed one deterministic representation regression in two jobs: adding the external-owner pointer had grown StringCell past its enforced 32-byte hot footprint. 472c5b51 fixes the design rather than weakening the invariant by packing the managed-cell bit into the aligned owner-pointer word. The compact-cell, external ownership, and class-finalizer focused witnesses are all green locally.

Superseding acceptance matrix: https://github.com/zig-utils/zig-js/actions/runs/29702521425. Keeping this open until that descendant matrix is accepted.

9 days ago

Accepted by descendant CI run 29703497726: 39 jobs passed, with only the intentional nightly skip. External ZigString ownership/finalization and its GC lifecycle coverage are green.

Sign in to comment on this issue.