From c88a293cf44c073b0e4df305e818c4d371076ca8 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 26 May 2026 23:04:00 +0300 Subject: [PATCH] ffi M4.B getter: weak property reads through objc_loadWeakRetained MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit emitObjcDefinedPropertyGetter dispatches on objcPropertyKind. The strong/copy/assign paths keep their bare load. The weak path: retained = objc_loadWeakRetained(field_addr) autoreleased = objc_autorelease(retained) return autoreleased `objc_loadWeakRetained` does the race-safe upgrade via libobjc's side-table: if the target has deinitialized (or is mid-dealloc on another thread), returns null; otherwise returns the target with refcount bumped (+1 retained, transferred to caller). `objc_autorelease` drops the +1 into the current pool so the caller doesn't need to manually balance — matches Apple's auto-nil weak-getter contract. The bare-load weak path (still in place pre-M4.B-getter) worked for the single-threaded test scenario because the runtime nils the slot before the load happens. The load-retained version covers the multi-threaded "between load and use, target deinit's" race that silent bare-load can't. 189/189 example tests pass; chess on iOS-sim green. --- src/ir/lower.zig | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 0a4d099..5fa7e49 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -12256,8 +12256,39 @@ pub const Lowering = struct { get_args[1] = ivar_handle; const state_ptr = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void); - // GEP to the field, load. const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state_ptr, .field_index = fidx, .base_type = state_ty } }, ptr_void); + + // M4.B getter — weak fields go through objc_loadWeakRetained + + // objc_autorelease for race-safe reads. The bare-load path + // (strong/copy/assign) is the common case and reads the slot + // directly. + const kind = self.objcPropertyKind(field); + if (kind == .weak) { + self.ensureArcRuntimeDecls(); + const load_weak_fid = self.ensureCRuntimeDecl("objc_loadWeakRetained", &.{ptr_void}, ptr_void); + const autorelease_fid = self.ensureCRuntimeDecl("objc_autorelease", &.{ptr_void}, ptr_void); + + // retained = objc_loadWeakRetained(field_addr) + // - atomic upgrade-to-strong via libobjc's side-table; if the + // target deinitialised, returns null. The caller gets a + // +1 retained reference (or null). + const load_args = self.alloc.alloc(Ref, 1) catch return; + load_args[0] = field_addr; + const retained = self.builder.emit(.{ .call = .{ .callee = load_weak_fid, .args = load_args } }, ptr_void); + + // autoreleased = objc_autorelease(retained) + // - drops it into the current pool so the caller doesn't need + // to manually release. Returns the same pointer (typed). + const ar_args = self.alloc.alloc(Ref, 1) catch return; + ar_args[0] = retained; + const autoreleased = self.builder.emit(.{ .call = .{ .callee = autorelease_fid, .args = ar_args } }, ptr_void); + + self.builder.ret(autoreleased, field_ty); + self.builder.finalize(); + return; + } + + // strong / copy / assign — bare load. const val = self.builder.load(field_addr, field_ty); self.builder.ret(val, field_ty); self.builder.finalize();