ffi M3.2: SxSceneDelegate migrated + #implements protocol conformance

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.
This commit is contained in:
agra
2026-05-26 07:37:14 +03:00
parent 66f84f67b8
commit 066840d9e0
3 changed files with 56 additions and 43 deletions

View File

@@ -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();

View File

@@ -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, &reg_args, 1, "");

View File

@@ -878,3 +878,7 @@ entry:
}
declare ptr @object_getClass(ptr)
declare ptr @objc_getProtocol(ptr)
declare i8 @class_addProtocol(ptr, ptr)