ffi 1.8b: sret transform for #objc_call(>16 B non-HFA struct)

104/104 regression tests pass. The Triple round-trip
(triple_imp writes {11, 22, 33} on the IMP side → #objc_call(Triple)
reads them back) is the test of record.

emit_llvm.zig changes:

1. `objc_msg_send` arm — when `needsByval(ret_ty)` (same predicate
   the plain-foreign-call path uses), apply the sret transform:
     - ret type collapses to void
     - prepend a `ptr` param at index 0 (call site provides an
       alloca slot)
     - mirror `sret(<RetType>)` on the call site so the AArch64 x8
       / SysV-AMD64 hidden-ptr ABI lowers correctly
     - load the result from the slot post-call
   The IR shape now matches clang exactly:
     call void @objc_msgSend(ptr sret({...}) %slot, ptr %recv, ptr %sel)

2. `.ret` arm — the body-side counterpart for sx fns whose declared
   return type is sret-shaped (sx-defined IMPs registered via
   `class_addMethod` produce these). When the current function's
   `needsByval(func.ret)` predicate holds, store the IR ret value
   through the prepended sret slot (param 0) and emit `ret void`.
   Previously the unconditional coerceArg path turned the struct
   value into `undef` and emitted `ret void undef` — illegal LLVM.

Test mechanics: registers `SxTripleProbe : NSObject` at runtime via
`objc_allocateClassPair` + `class_addMethod`, IMP returns
Triple{11, 22, 33}. `#objc_call(Triple)(instance, "tripleValue")`
gets them back, round-trip pinned in the .txt snapshot and the
IR-shape snapshot.
This commit is contained in:
agra
2026-05-19 18:50:26 +03:00
parent 865890aed9
commit e388687f1a
4 changed files with 860 additions and 551 deletions

View File

@@ -1041,25 +1041,36 @@ pub const LLVMEmitter = struct {
// ── Calls ─────────────────────────────────────────────
.objc_msg_send => |msg| {
const msg_send = self.getObjcMsgSendValue();
// Per-call-site LLVM function type. The Obj-C ABI uses
// the C calling convention: recv + sel in the first
// two int registers, additional args follow the C
// rules for their types. We hand the precise type to
// LLVMBuildCall2 — opaque pointers make the function
// value type-agnostic.
const ret_ty = self.toLLVMType(instruction.ty);
const total_params: usize = 2 + msg.args.len;
// Detect the sret case: >16 B non-HFA struct return.
// Same predicate as the plain-foreign-call path so the
// two arms stay in lockstep.
const raw_ret_ty = self.toLLVMType(instruction.ty);
const uses_sret = self.needsByval(instruction.ty, raw_ret_ty);
const ret_ty = if (uses_sret) self.cached_void else raw_ret_ty;
// Slot layout:
// uses_sret = false → [recv, sel, args...]
// uses_sret = true → [sret_slot, recv, sel, args...]
const sret_off: usize = if (uses_sret) 1 else 0;
const total_params: usize = 2 + msg.args.len + sret_off;
const param_types = self.alloc.alloc(c.LLVMTypeRef, total_params) catch unreachable;
defer self.alloc.free(param_types);
const call_args = self.alloc.alloc(c.LLVMValueRef, total_params) catch unreachable;
defer self.alloc.free(call_args);
var sret_slot: c.LLVMValueRef = null;
if (uses_sret) {
sret_slot = c.LLVMBuildAlloca(self.builder, raw_ret_ty, "objc.sret");
param_types[0] = self.cached_ptr;
call_args[0] = sret_slot;
}
// recv (typed *void from the IR)
param_types[0] = self.cached_ptr;
call_args[0] = self.coerceArg(self.resolveRef(msg.recv), self.cached_ptr);
param_types[sret_off] = self.cached_ptr;
call_args[sret_off] = self.coerceArg(self.resolveRef(msg.recv), self.cached_ptr);
// sel (loaded SEL — opaque ptr)
param_types[1] = self.cached_ptr;
call_args[1] = self.coerceArg(self.resolveRef(msg.sel), self.cached_ptr);
param_types[sret_off + 1] = self.cached_ptr;
call_args[sret_off + 1] = self.coerceArg(self.resolveRef(msg.sel), self.cached_ptr);
// additional args take their IR types, with ABI
// coercion applied so structs / strings decay the
// same way they do for any C foreign call.
@@ -1067,13 +1078,23 @@ pub const LLVMEmitter = struct {
const raw_ty = self.getRefIRType(arg_ref) orelse .void;
const raw_llvm = self.toLLVMType(raw_ty);
const coerced_ty = self.abiCoerceParamType(raw_ty, raw_llvm);
param_types[i + 2] = coerced_ty;
call_args[i + 2] = self.coerceArg(self.resolveRef(arg_ref), coerced_ty);
param_types[i + 2 + sret_off] = coerced_ty;
call_args[i + 2 + sret_off] = self.coerceArg(self.resolveRef(arg_ref), coerced_ty);
}
const fn_ty = c.LLVMFunctionType(ret_ty, param_types.ptr, @intCast(total_params), 0);
const call_label: [*:0]const u8 = if (instruction.ty == .void) "" else "objc.msg";
const result = c.LLVMBuildCall2(self.builder, fn_ty, msg_send, call_args.ptr, @intCast(total_params), call_label);
const call_label: [*:0]const u8 = if (instruction.ty == .void or uses_sret) "" else "objc.msg";
var result = c.LLVMBuildCall2(self.builder, fn_ty, msg_send, call_args.ptr, @intCast(total_params), call_label);
if (uses_sret) {
// Tag the call's arg 0 (sret slot) with the sret
// attribute so the AArch64 / SysV backends route
// through the x8 / hidden-pointer convention.
const sret_kind = c.LLVMGetEnumAttributeKindForName("sret", 4);
const sret_attr = c.LLVMCreateTypeAttribute(self.context, sret_kind, raw_ret_ty);
const param1_idx: c.LLVMAttributeIndex = @bitCast(@as(i32, 1));
c.LLVMAddCallSiteAttribute(result, param1_idx, sret_attr);
result = c.LLVMBuildLoad2(self.builder, raw_ret_ty, sret_slot, "objc.sret.load");
}
// Always mapRef — the IR Ref counter for this
// instruction advances regardless of return type,
// so skipping it would misalign every subsequent
@@ -1254,6 +1275,21 @@ pub const LLVMEmitter = struct {
// ── Terminators ────────────────────────────────────────
.ret => |un| {
var val = self.resolveRef(un.operand);
// sret-shaped function: declared return-type-in-IR is
// the struct, but the LLVM signature is void with a
// prepended ptr sret param. Store the value through
// the sret slot and emit ret void.
const func = &self.ir_mod.functions.items[self.current_func_idx];
const needs_c_abi = func.is_extern or func.call_conv == .c;
const raw_ret = self.toLLVMType(func.ret);
if (needs_c_abi and self.needsByval(func.ret, raw_ret)) {
const llvm_func2 = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(self.builder));
const sret_ptr = c.LLVMGetParam(llvm_func2, 0);
_ = c.LLVMBuildStore(self.builder, val, sret_ptr);
_ = c.LLVMBuildRetVoid(self.builder);
self.advanceRefCounter();
return;
}
// Coerce return value to match the function's LLVM return type
const llvm_func = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(self.builder));
const fn_ty = c.LLVMGlobalGetValueType(llvm_func);