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:
@@ -1,37 +1,48 @@
|
|||||||
// Phase 1 step 1.8 (PLAN-FFI.md): >16 B non-HFA struct returns
|
// Phase 1 step 1.8 (PLAN-FFI.md): >16 B non-HFA struct returns
|
||||||
// through `#objc_call`. AAPCS64 routes these through the indirect-
|
// through `#objc_call`. AAPCS64 routes these through the indirect-
|
||||||
// return convention: caller allocates the result slot, passes its
|
// return convention: caller allocates the result slot, passes its
|
||||||
// pointer as a hidden `x8` arg with the `sret(<T>)` attribute,
|
// pointer in x8 with the `sret(<T>)` attribute, callee writes
|
||||||
// callee writes through it and returns void.
|
// through it and returns void.
|
||||||
//
|
//
|
||||||
// 1.6's `objc_msg_send` lowering hands the IR struct type straight
|
// Register a runtime-built Obj-C class with a method that returns
|
||||||
// to LLVMBuildCall2 — works for ≤16 B aggregates and HFAs of any
|
// a fixed `Triple`. The IMP is a plain sx fn (callconv .c) — its
|
||||||
// size (since those stay register-resident) but breaks >16 B int
|
// sret-shaped lowering already works (Phase 0.3 fix for plain
|
||||||
// aggregates: LLVM accepts the signature but the AArch64 backend
|
// `#foreign` returns). The `#objc_call` dispatch side now produces
|
||||||
// expects the result in x0/x1, the runtime stub doesn't populate
|
// the matching call shape: `call void @objc_msgSend(ptr sret %slot,
|
||||||
// those for sret-shaped returns, and the upper fields come back
|
// ...)` + load. The two halves must agree on the ABI for the
|
||||||
// as garbage.
|
// round-trip to return the right bytes.
|
||||||
//
|
|
||||||
// 1.8a (this commit): xfail. Snapshot shows garbage in the third
|
|
||||||
// field — pins the broken behavior.
|
|
||||||
// 1.8b (next commit): emit_llvm.zig applies the sret transform
|
|
||||||
// (ret type collapses to void, prepend ptr sret param, alloca
|
|
||||||
// slot at call site, load result post-call) for non-HFA >16 B
|
|
||||||
// returns. Snapshot flips to all-zeros (Obj-C runtime contract).
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
#import "modules/compiler.sx";
|
#import "modules/compiler.sx";
|
||||||
|
#import "modules/std/objc.sx";
|
||||||
|
|
||||||
// 24 B non-HFA integer aggregate. Distinct from any HFA path —
|
|
||||||
// no all-float / all-double check fires.
|
|
||||||
Triple :: struct { a: s64; b: s64; c: s64; }
|
Triple :: struct { a: s64; b: s64; c: s64; }
|
||||||
|
|
||||||
|
// IMP for the runtime-installed method. Obj-C convention: implicit
|
||||||
|
// (self, _cmd) prefix, then declared args. Returns the value bytes.
|
||||||
|
triple_imp :: (self: *void, _cmd: *void) -> Triple callconv(.c) {
|
||||||
|
Triple.{ a = 11, b = 22, c = 33 };
|
||||||
|
}
|
||||||
|
|
||||||
main :: () -> s32 {
|
main :: () -> s32 {
|
||||||
inline if OS == .macos {
|
inline if OS == .macos {
|
||||||
t := #objc_call(Triple)(null, "tripleValue");
|
// Build the class:
|
||||||
// Per the [nil structReturn] = 0 contract, all three fields
|
// @interface SxTripleProbe : NSObject
|
||||||
// should be 0 after 1.8b lands. Today: a/b correct, c is
|
// - (Triple)tripleValue;
|
||||||
// whatever the callee-saves left in the high register.
|
// @end
|
||||||
|
ns_object := objc_getClass("NSObject".ptr);
|
||||||
|
my_cls := objc_allocateClassPair(ns_object, "SxTripleProbe".ptr, 0);
|
||||||
|
sel := sel_registerName("tripleValue".ptr);
|
||||||
|
// Type encoding: {Triple=qqq}@: → returns 24 B struct of 3 s64,
|
||||||
|
// implicit (self: id, _cmd: SEL).
|
||||||
|
ok := class_addMethod(my_cls, sel, xx triple_imp, "{Triple=qqq}@:".ptr);
|
||||||
|
print("addMethod = {}\n", ok);
|
||||||
|
objc_registerClassPair(my_cls);
|
||||||
|
|
||||||
|
// Call through #objc_call — sret transform applies because
|
||||||
|
// Triple is 24 B non-HFA.
|
||||||
|
instance := class_createInstance(my_cls, 0);
|
||||||
|
t := #objc_call(Triple)(instance, "tripleValue");
|
||||||
print("triple = ({}, {}, {})\n", t.a, t.b, t.c);
|
print("triple = ({}, {}, {})\n", t.a, t.b, t.c);
|
||||||
}
|
}
|
||||||
inline if OS != .macos {
|
inline if OS != .macos {
|
||||||
|
|||||||
@@ -1041,25 +1041,36 @@ pub const LLVMEmitter = struct {
|
|||||||
// ── Calls ─────────────────────────────────────────────
|
// ── Calls ─────────────────────────────────────────────
|
||||||
.objc_msg_send => |msg| {
|
.objc_msg_send => |msg| {
|
||||||
const msg_send = self.getObjcMsgSendValue();
|
const msg_send = self.getObjcMsgSendValue();
|
||||||
// Per-call-site LLVM function type. The Obj-C ABI uses
|
// Detect the sret case: >16 B non-HFA struct return.
|
||||||
// the C calling convention: recv + sel in the first
|
// Same predicate as the plain-foreign-call path so the
|
||||||
// two int registers, additional args follow the C
|
// two arms stay in lockstep.
|
||||||
// rules for their types. We hand the precise type to
|
const raw_ret_ty = self.toLLVMType(instruction.ty);
|
||||||
// LLVMBuildCall2 — opaque pointers make the function
|
const uses_sret = self.needsByval(instruction.ty, raw_ret_ty);
|
||||||
// value type-agnostic.
|
const ret_ty = if (uses_sret) self.cached_void else raw_ret_ty;
|
||||||
const ret_ty = self.toLLVMType(instruction.ty);
|
|
||||||
const total_params: usize = 2 + msg.args.len;
|
// 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;
|
const param_types = self.alloc.alloc(c.LLVMTypeRef, total_params) catch unreachable;
|
||||||
defer self.alloc.free(param_types);
|
defer self.alloc.free(param_types);
|
||||||
const call_args = self.alloc.alloc(c.LLVMValueRef, total_params) catch unreachable;
|
const call_args = self.alloc.alloc(c.LLVMValueRef, total_params) catch unreachable;
|
||||||
defer self.alloc.free(call_args);
|
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)
|
// recv (typed *void from the IR)
|
||||||
param_types[0] = self.cached_ptr;
|
param_types[sret_off] = self.cached_ptr;
|
||||||
call_args[0] = self.coerceArg(self.resolveRef(msg.recv), self.cached_ptr);
|
call_args[sret_off] = self.coerceArg(self.resolveRef(msg.recv), self.cached_ptr);
|
||||||
// sel (loaded SEL — opaque ptr)
|
// sel (loaded SEL — opaque ptr)
|
||||||
param_types[1] = self.cached_ptr;
|
param_types[sret_off + 1] = self.cached_ptr;
|
||||||
call_args[1] = self.coerceArg(self.resolveRef(msg.sel), 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
|
// additional args take their IR types, with ABI
|
||||||
// coercion applied so structs / strings decay the
|
// coercion applied so structs / strings decay the
|
||||||
// same way they do for any C foreign call.
|
// 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_ty = self.getRefIRType(arg_ref) orelse .void;
|
||||||
const raw_llvm = self.toLLVMType(raw_ty);
|
const raw_llvm = self.toLLVMType(raw_ty);
|
||||||
const coerced_ty = self.abiCoerceParamType(raw_ty, raw_llvm);
|
const coerced_ty = self.abiCoerceParamType(raw_ty, raw_llvm);
|
||||||
param_types[i + 2] = coerced_ty;
|
param_types[i + 2 + sret_off] = coerced_ty;
|
||||||
call_args[i + 2] = self.coerceArg(self.resolveRef(arg_ref), 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 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 call_label: [*:0]const u8 = if (instruction.ty == .void or uses_sret) "" else "objc.msg";
|
||||||
const result = c.LLVMBuildCall2(self.builder, fn_ty, msg_send, call_args.ptr, @intCast(total_params), call_label);
|
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
|
// Always mapRef — the IR Ref counter for this
|
||||||
// instruction advances regardless of return type,
|
// instruction advances regardless of return type,
|
||||||
// so skipping it would misalign every subsequent
|
// so skipping it would misalign every subsequent
|
||||||
@@ -1254,6 +1275,21 @@ pub const LLVMEmitter = struct {
|
|||||||
// ── Terminators ────────────────────────────────────────
|
// ── Terminators ────────────────────────────────────────
|
||||||
.ret => |un| {
|
.ret => |un| {
|
||||||
var val = self.resolveRef(un.operand);
|
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
|
// Coerce return value to match the function's LLVM return type
|
||||||
const llvm_func = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(self.builder));
|
const llvm_func = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(self.builder));
|
||||||
const fn_ty = c.LLVMGlobalGetValueType(llvm_func);
|
const fn_ty = c.LLVMGlobalGetValueType(llvm_func);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +1,2 @@
|
|||||||
triple = (0, 0, 0)
|
addMethod = true
|
||||||
|
triple = (11, 22, 33)
|
||||||
|
|||||||
Reference in New Issue
Block a user