Skip to content

fix(ios): restore backend ABI parity, and gate it in CI - #108

Merged
nicolas-maman merged 1 commit into
mainfrom
fix/uikit-backend-parity
Sep 7, 2026
Merged

fix(ios): restore backend ABI parity, and gate it in CI#108
nicolas-maman merged 1 commit into
mainfrom
fix/uikit-backend-parity

Conversation

@nicolas-maman

Copy link
Copy Markdown
Contributor

What I found

While checking whether #22's UIKit backend was actually complete, I compared the
ABI in aether_ui_backend.h against what each backend defines. Seven entry
points were on AppKit, GTK4 and Win32 but not on UIKit:

aether_ui_app_quit_impl
aether_ui_set_width_impl / aether_ui_set_height_impl
aether_ui_get_width_impl / aether_ui_get_height_impl
aether_ui_canvas_draw_image_borrowed_impl
aether_ui_canvas_draw_image_scaled_borrowed_impl
aether_ui_modifiers_impl

An iOS app calling any of them fails at link.

Why nothing caught it

This is the part worth fixing properly. Phase 1e compiles the UIKit backend and
links it against tests/ios/link_stub.c, which proves that everything it
references resolves. It says nothing about what it omits. The Win32 lane
only cross-compiles. So a missing entry point is invisible to every phase in
CI, because nothing in CI calls it.

The fix

All seven implemented. Two are documented no-ops with a stated reason, which
is different from a gap:

  • app_quit does nothing on iOS. Terminating your own app is grounds for
    App Store rejection; the platform expects the user to leave via the home
    gesture. A cross-platform app with a Quit item now links and no-ops rather
    than failing to build for iOS.
  • modifiers returns 0. A touch carries no modifiers, and UIKit has no
    pollable global modifier state. A hardware keyboard's modifiers arrive
    attached to the key event (UIKey.modifierFlags), which is a different
    question from "what is held right now".

get_width / get_height round the frame's edges rather than its size, so
iOS carries the same tiling guarantee the other three backends got in #101.

The gate

tests/scripts/check_backend_parity.py, wired in as phase 1c2: every function
declared in aether_ui_backend.h must be defined either in the shared sources
(aether_ui_system_extras.c and friends, which every backend links) or on all
four
backends.

It distinguishes a definition from a declaration by scanning from the name to
whichever of ; or { comes first, rather than with one regex. That matters:
my first attempt used a single pattern and reported Win32 as missing
notify_impl, which Win32 defines at line 9779. A non-greedy match starting
earlier had swallowed it. A checker that under-reports is worse than none.

Verification

  • Parity check passes: every ABI entry point on all four backends.
  • Verified it fails correctly: removing the four size functions again makes
    it report all four and exit 1.
  • aeb .all.ae builds clean on AppKit, zero warnings.
  • The UIKit file itself cannot be compiled here (no iOS SDK on this machine,
    only CommandLineTools), so phase 1e's -Wall -Werror simulator build is the
    compile check. The additions mirror the AppKit implementations and the file's
    existing idioms (static const char association keys, objc_setAssociatedObject).

On #22

With this merged, every item in #22's work list is implemented and the ABI is
complete on UIKit. Its stated prerequisite, aether-lang-dev/aether#1385 (the
iOS --emit=lib target), is still open and is the remaining blocker for
actually shipping an app, so I have left #22 open rather than closing it from
here.

🤖 Generated with Claude Code

Seven entry points were implemented on AppKit, GTK4 and Win32 but not on
UIKit: app_quit, set_width / set_height, get_width / get_height, the two
borrowed-pixel blit variants, and modifiers. An iOS app calling any of them
would have failed at link.

Nothing caught it, and that is the more important half. The iOS phase
compiles the backend and links it against a stub, which proves that what it
REFERENCES resolves and says nothing about what it omits; the Win32 lane
only cross-compiles. A missing entry point is invisible to every phase
because nothing in CI calls it.

All seven are implemented. Two are documented no-ops with a stated reason
rather than a silent gap: iOS has no programmatic quit, since terminating
your own app is grounds for App Store rejection, and modifiers is always 0
because a touch carries none and UIKit has no pollable global modifier state
(a hardware keyboard's modifiers arrive attached to the key event, which is
a different question from what is held right now). get_width / get_height
round the frame's EDGES rather than its size, so iOS carries the same tiling
guarantee the other backends got in #101.

tests/scripts/check_backend_parity.py holds the line: every function
declared in aether_ui_backend.h must be defined in the shared sources or on
all four backends. It distinguishes a definition from a declaration by
scanning from the name to whichever of `;` or `{` comes first, not by a
single regex, because a regex lets one match swallow a later one: the first
version of this check reported Win32 as missing notify_impl, which it
defines. Verified both ways, by removing the four size functions again and
confirming the gate fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nicolas-maman
nicolas-maman merged commit 785ba05 into main Sep 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant