ffi 3.2 C1: migrate uikit.sx Foundation utility cluster to #objc_class

First of five Phase-3.2 migration clusters. Foundation utility
classes (NSValue, NSNumber, NSDictionary, NSMutableDictionary, NSSet)
in `library/modules/platform/uikit.sx` move from the explicit
`#objc_call(T)(recv, "selector:", args)` form to declarative
`#foreign #objc_class("Cls") { ... }` blocks with `recv.method(args)`
dispatch.

Classes declared (all near the top of uikit.sx, after the CGRect
struct):

- NSValue          → CGRectValue (instance)
- NSNumber         → numberWithBool (class), doubleValue +
                     unsignedLongValue (instance)
- NSDictionary     → objectForKey (instance)
- NSMutableDictionary → dictionary (class), setObject_forKey (instance)
- NSSet            → anyObject (instance)

Each call site casts the `*void` receiver to the typed foreign-class
pointer before dispatch — the existing `*void` shape is preserved
in the struct fields and outer parameter types; only the dispatch-
local copy is typed. This keeps the diff scoped to call-site
rewrites without rippling type changes through every consumer.

The four `objc_getClass(...)` calls that previously resolved
NSMutableDictionary / NSNumber at runtime are gone — Phase 3.1's
`__sx_objc_class_init` constructor populates the cached class slot
for each declared `#objc_class` at module load via
`OBJC_CLASSLIST_REFERENCES_<Cls>`.

166/166 example tests; chess clean on macOS + Android via
`tools/verify-step.sh` (iOS sim skipped — no booted simulator in
this run; previous full run was green at HEAD~6).
This commit is contained in:
agra
2026-05-25 17:10:23 +03:00
parent a32cc2dc27
commit 1ea9cda12b
2 changed files with 68 additions and 23 deletions

View File

@@ -543,8 +543,18 @@ one fixture. Both `.txt` and `.ir` snapshots locked — a change to
`deriveObjcSelector` produces one diff that surfaces every affected
case at once via the `OBJC_METH_VAR_NAME_*` constants in the IR.
Open work, in roughly the order they make sense:
- **Phase 3 step 3.2 — C1..C5** — uikit.sx migration, one cluster
Phase 3.2 C1 landed: Foundation utility cluster in uikit.sx
migrated to declarative `#objc_class` bodies. Five classes declared
near the top of the file (NSValue, NSNumber, NSDictionary,
NSMutableDictionary, NSSet). Call sites rewritten from
`#objc_call(T)(recv, "sel:", args)` to `recv.method(args)` /
`Cls.method(args)`. Receivers cast from `*void` to the typed
foreign-class pointer at the dispatch site. The `objc_getClass(...)`
calls for these classes are gone — the class slot is now populated
by emit_llvm's `__sx_objc_class_init` constructor (Phase 3.1).
Open work:
- **Phase 3 step 3.2 — C2..C5** — uikit.sx migration, one cluster
per commit, chess regression after each.
test for the default-mangling table. Escape hatch for selectors
that don't fit the underscore-split rule (e.g. `tableView_

View File

