From 066840d9e0702a5d3e895a08d9277e07ef878785 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 26 May 2026 07:37:14 +0300 Subject: [PATCH] ffi M3.2: SxSceneDelegate migrated + #implements protocol conformance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates SxSceneDelegate from the hand-rolled objc_allocateClassPair + class_addMethod + class_addProtocol sequence to the declarative form: SxSceneDelegate :: #objc_class("SxSceneDelegate") { #extends UIResponder; #implements UISceneDelegate; #implements UIWindowSceneDelegate; scene_willConnectToSession_options :: (self, scene, session, options) { ... } window :: (self) -> *void { ... } setWindow :: (self, w) { ... } } emit_llvm now honors '#implements' in the class-pair init constructor — for each #implements ProtocolAlias on the cache entry's AST, emit before objc_registerClassPair: proto = objc_getProtocol("ProtocolName") class_addProtocol(cls, proto) iOS checks 'class_conformsToProtocol' when instantiating scene delegates; without the conformance the runtime silently rejects the class and a default scene with no delegate gets created instead. The protocol-getter returns null on dead-strip / runtime mismatch (rare but possible) — the runtime treats class_addProtocol(cls, null) as a no-op, so no explicit null check needed. Method bodies forward to the existing legacy free IMP functions (uikit_scene_will_connect, uikit_window_getter, uikit_window_setter) so we don't have to inline the scene- connect setup logic (~80 lines). uikit_register_classes is now tiny — just the two remaining view-class helpers (M3.3 SxGLView + M3.4 SxMetalView). M3.5 deletes the function entirely once those land. Chess on iOS-sim: board renders, scene delegate fires, touch events route correctly. 183 example tests + zig build test green. --- library/modules/platform/uikit.sx | 73 ++++++++----------- src/ir/emit_llvm.zig | 22 ++++++ .../142-objc-class-method-lowering.ir | 4 + 3 files changed, 56 insertions(+), 43 deletions(-) diff --git a/library/modules/platform/uikit.sx b/library/modules/platform/uikit.sx index 7c7f1eb..17d4752 100644 --- a/library/modules/platform/uikit.sx +++ b/library/modules/platform/uikit.sx @@ -197,6 +197,30 @@ SxAppDelegate :: #objc_class("SxAppDelegate") { init :: (self: *SxAppDelegate) -> *SxAppDelegate; } +// SxSceneDelegate — iOS 13+ scene-based lifecycle delegate. +// UIApplicationSceneManifest names this in Info.plist; iOS +// instantiates it via scene-session connection. Replaces the M3.2 +// hand-rolled registration. Two `#implements` declarations +// formally conform to the scene-delegate protocols — iOS rejects +// the class otherwise. +SxSceneDelegate :: #objc_class("SxSceneDelegate") { + #extends UIResponder; + #implements UISceneDelegate; + #implements UIWindowSceneDelegate; + + scene_willConnectToSession_options :: (self: *Self, scene: *void, session: *void, options: *void) { + uikit_scene_will_connect(xx self, xx 0, scene, session, options); + } + + window :: (self: *Self) -> *void { + return uikit_window_getter(xx self, xx 0); + } + + setWindow :: (self: *Self, w: *void) { + uikit_window_setter(xx self, xx 0, w); + } +} + // 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; @@ -428,49 +452,12 @@ uikit_chdir_to_bundle :: () { uikit_register_classes :: () { inline if OS == .ios { - // SxAppDelegate is now declared as `#objc_class("SxAppDelegate")` - // (M3.1) — the compiler synthesises its IMPs and class-pair - // registration at module init. The old hand-rolled - // objc_allocateClassPair + class_addMethod sequence is gone. - - UIResponder_cls := objc_getClass("UIResponder".ptr); - - // SxSceneDelegate handles the per-scene UI setup. iOS 13+ scene-based - // lifecycle: didFinishLaunching is too early for the window — the - // UIWindowScene doesn't connect until `scene:willConnectTo:options:`. - // The class is named in Info.plist's UIApplicationSceneManifest → - // UISceneDelegateClassName. - SxSceneDelegate := objc_allocateClassPair(UIResponder_cls, "SxSceneDelegate".ptr, 0); - - class_addMethod(SxSceneDelegate, - sel_registerName("scene:willConnectToSession:options:".ptr), - xx uikit_scene_will_connect, "v@:@@@".ptr); - class_addMethod(SxSceneDelegate, - sel_registerName("window".ptr), - xx uikit_window_getter, "@@:".ptr); - class_addMethod(SxSceneDelegate, - sel_registerName("setWindow:".ptr), - xx uikit_window_setter, "v@:@".ptr); - - // Formal protocol conformance is required for UISceneDelegate - // (iOS checks -[cls conformsToProtocol:@protocol(UISceneDelegate)] - // before instantiating; without it the class is silently rejected - // with "does not conform to the UISceneDelegate protocol" in the - // log and a default scene with no delegate is created instead). - // Add the protocol BEFORE registerClassPair — the runtime locks - // the class layout after registration. - UISceneDelegateProto := objc_getProtocol("UISceneDelegate".ptr); - UIWindowSceneDelegateProto := objc_getProtocol("UIWindowSceneDelegate".ptr); - if UISceneDelegateProto != null { - class_addProtocol(SxSceneDelegate, UISceneDelegateProto); - } else { - NSLog(ns_string("[sx] WARN: UISceneDelegate protocol not found (dead-stripped)\n".ptr)); - } - if UIWindowSceneDelegateProto != null { - class_addProtocol(SxSceneDelegate, UIWindowSceneDelegateProto); - } - - objc_registerClassPair(SxSceneDelegate); + // SxAppDelegate (M3.1) + SxSceneDelegate (M3.2) are now + // declarative `#objc_class(...)` blocks — the compiler + // synthesises their IMPs, class-pair registration, and + // protocol conformances at module init. The old hand-rolled + // objc_allocateClassPair + class_addMethod + class_addProtocol + // sequences are gone. uikit_register_gl_view_class(); uikit_register_metal_view_class(); diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index cdbf5a6..3c778c1 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -654,6 +654,28 @@ pub const LLVMEmitter = struct { _ = c.LLVMBuildCall2(self.builder, add_method_ty, add_method_fn, &add_args, 4, ""); } + // M2.3 / M3.2 — register `#implements` protocol conformances + // BEFORE objc_registerClassPair. iOS checks + // `class_conformsToProtocol` when instantiating scene + // delegates and other protocol-typed callbacks; without + // these the runtime silently rejects the class. + // + // The protocol may not be present on every SDK / runtime + // (dead-strip pruning, version skew), so `objc_getProtocol` + // returning null is non-fatal — skip the addProtocol call. + const get_proto_fn, const get_proto_ty = self.lazyDeclareCRuntime("objc_getProtocol", &[_]c.LLVMTypeRef{ptr_ty}, ptr_ty, 0); + const add_proto_fn, const add_proto_ty = self.lazyDeclareCRuntime("class_addProtocol", &[_]c.LLVMTypeRef{ ptr_ty, ptr_ty }, i8_ty, 0); + for (fcd.members) |m| switch (m) { + .implements => |proto_alias| { + const proto_str_global = self.emitPrivateCString(proto_alias, "OBJC_PROTOCOL_NAME_"); + var gp_args: [1]c.LLVMValueRef = .{proto_str_global}; + const proto_val = c.LLVMBuildCall2(self.builder, get_proto_ty, get_proto_fn, &gp_args, 1, "proto"); + var ap_args: [2]c.LLVMValueRef = .{ cls_val, proto_val }; + _ = c.LLVMBuildCall2(self.builder, add_proto_ty, add_proto_fn, &ap_args, 2, ""); + }, + else => {}, + }; + // objc_registerClassPair(cls) var reg_args: [1]c.LLVMValueRef = .{cls_val}; _ = c.LLVMBuildCall2(self.builder, register_ty, register_fn, ®_args, 1, ""); diff --git a/tests/expected/142-objc-class-method-lowering.ir b/tests/expected/142-objc-class-method-lowering.ir index 4622e6e..0668b5b 100644 --- a/tests/expected/142-objc-class-method-lowering.ir +++ b/tests/expected/142-objc-class-method-lowering.ir @@ -878,3 +878,7 @@ entry: } declare ptr @object_getClass(ptr) + +declare ptr @objc_getProtocol(ptr) + +declare i8 @class_addProtocol(ptr, ptr)