ReviewOS

zig-utils/zig-js

ObjC: replace libffi with an owned native ABI dispatcher

#463
Open chris opened this 8 days ago ·
8 days ago

Parent: #459 Depends on: #462

Goal

Remove the Objective-C bridge's libffi dependency and implement the required native-call ABI machinery in zig-utils-owned Zig/assembly code.

Today src/objc_bridge.m includes <ffi/ffi.h> and Objective-C bridge tests link -lffi. This violates the owned-dependency target even though the core engine is pure Zig.

Scope

  • Inventory every Objective-C/block signature the pinned macOS bridge profile accepts: integer widths/signs, floats, doubles, pointers/objects/selectors/classes, void, and supported struct returns/arguments (CGPoint, CGSize, CGRect, NSRange, including ABI classification).
  • Define a stable Zig call descriptor and architecture-specific lowering for supported macOS AArch64 and x86-64 ABIs.
  • Preserve block invocation, exported-method dispatch, exceptions, autorelease/lifetime behavior, reentrancy, recursive/cyclic Foundation conversion, and failure injection.
  • Reject unsupported encodings explicitly before invocation; do not silently reinterpret, truncate, or route through a slower foreign library.
  • Keep Foundation/Objective-C as the platform API being implemented, not as a source of JavaScript execution.

No-workaround rules

  • Do not vendor or dynamically load libffi.
  • Do not shell out, use generated C trampolines per call, restrict the published profile to avoid hard signatures, or retain a hidden -lffi fallback.
  • Do not use undefined Zig calling-convention/layout behavior; ABI conformance needs byte-level fixtures.

Acceptance

  • No ffi/ffi.h, ffi_*, -lffi, or linkSystemLibrary("ffi") remains.
  • Header, runtime, lifetime, sanitizer, and fault-injection Objective-C suites pass on every declared macOS architecture.
  • Differential transcripts against the pinned system JSC bridge remain exact.
  • ABI fixtures cover register/stack splits, alignment, mixed scalar/struct signatures, return conventions, exceptions, and nested/reentrant calls.
  • The owned dispatcher has focused microbenchmarks and does not regress bridge call throughput by more than the accepted noise gate.

Foundation

The completed bridge inventory and semantics are tracked by #138 and its children; preserve that contract exactly.

8 days ago

Implementation landed on main in 63ccdb26.

What changed:

  • removed ffi/ffi.h, every ffi call/preparation path, every -lffi, and every build-system ffi link;
  • added a stable 56-byte C/Zig descriptor and Zig classification for all pinned scalar, pointer/object/class/selector, CGPoint, CGSize, CGRect, and NSRange encodings;
  • added unwind-safe owned Darwin AArch64 and x86-64 assembly leaves for register/stack loading, calls, and scalar/HFA/sret capture;
  • kept arbitrary argument counts via caller-sized scratch, explicit pre-invocation rejection, one failure-atomic allocation, and no fallback/trampoline generation;
  • added focused ABI and benchmark runners.

Verification is green: native arm64 and Rosetta x86-64 register/stack/aggregate/sret/pointer/reentry/exception/fail-before-entry fixtures; Debug, ReleaseSafe, and TSan; complete Objective-C evidence matrix; exact 18-row system-JSC transcript; ASan/UBSan; 200-cycle lifetime stress; deterministic faults; 0 leaked bytes.

Performance is the only remaining checkbox before closure. An order-balanced 7-pair exact-parent diagnostic on battery showed block 0.998x parent (candidate RSD 8.81%, noisy) and JSExport 1.027x parent (RSD 1.95%). Per the evidence policy I am not promoting that battery run or calling the <=10% gate passed. #463 stays open for the quiet AC-powered exact-parent run; the raw diagnostic remains temporary.

8 days ago

A second controlled exact-parent diagnostic completed on the Apple M3 Pro reference host (macOS 27.0 build 26A5388g, Zig 0.17.0-dev.1441+d5181a9). It compared pre-change libffi revision 6adf47fe with owned-dispatcher revision 63ccdb26, using identical benchmark source SHA-256 3254dca5e929885b4ab6058bb1f43d106b5970f00663200f5c269acce0985e47, 200,000 calls per process, seven alternating parent/candidate pairs, and exact checksum 50,991,744 in every sample.

Results:

  • typed block: parent median 2,105,581,166 ns, 3.84% RSD; candidate median 2,039,975,083 ns, 1.34% RSD; candidate/parent 0.9688 (3.12% faster).
  • JSExport: parent median 9,008,698,334 ns, 4.98% RSD; candidate median 8,874,635,792 ns, 6.87% RSD; candidate/parent 0.9851 (1.49% faster).

The source was AC, low-power mode was disabled, but the battery remained discharging. The candidate JSExport row exceeds the 5% RSD quiet-reference limit. I am therefore not promoting this diagnostic, not discarding/retrying samples until they pass, and not checking the performance box or closing #463. The medians show no apparent regression, but the strict evidence gate remains open for a genuinely stable powered run.

8 days ago

Reconciled the acceptance checklist against the implementation evidence already recorded in this issue. Commit 63ccdb26 removed every libffi path; native arm64 and Rosetta x86-64 ABI/runtime/lifetime/sanitizer/fault fixtures, exact 18-row system-JSC differential transcripts, register/stack/aggregate/sret/reentry/exception coverage, and leak checks are all complete. Those four boxes are now checked.

The performance box remains deliberately open. The latest exact-parent diagnostic had a 6.87% candidate RSD on JSExport and a discharging battery, so it does not satisfy the strict quiet-reference evidence policy despite showing no apparent median regression. No sample was discarded, no threshold changed, and #463 remains open for one valid powered run.

Sign in to comment on this issue.