@@ -52,6 +52,38 @@ CGRect :: struct {
height: f64;
}
// ── Foundation utility classes (Phase 3.2 C1) ─────────────────────────
// Declarative `#objc_class` bindings replace the previous
// `objc_getClass(...) + #objc_call(T)(...)` pattern. Each method's sx-
// side name maps to its Obj-C selector via the default mangling rule
// (split on `_`; each piece becomes a keyword with `:`).
NSValue :: #foreign #objc_class("NSValue") {
// CGRect unboxing — returns by value via the sret/HFA path.
CGRectValue :: (self: *Self) -> CGRect;
}
NSNumber :: #foreign #objc_class("NSNumber") {
// Class method (no `self: *Self` first param → static dispatch).
numberWithBool :: (b: s8) -> *NSNumber;
// Instance value extractors.
doubleValue :: (self: *Self) -> f64;
unsignedLongValue :: (self: *Self) -> u64;
}
NSDictionary :: #foreign #objc_class("NSDictionary") {
objectForKey :: (self: *Self, key: *void) -> *void;
}
NSMutableDictionary :: #foreign #objc_class("NSMutableDictionary") {
dictionary :: () -> *NSMutableDictionary;
setObject_forKey :: (self: *Self, obj: *void, key: *void);
}
NSSet :: #foreign #objc_class("NSSet") {
anyObject :: (self: *Self) -> *void;
}
// GLenum constants for renderbuffer/framebuffer setup that aren't in opengl.sx's
// loader path (they live on the framework's symbol table directly).
GL_RENDERBUFFER :u32: 0x8D41;
@@ -356,23 +388,28 @@ uikit_keyboard_will_change_frame :: (self: *void, _cmd: *void, notification: *vo
if g_uikit_plat == null { return; }
plat := g_uikit_plat;
user_info := #objc_call(*void)(notification, "userInfo");
if user_info == null { return; }
user_info_raw := #objc_call(*void)(notification, "userInfo");
if user_info_raw == null { return; }
user_info : *NSDictionary = xx user_info_raw;
end_value := #objc_call(*void)(user_info, "objectForKey:",
ns_string("UIKeyboardFrameEndUserInfoKey".ptr));
if end_value == null { return; }
end_rect := #objc_call(CGRect)(end_value, "CGRectValue");
end_value_raw := user_info.objectForKey(ns_string("UIKeyboardFrameEndUserInfoKey".ptr));
if end_value_raw == null { return; }
end_value : *NSValue = xx end_value_raw;
end_rect := end_value.CGRectValue();
dur_value := #objc_call(*void)(user_info, "objectForKey:",
ns_string("UIKeyboardAnimationDurationUserInfoKey".ptr));
dur_value_raw := user_info.objectForKey(ns_string("UIKeyboardAnimationDurationUserInfoKey".ptr));
anim_dur : f64 = 0.0;
if dur_value != null { anim_dur = #objc_call(f64)(dur_value, "doubleValue"); }
if dur_value_raw != null {
dur_value : *NSNumber = xx dur_value_raw;
anim_dur = dur_value.doubleValue();
}
curve_value := #objc_call(*void)(user_info, "objectForKey:",
ns_string("UIKeyboardAnimationCurveUserInfoKey".ptr));
curve_value_raw := user_info.objectForKey(ns_string("UIKeyboardAnimationCurveUserInfoKey".ptr));
curve_int : u64 = 0;
if curve_value != null { curve_int = #objc_call(u64)(curve_value, "unsignedLongValue"); }
if curve_value_raw != null {
curve_value : *NSNumber = xx curve_value_raw;
curve_int = curve_value.unsignedLongValue();
}
// Screen height in points. The window lives on the connected scene's screen.
if plat.window == null { return; }
@@ -520,10 +557,7 @@ uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) {
// EAGLContext.renderbufferStorage:fromDrawable: (color format,
// non-retained backing). Without this dict the renderbuffer
// allocation silently fails and the framebuffer reports INCOMPLETE.
NSMutableDictionary := objc_getClass("NSMutableDictionary".ptr);
NSNumber := objc_getClass("NSNumber".ptr);
ns_no := #objc_call(*void)(NSNumber, "numberWithBool:", xx 0);
ns_no := NSNumber.numberWithBool(0);
// The EAGL dict keys/values must be the framework-provided NSString
// constants (pointer identity is checked) — dlsym them from OpenGLES.
@@ -531,10 +565,10 @@ uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) {
colorformat_key := uikit_extern_nsstring("kEAGLDrawablePropertyColorFormat".ptr);
rgba8_value := uikit_extern_nsstring("kEAGLColorFormatRGBA8".ptr);
dict := #objc_call(*void)(NSMutableDictionary, "dictionary");
#objc_call(void)(dict, "setObject:forKey:", ns_no, retained_key);
#objc_call(void)(dict, "setObject:forKey:", rgba8_value, colorformat_key);
#objc_call(void)(plat.gl_layer, "setDrawableProperties:", dict);
dict := NSMutableDictionary.dictionary();
dict.setObject_forKey(xx ns_no, retained_key);
dict.setObject_forKey(rgba8_value, colorformat_key);
#objc_call(void)(plat.gl_layer, "setDrawableProperties:", xx dict);
}
// EAGLContext + load_gl were already done in uikit_create_gl_context()
@@ -732,7 +766,8 @@ uikit_touch_location :: (touch: *void, view: *void) -> Point {
}
uikit_first_touch :: (touches: *void) -> *void {
#objc_call(*void)(touches, "anyObject");
touches_set : *NSSet = xx touches;
touches_set.anyObject();
}
uikit_gl_view_touches_began :: (self: *void, _cmd: *void, touches: *void, event: *void) callconv(.c) {