ReviewOS

zig-utils/zig-js

Private ABI: VM execution-control exports (API lock + execution-time-limit)

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

Parent: #140 Tracked by: #134, #163, #164

Scope

Implemented the six pending VM execution-control exports declared in the pinned Home source (packages/runtime/src/jsc/VM.zig @ 7ed99c02) and pinned Bun bindings (bindings.cpp @ 4982b91e), present in both private-ABI inventories:

  • JSC__VM__getAPILock — vm->apiLock().lock() (bindings.cpp:4906)
  • JSC__VM__holdAPILock — JSLockHolder + callback (bindings.cpp:4896)
  • JSC__VM__releaseAPILock — vm->apiLock().unlock() (bindings.cpp:4912)
  • JSC__VM__setExecutionTimeLimit — vm->ensureWatchdog().setTimeLimit(Seconds{limit}) (bindings.cpp:4875)
  • JSC__VM__clearExecutionTimeLimit — watchdog->setTimeLimit(noTimeLimit) (bindings.cpp:4869)
  • JSC__VM__hasExecutionTimeLimit — vm->watchdog() && watchdog->hasTimeLimit() (bindings.cpp:2830)

Acceptance

  • All six exports implemented in src/c_api.zig with the pinned semantics above
  • Compiled-fixture coverage in tests/abi/home_private_value_shims.zig (287/287):
    • API lock: callback executes under holdAPILock; recursive same-thread re-entry (JSLock is recursive per owner thread); real mutual exclusion proven by a second thread blocking until release; null-VM tolerance
    • Time limit: has reports armed state shared across sibling realms and isolated from foreign VMs; a 30 ms limit actually aborts a running while (true) {} evaluation (termination_requested set — attributable to the watchdog, not the 500M-step budget); limit stays armed after firing (JSC hasTimeLimit semantics); clear disarms without clearing the termination request; +inf maps to noTimeLimit; null-VM tolerance
  • Both inventories refreshed: Home 285/431 private implemented (146 pending), Bun 277/421 private implemented (144 pending); exactly six status flips per inventory
  • zig build test-home-private-abi (287/287 symbols linked; runtime matrix passed), bun-private-abi-audit, c-api-audit (117/117, 293 private profile exports), test-c-api, test-home-public-abi (50/50), zig build test -Dtest-filter=c_api (148/148) all green
  • Full zig build test validated on a clean-HEAD worktree (baseline vs candidate, same pinned base 5413726c): 1120/1121 both runs, the single failure being the pre-existing gc slab assertion from the in-flight wasm/gc track — zero new failures; sibling wasm work on the main tree untouched
  • README/docs counts + evidence updated; benchmark matrix unchanged

Evidence

  • Commits: 7f6cbf17 feat(abi): add VM API-lock and execution-time-limit exports (#302); e1b0755f docs(abi): publish VM execution-control evidence (#302) — both pushed to main
  • Fixture: Home private value shims: 287/287 symbols linked; runtime matrix passed
  • Home audit: implemented-private=285, pending-private=146, unclassified=0
  • Bun audit: implemented-private=277, pending-private=144, unclassified=0
  • C-API audit: 117 declarations, 117 complete, 0 pending, 293 private profile exports

Design notes (as implemented)

  • API lock maps JSC::VM::apiLock() onto a per-CContextGroup recursive lock (atomic mutex + owner thread id + depth). Same-thread nesting increments depth without deadlocking (JSLock recursion semantics); foreign threads get real mutual exclusion — proven in the fixture by a spawned thread blocking until release.
  • Execution-time limit maps JSC::Watchdog onto a lazily spawned host-watchdog thread per context group — the contract Context.requestTermination() documents (src/context.zig:3202). On expiry it sets the group termination_requested flag (existing TopExceptionScope machinery materializes a TerminationError at ABI boundaries) and calls requestTermination() so the interpreter's cooperative stop poll aborts running evaluation. The watchdog is joined in CContextGroup.destroy before realm teardown.
  • Timeout mapping mirrors JSC::Watchdog: +inf == noTimeLimit (disarmed), non-positive == fire at the next watchdog tick, NaN == armed but never fires (JSC comparison semantics), finite values saturate on overflow. One arming fires at most once (watchdog_fired_ns); re-arming fires again.
  • Documented platform delta: zig-js's host-watchdog contract treats termination as terminal for the context, whereas JSC allows VM reuse after clearHasTerminationRequest. Called out in the code and in docs/abi/README.md rather than hidden.
  • No symbol-only stubs (#259): the lock really serializes native threads; the watchdog really interrupts running evaluation (fixture proves both).

Deferred from the VM-lifecycle cluster (rationale recorded on #163): JSC__VM__create/deinit/deferGC and the Zig__GlobalObject__create*/destructOnExit trio.

Sign in to comment on this issue.