ReviewOS

also looking at this

craft-native/craft

iOS Phase 3: Tier-0 actions in Zig — 19 migrated, ratchet 105 → 86

#85
Merged glennmichael123 wants to merge feat/ios-phase3-tier0 into main
16 files +5086 -78
10 days ago

Follow-on to #83 and #84. The first phase where the migration visibly moves: 19 actions cross from the Swift spec into Zig, the conformance ratchet drops from 105 to 86, and the seam that makes it safe is now wired into the real CraftApp.swift.

The seam, wired and proven both ways

CraftApp.swift's dispatcher splits: the WKScriptMessageHandler entry becomes a thin wrapper, and the action switch is reachable from both the page and CraftSwiftShim.handleAction. Hand-off replies carry a synthetic zig:<id>:<action> callbackId that the reply helpers route back through Zig — one reply path, owned by whichever side received the page's message.

Discovery is symmetric and runtime-only: Zig finds the shim by objc_getClass; the shim finds Zig's exports by dlsym. Neither links the other, so a pure-Swift app and a fully-migrated app both build with no dead dependency. Whether dlsym finds statically-linked Zig exports was the design's load-bearing assumption — now a simulator assertion, not a hope.

Rejections get their own export (craft_ios_deliver_error → __craftBridgeError), so a shim rejection rejects the page's promise. Delivering it as a result would run the app's then-branch with an error-shaped object — fabricated success wearing a different hat. The fixture pushes a message containing a backslash, a quote, and a newline through it and asserts all three arrive intact. (Fixing that also fixed the pre-existing Swift escaping bug: rejectCallback escaped ' and \n but not backslash.)

Six new modules, 19 actions

moduleactions
clipboardclipboardRead, clipboardWrite
hapticshaptic, vibrate
devicegetMemoryUsage, getAppState, getNetworkStatus, log
systemopenURL, share, requestReview
displaysetBadge, clearBadge, setKeepAwake, lockOrientation, unlockOrientation
storagesetSharedItem, getSharedItem, removeSharedItem

Each was written against the Swift implementation and the injected JS read together — the JS is what shows the exact field names a page sends and the reply shape it consumes, which is the part this repo has repeatedly shipped wrong. Where the spec surprises, the modules follow the spec: storage is Keychain (not NSUserDefaults), and getAppState replies with a bare JSON string because Swift serialises with .fragmentsAllowed.

An adversarial review pass per module found real defects before they shipped, including a fabricated success reachable from a page: a clipboard write containing a NUL byte truncated inside stringWithUTF8String:, bumped changeCount, and replied true for a write that stored a different string than the page sent. Refused as InvalidParameter now. Also an unchecked nil that would have raised NSInvalidArgumentException — an ObjC exception Zig cannot catch.

Two build-system defects found and fixed

Module tests were not running at all. Zig collects tests only from the root module — test { _ = import } chains through a named-module import enroll nothing. Proven with a canary: a deliberately failing test in a module, and a green build, under two successive "fixes" that didn't work. The arrangement that does: a test artifact rooted at src/ios.zig. The canary now fails the build and passes when removed.

The iOS archives now bundle compiler-rt. They're linked by clang/swiftc, not Zig, and std.json's number parsing wants f128 soft-float intrinsics no later link step provides.

Verification

Ten fixture assertions on an iPhone 17 Pro simulator (iOS 26.5), each individually proven non-vacuous at some point in this branch's history:

ok: page -> native (i=1 seen 1x)
ok: native -> page, round trip closed (i=2 seen 1x)
ok: no feedback loop
ok: user script ran at document start
ok: unserved action reached the dispatcher (i=4 seen 1x)
ok: hand-off to host shim closed (i=5 seen 1x)
ok: rejecting action reached the dispatcher (i=6 seen 1x)
ok: error route closed with escaping intact (i=7 seen 1x)
ok: Tier-0 action served by Zig with a live UIKit value (i=8 seen 1x)
PASS

Plus: zig build test / test:ios / desktop / build-ios / build-ios-simulator / build-android, zig fmt --check, verify:cimports, pickier, and CraftApp.swift type-checks via the new packages/ios/scripts/typecheck-template.sh (exit-code based — the file is full of error: argument labels that a grep counts as compiler errors).

The ratchet at 86 only goes down from here. Remaining: the permission/Keychain tier (async replies), presented UI, subscriptions — per docs/ios-development.md.

16 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.