diff --git a/examples/ffi-objc-arc-01-autoreleasepool.sx b/examples/ffi-objc-arc-01-autoreleasepool.sx new file mode 100644 index 0000000..d707f28 --- /dev/null +++ b/examples/ffi-objc-arc-01-autoreleasepool.sx @@ -0,0 +1,50 @@ +// ffi-objc-arc-01 — M4.A smoke test for NSObject + autoreleasepool. +// +// Exercises: +// 1. NSObject is declared in std/objc.sx and reachable from user code. +// `obj.retain()` / `obj.release()` dispatch via the M2.3 #extends-aware +// method chain. Pattern: `defer obj.release();` as the canonical +// sx idiom for owned Obj-C handles. +// 2. `autoreleasepool(body)` stdlib helper wraps `body` in a +// push/defer-pop pair so Foundation factory returns drain at block +// end. +// +// macOS-only — libobjc + NSObject must be available at runtime. + +#import "modules/std.sx"; +#import "modules/std/objc.sx"; +#import "modules/compiler.sx"; + +main :: () -> s32 { + inline if OS == .macos { + // Manual retain/release on an NSObject instance — the + // `defer obj.release();` pattern is the canonical sx idiom. + obj := NSObject.alloc().init(); + if obj == null { print("FAIL: alloc null\n"); return 1; } + defer obj.release(); + + // Bump the count and drop the extra; refcount math stays balanced. + _ = obj.retain(); + obj.release(); + + print("retain/release: ok\n"); + + // autoreleasepool helper round-trip — just exercise that the + // push/pop pair executes. We don't have a side-effect to observe + // (NSObject.new returns a +1 retained, NOT autoreleased), so this + // is a smoke test of the helper's shape, not the runtime + // behavior. + autoreleasepool(() => { + inner := NSObject.new(); + if inner != null { + inner.release(); + } + }); + + print("autoreleasepool: ok\n"); + } + inline if OS != .macos { + print("skipped (not macos)\n"); + } + 0; +} diff --git a/library/modules/platform/uikit.sx b/library/modules/platform/uikit.sx index 58a2e1f..fa65302 100644 --- a/library/modules/platform/uikit.sx +++ b/library/modules/platform/uikit.sx @@ -59,11 +59,13 @@ CGRect :: struct { // (split on `_`; each piece becomes a keyword with `:`). NSValue :: #foreign #objc_class("NSValue") { + #extends NSObject; // CGRect unboxing — returns by value via the sret/HFA path. CGRectValue :: (self: *Self) -> CGRect; } NSNumber :: #foreign #objc_class("NSNumber") { + #extends NSObject; // Class method (no `self: *Self` first param → static dispatch). numberWithBool :: (b: s8) -> *NSNumber; // Instance value extractors. @@ -72,30 +74,36 @@ NSNumber :: #foreign #objc_class("NSNumber") { } NSDictionary :: #foreign #objc_class("NSDictionary") { + #extends NSObject; objectForKey :: (self: *Self, key: *void) -> *void; } NSMutableDictionary :: #foreign #objc_class("NSMutableDictionary") { + #extends NSDictionary; dictionary :: () -> *NSMutableDictionary; setObject_forKey :: (self: *Self, obj: *void, key: *void); } NSSet :: #foreign #objc_class("NSSet") { + #extends NSObject; anyObject :: (self: *Self) -> *void; } // ── Notifications + Bundle (Phase 3.2 C2) ────────────────────────────── NSNotification :: #foreign #objc_class("NSNotification") { + #extends NSObject; userInfo :: (self: *Self) -> *NSDictionary; } NSBundle :: #foreign #objc_class("NSBundle") { + #extends NSObject; mainBundle :: () -> *NSBundle; resourcePath :: (self: *Self) -> *void; } NSNotificationCenter :: #foreign #objc_class("NSNotificationCenter") { + #extends NSObject; defaultCenter :: () -> *NSNotificationCenter; addObserver_selector_name_object :: (self: *Self, observer: *void, sel: *void, name: *void, obj: *void); } @@ -103,10 +111,12 @@ NSNotificationCenter :: #foreign #objc_class("NSNotificationCenter") { // ── RunLoop + display timing (Phase 3.2 C3) ──────────────────────────── NSRunLoop :: #foreign #objc_class("NSRunLoop") { + #extends NSObject; currentRunLoop :: () -> *NSRunLoop; } CADisplayLink :: #foreign #objc_class("CADisplayLink") { + #extends NSObject; displayLinkWithTarget_selector :: (target: *void, sel: *void) -> *CADisplayLink; addToRunLoop_forMode :: (self: *Self, runloop: *NSRunLoop, mode: *void); targetTimestamp :: (self: *Self) -> f64; @@ -117,6 +127,7 @@ CADisplayLink :: #foreign #objc_class("CADisplayLink") { // (Declared before UIView so `layer :: (...) -> *CALayer` can reference it.) CALayer :: #foreign #objc_class("CALayer") { + #extends NSObject; setOpaque :: (self: *Self, opaque: s8); } @@ -132,6 +143,7 @@ CAMetalLayer :: #foreign #objc_class("CAMetalLayer") { } EAGLContext :: #foreign #objc_class("EAGLContext") { + #extends NSObject; alloc :: () -> *EAGLContext; initWithAPI :: (self: *Self, api: s32) -> *EAGLContext; setCurrentContext :: (ctx: *EAGLContext); @@ -142,6 +154,7 @@ EAGLContext :: #foreign #objc_class("EAGLContext") { // ── UIKit chrome (Phase 3.2 C4) ──────────────────────────────────────── UIScreen :: #foreign #objc_class("UIScreen") { + #extends NSObject; mainScreen :: () -> *UIScreen; nativeScale :: (self: *Self) -> f64; bounds :: (self: *Self) -> CGRect; @@ -152,6 +165,7 @@ UIScreen :: #foreign #objc_class("UIScreen") { // participate in lifecycle callbacks (delegates, scene delegates) // extend it so the runtime picks up the responder-chain behavior. UIResponder :: #foreign #objc_class("UIResponder") { + #extends NSObject; becomeFirstResponder :: (self: *Self) -> s8; resignFirstResponder :: (self: *Self) -> s8; } @@ -165,6 +179,7 @@ UIView :: #foreign #objc_class("UIView") { } UIWindow :: #foreign #objc_class("UIWindow") { + #extends UIView; alloc :: () -> *UIWindow; initWithWindowScene :: (self: *Self, scene: *void) -> *UIWindow; setRootViewController :: (self: *Self, vc: *void); @@ -173,6 +188,7 @@ UIWindow :: #foreign #objc_class("UIWindow") { } UIViewController :: #foreign #objc_class("UIViewController") { + #extends UIResponder; alloc :: () -> *UIViewController; init :: (self: *Self) -> *UIViewController; setView :: (self: *Self, view: *void); diff --git a/library/modules/std/objc.sx b/library/modules/std/objc.sx index f6b1c2b..f2d4c9d 100644 --- a/library/modules/std/objc.sx +++ b/library/modules/std/objc.sx @@ -93,3 +93,50 @@ c_string :: (ns: *void) -> [*]u8 { fn_ptr : (*void, *void) -> [*]u8 callconv(.c) = xx objc_msgSend; return fn_ptr(ns, sel); } + +// ─── NSObject (Phase 4 / M4.A) ─────────────────────────────────────────── +// Root of every Obj-C class hierarchy. Apple's runtime supplies the IMPs; +// sx declares the method surface so user code can write +// `defer view.release();` and `view.retain()` directly instead of going +// through `objc_msgSend` casts. M2.3's `#extends`-aware dispatch finds +// these methods automatically once a class roots its `#extends` chain at +// NSObject (foreign classes in uikit.sx etc. add `#extends NSObject;` to +// inherit the surface). +// +// `release` is NOT a reserved keyword in sx — modern clang ARC bans +// user-source calls to it (the ARC compiler emits them automatically), but +// sx isn't under clang ARC. Calling `view.release()` here is equivalent to +// pre-ARC Obj-C source code: dispatches through the runtime, decrements the +// refcount, fires `-dealloc` at zero. +NSObject :: #foreign #objc_class("NSObject") { + alloc :: () -> *NSObject; + init :: (self: *Self) -> *NSObject; + new :: () -> *NSObject; // shorthand for [[Cls alloc] init] + retain :: (self: *Self) -> *Self; + release :: (self: *Self); + autorelease :: (self: *Self) -> *Self; + class :: () -> *void; // metaclass query — `Cls.class()` + description :: (self: *Self) -> *void; // returns *NSString + hash :: (self: *Self) -> u64; + isEqual_ :: (self: *Self, other: *void) -> BOOL; + isKindOfClass_ :: (self: *Self, cls: *void) -> BOOL; + respondsToSelector_ :: (self: *Self, sel: *void) -> BOOL; +} + +// ─── Autoreleasepool (M4.A) ────────────────────────────────────────────── +// Foundation factory methods (`NSString.stringWithUTF8String:`, +// `[NSArray array]`, ...) return autoreleased objects — valid until the +// current pool drains. Wrap such code in `autoreleasepool(() => { ... })` +// so the pool drains deterministically at block end. +// +// Stdlib helper, not a language keyword. The closure call adds a frame — +// for hot loops, inline the push/defer-pop pattern manually. + +objc_autoreleasePoolPush :: () -> *void #foreign objc; +objc_autoreleasePoolPop :: (pool: *void) #foreign objc; + +autoreleasepool :: (body: Closure()) { + pool := objc_autoreleasePoolPush(); + defer objc_autoreleasePoolPop(pool); + body(); +} diff --git a/tests/expected/ffi-objc-arc-01-autoreleasepool.exit b/tests/expected/ffi-objc-arc-01-autoreleasepool.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/ffi-objc-arc-01-autoreleasepool.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/ffi-objc-arc-01-autoreleasepool.txt b/tests/expected/ffi-objc-arc-01-autoreleasepool.txt new file mode 100644 index 0000000..bfb03de --- /dev/null +++ b/tests/expected/ffi-objc-arc-01-autoreleasepool.txt @@ -0,0 +1,2 @@ +retain/release: ok +autoreleasepool: ok diff --git a/tests/expected/ffi-objc-call-06-sret-return.ir b/tests/expected/ffi-objc-call-06-sret-return.ir index e47d423..eddaff3 100644 --- a/tests/expected/ffi-objc-call-06-sret-return.ir +++ b/tests/expected/ffi-objc-call-06-sret-return.ir @@ -903,13 +903,13 @@ entry: i64 30, label %match.arm.44 i64 32, label %match.arm.44 i64 36, label %match.arm.44 - i64 38, label %match.arm.44 + i64 39, label %match.arm.44 i64 34, label %match.arm.45 i64 35, label %match.arm.45 - i64 44, label %match.arm.47 - i64 51, label %match.arm.47 + i64 45, label %match.arm.47 + i64 52, label %match.arm.47 i64 33, label %match.arm.48 - i64 43, label %match.arm.48 + i64 44, label %match.arm.48 i64 17, label %match.arm.49 i64 20, label %match.arm.49 i64 22, label %match.arm.49 @@ -918,17 +918,17 @@ entry: i64 27, label %match.arm.49 i64 29, label %match.arm.49 i64 31, label %match.arm.49 - i64 39, label %match.arm.49 i64 40, label %match.arm.49 i64 41, label %match.arm.49 i64 42, label %match.arm.49 - i64 45, label %match.arm.49 + i64 43, label %match.arm.49 i64 46, label %match.arm.49 i64 47, label %match.arm.49 i64 48, label %match.arm.49 i64 49, label %match.arm.49 i64 50, label %match.arm.49 - i64 52, label %match.arm.49 + i64 51, label %match.arm.49 + i64 53, label %match.arm.49 i64 13, label %match.arm.50 ] @@ -987,7 +987,7 @@ match.arm.44: ; preds = %entry, %entry, %ent i64 30, label %dispatch.case.76 i64 32, label %dispatch.case.77 i64 36, label %dispatch.case.78 - i64 38, label %dispatch.case.79 + i64 39, label %dispatch.case.79 ] match.arm.45: ; preds = %entry, %entry @@ -1007,8 +1007,8 @@ match.arm.47: ; preds = %entry, %entry %loadN = load { i64, i64 }, ptr %alloca, align 8 %allocaN = alloca { ptr, i64 }, align 8 switch i64 %loadN, label %dispatch.default.139 [ - i64 44, label %dispatch.case.140 - i64 51, label %dispatch.case.141 + i64 45, label %dispatch.case.140 + i64 52, label %dispatch.case.141 ] match.arm.48: ; preds = %entry, %entry @@ -1017,7 +1017,7 @@ match.arm.48: ; preds = %entry, %entry %allocaN = alloca { ptr, i64 }, align 8 switch i64 %loadN, label %dispatch.default.153 [ i64 33, label %dispatch.case.154 - i64 43, label %dispatch.case.155 + i64 44, label %dispatch.case.155 ] match.arm.49: ; preds = %entry, %entry, %entry, %entry, %entry, %entry, %entry, %entry, %entry, %entry, %entry, %entry, %entry, %entry, %entry, %entry, %entry, %entry, %entry @@ -1033,17 +1033,17 @@ match.arm.49: ; preds = %entry, %entry, %ent i64 27, label %dispatch.case.173 i64 29, label %dispatch.case.174 i64 31, label %dispatch.case.175 - i64 39, label %dispatch.case.176 - i64 40, label %dispatch.case.177 - i64 41, label %dispatch.case.178 - i64 42, label %dispatch.case.179 - i64 45, label %dispatch.case.180 - i64 46, label %dispatch.case.181 - i64 47, label %dispatch.case.182 - i64 48, label %dispatch.case.183 - i64 49, label %dispatch.case.184 - i64 50, label %dispatch.case.185 - i64 52, label %dispatch.case.186 + i64 40, label %dispatch.case.176 + i64 41, label %dispatch.case.177 + i64 42, label %dispatch.case.178 + i64 43, label %dispatch.case.179 + i64 46, label %dispatch.case.180 + i64 47, label %dispatch.case.181 + i64 48, label %dispatch.case.182 + i64 49, label %dispatch.case.183 + i64 50, label %dispatch.case.184 + i64 51, label %dispatch.case.185 + i64 53, label %dispatch.case.186 ] match.arm.50: ; preds = %entry @@ -1833,6 +1833,15 @@ declare ptr @ns_string(ptr, ptr) #0 ; Function Attrs: nounwind declare ptr @c_string(ptr, ptr) #0 +; Function Attrs: nounwind +declare ptr @objc_autoreleasePoolPush() #0 + +; Function Attrs: nounwind +declare void @objc_autoreleasePoolPop(ptr) #0 + +; Function Attrs: nounwind +declare void @autoreleasepool(ptr, [2 x i64]) #0 + ; Function Attrs: nounwind define internal void @triple_imp(ptr sret({ i64, i64, i64 }) %0, ptr %1, ptr %2) #0 { entry: @@ -3610,3 +3619,5 @@ entry: store ptr %sel, ptr @OBJC_SELECTOR_REFERENCES_tripleValue, align 8 ret void } + + diff --git a/tests/expected/ffi-objc-dsl-07-mangling-table.ir b/tests/expected/ffi-objc-dsl-07-mangling-table.ir index 61290dd..c66c53c 100644 --- a/tests/expected/ffi-objc-dsl-07-mangling-table.ir +++ b/tests/expected/ffi-objc-dsl-07-mangling-table.ir @@ -798,6 +798,15 @@ declare ptr @ns_string(ptr, ptr) #0 ; Function Attrs: nounwind declare ptr @c_string(ptr, ptr) #0 +; Function Attrs: nounwind +declare ptr @objc_autoreleasePoolPush() #0 + +; Function Attrs: nounwind +declare void @objc_autoreleasePoolPop(ptr) #0 + +; Function Attrs: nounwind +declare void @autoreleasepool(ptr, [2 x i64]) #0 + ; Function Attrs: nounwind define internal i32 @universal_imp(ptr %0, ptr %1, i32 %2, i32 %3, i32 %4, i32 %5) #0 { entry: @@ -944,3 +953,5 @@ entry: store ptr %selN, ptr @OBJC_SELECTOR_REFERENCES_actualSelectorName, align 8 ret void } + +