ffi M4.B setter: emit ARC ops in sx-defined property setters
emitObjcDefinedPropertySetter now dispatches on objcPropertyKind to
emit the right runtime ops per Apple's ARC contract:
- assign → bare store (primitives, explicitly opted-out object slots).
- strong → load old; objc_retain(new); store new; objc_release(old).
Apple's runtime treats release(NULL) as a safe no-op, so
no explicit null-check on the old value.
- weak → objc_storeWeak(field_addr, val) — handles first-store
(init) and re-store (destroy + init) atomically. Registers
the slot with libobjc's side-table; the runtime auto-nils
it when the target deallocates.
- copy → [val copy] (sends `copy` selector — returns retained per
the NSCopying contract); load old; store the copied
instance; release old.
Side-effect on the weak path: even with the bare-load getter still in
place (loaded directly from the slot), weak reads work because Apple's
runtime side-table-nils the slot at target dealloc. The getter
improvement via objc_loadWeakRetained is the next commit and is
needed for race-safe reads (between load and use, the target could
deinit on another thread); for the single-threaded test scenarios
the bare load is sufficient.
ffi-objc-arc-02-strong-property advances from "child dealloc'd at
midpoint" to "unbalanced; alloc=2 dealloc=1" — strong setter now
retains, but the M4.B-dealloc cleanup hasn't landed so the child
held by the property isn't released when the parent deallocates.
Final commit (M4.B dealloc) closes the loop.
ffi-objc-arc-03-weak-property turns fully green: storeWeak +
auto-nil side-table do the work.
189/189 example tests pass; chess on iOS-sim green.
This commit is contained in:
@@ -12315,7 +12315,72 @@ pub const Lowering = struct {
|
||||
const state_ptr = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void);
|
||||
|
||||
const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state_ptr, .field_index = fidx, .base_type = state_ty } }, ptr_void);
|
||||
self.builder.store(field_addr, val_ref);
|
||||
|
||||
// M4.B setter — emit ARC ops based on the property's modifier kind.
|
||||
const kind = self.objcPropertyKind(field);
|
||||
switch (kind) {
|
||||
.assign => {
|
||||
// Primitives or explicit assign: bare store, no ARC.
|
||||
self.builder.store(field_addr, val_ref);
|
||||
},
|
||||
.strong => {
|
||||
// Retain new, release old. Order matters: retain first
|
||||
// (in case val == old, we don't release before retain).
|
||||
self.ensureArcRuntimeDecls();
|
||||
const retain_fid = self.ensureCRuntimeDecl("objc_retain", &.{ptr_void}, ptr_void);
|
||||
const release_fid = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void);
|
||||
|
||||
// old = load field_addr
|
||||
const old_val = self.builder.load(field_addr, field_ty);
|
||||
// new = objc_retain(val)
|
||||
const retain_args = self.alloc.alloc(Ref, 1) catch return;
|
||||
retain_args[0] = val_ref;
|
||||
_ = self.builder.emit(.{ .call = .{ .callee = retain_fid, .args = retain_args } }, ptr_void);
|
||||
// store field_addr, val
|
||||
self.builder.store(field_addr, val_ref);
|
||||
// objc_release(old) — Apple's runtime treats release(NULL) as a no-op,
|
||||
// so we skip an explicit null-check (saves a branch on every assign).
|
||||
const release_args = self.alloc.alloc(Ref, 1) catch return;
|
||||
release_args[0] = old_val;
|
||||
_ = self.builder.emit(.{ .call = .{ .callee = release_fid, .args = release_args } }, .void);
|
||||
},
|
||||
.weak => {
|
||||
// objc_storeWeak(field_addr, val) handles first-store
|
||||
// (init) and re-store (destroy old + init new) atomically.
|
||||
self.ensureArcRuntimeDecls();
|
||||
const store_weak_fid = self.ensureCRuntimeDecl("objc_storeWeak", &.{ ptr_void, ptr_void }, ptr_void);
|
||||
const store_args = self.alloc.alloc(Ref, 2) catch return;
|
||||
store_args[0] = field_addr;
|
||||
store_args[1] = val_ref;
|
||||
_ = self.builder.emit(.{ .call = .{ .callee = store_weak_fid, .args = store_args } }, ptr_void);
|
||||
},
|
||||
.copy => {
|
||||
// copy = objc_msgSend(val, sel_copy) — returns retained
|
||||
// (NSCopying contract).
|
||||
// Release old, then store the copy.
|
||||
self.ensureArcRuntimeDecls();
|
||||
const release_fid = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void);
|
||||
|
||||
// Load + cache the `copy` selector slot.
|
||||
const sel_copy_gid = self.internObjcSelector("copy");
|
||||
const sel_slot_ptr = self.builder.emit(.{ .global_addr = sel_copy_gid }, self.module.types.ptrTo(ptr_void));
|
||||
const sel_copy = self.builder.emit(.{ .load = .{ .operand = sel_slot_ptr } }, ptr_void);
|
||||
|
||||
// copy = [val copy]
|
||||
const copy_args = self.alloc.alloc(Ref, 0) catch return;
|
||||
const copied = self.builder.emit(.{ .objc_msg_send = .{
|
||||
.recv = val_ref,
|
||||
.sel = sel_copy,
|
||||
.args = copy_args,
|
||||
} }, ptr_void);
|
||||
|
||||
const old_val = self.builder.load(field_addr, field_ty);
|
||||
self.builder.store(field_addr, copied);
|
||||
const release_args = self.alloc.alloc(Ref, 1) catch return;
|
||||
release_args[0] = old_val;
|
||||
_ = self.builder.emit(.{ .call = .{ .callee = release_fid, .args = release_args } }, .void);
|
||||
},
|
||||
}
|
||||
self.builder.retVoid();
|
||||
self.builder.finalize();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user