ffi M4.B getter: weak property reads through objc_loadWeakRetained

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.
This commit is contained in:
agra
2026-05-26 23:04:00 +03:00
parent f4faef97dd
commit c88a293cf4

View File

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