Moves startAR, stopAR, placeARObject, removeARObject and getARPlanes
to Zig. Ratchet 18 → 13.
This was the last group whose deferral reason was an effort judgement rather
than a wall — "SceneKit node-graph glue; miserable and low-value through
objc_msgSend". The node graph turned out not to be the hard part.
The actual risk: three struct returns, not the node graph
Every previously migrated module passes and returns pointers and integers. This one passes and returns structs by value in floating-point registers, three different ways:
| Type | Used for | Return rule on arm64 |
|---|---|---|
CGRect | UIScreen.main.bounds, -initWithFrame: | HFA of 4 doubles → v0–v3 |
SCNVector3 | SCNNode.position, eulerAngles | HFA of 3 → v0–v2 |
simd_float3 | ARPlaneAnchor.center / .extent | 16 bytes in v0 alone |
The third uses a different rule from the other two. Getting it wrong does not crash — it yields plausible coordinates that ARKit never measured, which is a wrong answer delivered as a right one.
So none of the three is asserted from the headers and hoped for. SceneKit
ships on macOS even though ARKit does not, which makes SCNNode, SCNBox and
— crucially — simdPosition reachable from a host test. All three round-trip
against real objects in zig build test:ios.
The simd_float3 guard was mutation-tested: swapping @Vector(4, f32) for a
four-field extern struct makes it read y = 0 and z = 0 while x still
agrees, and the test fails exactly there. It bites.
What this cannot verify, and won't pretend to
ARWorldTrackingConfiguration.isSupported is false in the simulator, so
startAR refuses there and every line past that guard needs hardware I don't
have. The tests cover argument parsing, the reply shapes, the plane JSON, the
refusal paths and the three ABI mechanisms. They do not cover a running AR
session, and no test in this repository can.
One further limit, written into the module rather than glossed: SceneKitTypes.h
declares SCNVector3 as three CGFloats on macOS and three floats on iOS
(checked in both SDKs). The alias follows the target, so the host test verifies
the mechanism at double width, not the exact iOS layout.
Two details that are easy to miss, and are ported
extentcontributes x and z, mapped towidthandheight. A plane'syextent never reaches the page at all.positionis a[String: Double]cast of the whole dictionary. It fails as a unit, so{"x":1,"y":"up"}is not "x with a bad y" — it is no position, and the node lands at the default(0, 0, -0.5). A reader that keptxwould place the object somewhere Swift never would. Pinned by its own test.
Divergences
INVALID_ARGUMENT→INVALID_PARAMETER, and "AR not supported" →PLATFORM_NOT_SUPPORTED.BridgeErrorcarries an enum, not free text — the same tradebridge_mobile_misc.zigandbridge_mobile_watch.zigdocument.- Reply ordering is fixed, not preserved.
getARPlanesis the only one of the five Swift does not wrap inDispatchQueue.main.async, so astartARimmediately followed by agetARPlaneshas the planes reply first. Both are synchronous here, so replies keep the order the page sent them in. - All five carry
config.enableARin the spec — unlike the recorder, where only the start is gated — so all five are added toios_config.gateFor. The conformance test that assertedgateFor("startAR") == nullis inverted rather than deleted: it now asserts the gate is there.
Verification
zig build test:ios— 7/7 steps, 1083/1095 passed, 12 skipped, 0 failed.zig build test— 1454/1454 passed. The one failing step isinjected_js_test.zigcompiling against a localzig-jscheckout that is incompatible with the pinned toolchain; it fails identically onmain, checked before claiming it.- No Swift template change: the five
casearms stay as the spec, and Zig answers first — the same arrangementreadLocationRecordinguses.