ffi M3.1 + M1.2 A.3 refactor: self=Obj-C id, self.field via ivar; SxAppDelegate migrated

Two coupled changes that unblock the uikit_register_classes
migration:

1) M1.2 A.3 — body's 'self' is the Obj-C id (opaque), NOT the
   state struct. Matches Apple's ObjC semantics where 'self' IS
   the object. Cocoa idiom 'xx self → id' works at runtime calls
   (addObserver:, etc.); previously the trampoline replaced
   'self' with the state-struct pointer, breaking any runtime
   call that expected an id.

   '*Self' substitution in resolveTypeWithBindings now points at
   foreignClassStructType(fcd) — the opaque class stub — instead
   of objcDefinedStateStructType(fcd).

   'self.field' access on a sx-defined class instance field is
   rewritten by lowerFieldAccess to go through the __sx_state
   ivar:
     state = object_getIvar(self, load(__<Cls>_state_ivar))
     val   = struct_gep(state, field_idx) → load

   Both read (lowerFieldAccess) and write (lowerAssignment) take
   this path. Compound ops (+=, -=, etc.) are supported via
   storeOrCompound. The lookup is filtered: skip property fields
   (those still go through the M2.2 msgSend getter/setter
   dispatch) and foreign classes (no state).

   New helpers in lower.zig:
   - lookupObjcDefinedStateFieldOnPointer — match check.
   - lowerObjcDefinedStateForObj — emit the object_getIvar +
     ivar-global-load idiom (shared between read + write paths).
   - lowerObjcDefinedStateFieldRead — the load path.

   Also moved the @llvm.global_ctors registration out of the
   sx-defined class-pair init constructor — global_ctors fires
   DURING dyld's framework load, before UIKit registers its Obj-C
   classes. objc_getClass("UIResponder") returned null, super
   was null, objc_registerClassPair crashed. main's entry block
   is post-framework-load but pre-user-code — exactly the right
   window. New helper injectCtorIntoMain.

2) M3.1 — SxAppDelegate migrated to declarative #objc_class.
   uikit_register_classes' hand-rolled objc_allocateClassPair +
   class_addMethod for SxAppDelegate is gone; the compiler
   synthesises the class at module init. The method bodies
   forward to the existing legacy IMP free functions
   (uikit_did_finish_launching, uikit_keyboard_will_change_frame)
   so we don't have to inline 70+ lines of keyboard-frame logic
   right now.

   Also adds UIResponder foreign-class declaration and chains
   UIView / UITextField to it via #extends UIResponder so the
   methods that previously lived on UITextField directly
   (becomeFirstResponder etc.) move to their proper home.

Chess on iOS-sim: board renders, full state intact. 183 example
tests + zig build test green.
This commit is contained in:
agra
2026-05-26 07:32:57 +03:00
parent ea32f8a27a
commit 66f84f67b8
4 changed files with 222 additions and 63 deletions

View File

@@ -735,8 +735,17 @@ pub const LLVMEmitter = struct {
}
_ = c.LLVMBuildRetVoid(self.builder);
// Register in @llvm.global_ctors + inject into main for ORC JIT.
self.appendModuleCtor(ctor, ctor_ty);
// Inject the call into main's entry block ONLY — skip
// @llvm.global_ctors. Apple's frameworks (UIKit on iOS,
// AppKit on macOS) register their Obj-C classes during
// dyld's image-init phase, which overlaps global_ctors. If
// we ran there too, `objc_getClass(\"UIResponder\")` would
// return null and `objc_allocateClassPair(null, ...)` would
// crash inside objc_registerClassPair. main's entry runs
// AFTER dyld's framework init is complete but BEFORE user
// code (UIApplicationMain), so the runtime sees the parent
// class properly.
self.injectCtorIntoMain(ctor, ctor_ty);
_ = i32_ty;
}
@@ -778,6 +787,30 @@ pub const LLVMEmitter = struct {
/// global if not present, extending the array if so) AND inject a
/// direct call from `main`'s entry block so the ORC JIT path runs
/// the constructor too.
/// Inject a call to `ctor()` at the start of `main`'s entry block
/// (past any existing init calls). Used by class-pair init etc.
/// that need to run BEFORE user code but AFTER dyld's framework
/// load — global_ctors is too early because Apple frameworks
/// (UIKit etc.) register their Obj-C classes during their own
/// init phase that overlaps ours.
fn injectCtorIntoMain(self: *LLVMEmitter, ctor: c.LLVMValueRef, ctor_ty: c.LLVMTypeRef) void {
const main_z = "main";
const main_fn = c.LLVMGetNamedFunction(self.llvm_module, main_z);
if (main_fn == null) return;
const entry_bb = c.LLVMGetEntryBasicBlock(main_fn);
var insert_before = c.LLVMGetFirstInstruction(entry_bb);
while (insert_before != null) : (insert_before = c.LLVMGetNextInstruction(insert_before)) {
if (c.LLVMGetInstructionOpcode(insert_before) != c.LLVMCall) break;
}
if (insert_before != null) {
c.LLVMPositionBuilderBefore(self.builder, insert_before);
} else {
c.LLVMPositionBuilderAtEnd(self.builder, entry_bb);
}
var no_args: [0]c.LLVMValueRef = .{};
_ = c.LLVMBuildCall2(self.builder, ctor_ty, ctor, &no_args, 0, "");
}
fn appendModuleCtor(self: *LLVMEmitter, ctor: c.LLVMValueRef, ctor_ty: c.LLVMTypeRef) void {
const i32_ty = self.cached_i32;
const ptr_ty = self.cached_ptr;