ffi M4.0b: thread context.allocator through sx-defined +alloc
Two converging paths now allocate the state struct via the protocol's
allocator instead of raw malloc:
(1) sx-side `Cls.alloc()`: compiler intercepts in `lowerObjcStaticCall`
when the receiver is a sx-defined `#objc_class` and the method is
the niladic `alloc`. Emits the inline alloc-and-init sequence
using the caller's `current_ctx_ref` as the context — so
`push Context.{ allocator = my_arena } { let f := SxFoo.alloc(); }`
honors `my_arena` end-to-end. The msgSend dispatch is bypassed
entirely for this case.
(2) Obj-C-runtime `[Cls alloc]` (Info.plist principal class, NSCoder,
UIKit reflection): the synthesized `+alloc` IMP shim reads
`__sx_default_context.allocator` and calls into the same shared
helper. The IMP has `has_implicit_ctx = false` and runs with no
caller-side context — the default GPA is the right policy choice
for "everything Apple's runtime instantiates".
Shared helper `emitObjcDefinedAllocAndInit(fcd, cls_ref, ctx_addr)`
does the work: `class_createInstance` → `ctx.allocator.alloc(STATE_SIZE)`
via the inline-protocol fn-ptr → memset 0 → store allocator at
state[0] (the M4.0a slot, captured for -dealloc's later use) →
`object_setIvar(instance, __sx_state_ivar, state)`. Loud failures
on missing globals via the diagnostics system.
The sx-side interception must explicitly bitcast the
`class_createInstance` result from `*void` to the method's declared
return type (`*<Cls>` or `?*<Cls>`). lowerVarDecl reads the Ref's IR
type when no type annotation is present, and coerceToType is a
no-op for ptr→ptr — without the bitcast, `let f := SxFoo.alloc();`
binds `f` at `*void` and downstream `f.class` / `f.method()` fails
to find anything.
-dealloc still uses `free(state)` (M4.0c rewrites it). 184/184 tests
pass; chess on iOS-sim green.
This commit is contained in:
195
src/ir/lower.zig
195
src/ir/lower.zig
@@ -4944,6 +4944,52 @@ pub const Lowering = struct {
|
||||
const class_slot_ptr = self.builder.emit(.{ .global_addr = class_slot_gid }, self.module.types.ptrTo(vptr_ty));
|
||||
const class_obj = self.builder.emit(.{ .load = .{ .operand = class_slot_ptr } }, vptr_ty);
|
||||
|
||||
// M4.0b: intercept `Cls.alloc()` for sx-defined classes — emit the
|
||||
// inline alloc-and-init sequence using the caller's `context.allocator`
|
||||
// instead of going through `objc_msgSend` (which would land in the
|
||||
// +alloc IMP and use `__sx_default_context.allocator`). This honors
|
||||
// a surrounding `push Context.{ allocator = ... }`.
|
||||
if (!fcd.is_foreign and
|
||||
fcd.runtime == .objc_class and
|
||||
method_args.len == 0 and
|
||||
std.mem.eql(u8, method.name, "alloc"))
|
||||
{
|
||||
const ctx_addr = if (self.current_ctx_ref != Ref.none)
|
||||
self.current_ctx_ref
|
||||
else blk: {
|
||||
// Fallback: no current ctx (e.g. compiler-internal callers).
|
||||
// Use the default context — same as the IMP would.
|
||||
const default_ctx_gi = self.global_names.get("__sx_default_context") orelse {
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, span, "Cls.alloc() on sx-defined class '{s}': no current context and __sx_default_context missing", .{fcd.name});
|
||||
}
|
||||
return Ref.none;
|
||||
};
|
||||
break :blk self.builder.emit(.{ .global_addr = default_ctx_gi.id }, vptr_ty);
|
||||
};
|
||||
const instance = self.emitObjcDefinedAllocAndInit(fcd, class_obj, ctx_addr) orelse return Ref.none;
|
||||
// class_createInstance returns *void; bitcast to the method's
|
||||
// declared return type (typically `*<Cls>` or `?*<Cls>`) so
|
||||
// downstream `let f := Cls.alloc();` binds f at the right type
|
||||
// (lowerVarDecl reads the Ref's IR type when no annotation is
|
||||
// present). coerceToType is a no-op for ptr→ptr; we need an
|
||||
// explicit bitcast IR op to retype the Ref.
|
||||
if (ret_ty == vptr_ty) return instance;
|
||||
// Optional-wrapped returns (e.g. `-> ?*Cls`): emit optional_wrap.
|
||||
if (!ret_ty.isBuiltin()) {
|
||||
const ret_info = self.module.types.get(ret_ty);
|
||||
if (ret_info == .optional) {
|
||||
const inner = ret_info.optional.child;
|
||||
const cast = if (inner == vptr_ty)
|
||||
instance
|
||||
else
|
||||
self.builder.emit(.{ .bitcast = .{ .operand = instance, .from = vptr_ty, .to = inner } }, inner);
|
||||
return self.builder.optionalWrap(cast, ret_ty);
|
||||
}
|
||||
}
|
||||
return self.builder.emit(.{ .bitcast = .{ .operand = instance, .from = vptr_ty, .to = ret_ty } }, ret_ty);
|
||||
}
|
||||
|
||||
// Load the SEL from its slot.
|
||||
const sel_slot_gid = self.internObjcSelector(derived.sel);
|
||||
const sel_slot_ptr = self.builder.emit(.{ .global_addr = sel_slot_gid }, self.module.types.ptrTo(vptr_ty));
|
||||
@@ -12307,26 +12353,27 @@ pub const Lowering = struct {
|
||||
}
|
||||
|
||||
/// Synthesize the `+alloc` IMP for an sx-defined `#objc_class`.
|
||||
/// Class method (registered on the metaclass by emit_llvm) — when
|
||||
/// `[SxFoo alloc]` runs (from sx, UIKit, Info.plist, ...), this
|
||||
/// IMP fires and returns a fully-initialised instance whose
|
||||
/// `__sx_state` ivar points at a zero-init state struct.
|
||||
/// Class method registered on the metaclass — when `[SxFoo alloc]`
|
||||
/// runs from Apple's runtime (Info.plist principal class,
|
||||
/// NSCoder unarchive, UIKit reflection), this IMP fires.
|
||||
///
|
||||
/// C-ABI: `(cls: id, _cmd: SEL) -> id`
|
||||
/// C-ABI: `(cls: id, _cmd: SEL) -> id`. No implicit ctx.
|
||||
///
|
||||
/// Body:
|
||||
/// Body (M4.0):
|
||||
/// %instance = class_createInstance(cls, 0)
|
||||
/// %state = malloc(STATE_SIZE)
|
||||
/// %ctx_addr = &__sx_default_context
|
||||
/// %state = ctx_addr.allocator.alloc(STATE_SIZE)
|
||||
/// memset(state, 0, STATE_SIZE)
|
||||
/// %iv = load @__<Cls>_state_ivar
|
||||
/// object_setIvar(instance, iv, state)
|
||||
/// state[0] = allocator ← capture for -dealloc
|
||||
/// object_setIvar(instance, __sx_state_ivar, state)
|
||||
/// ret instance
|
||||
///
|
||||
/// STATE_SIZE = max(typeSizeBytes(__<Cls>State), 1) — we always
|
||||
/// allocate at least one byte so the ivar is never null. State
|
||||
/// is freed in `-dealloc` (M1.2 A.6).
|
||||
/// Sx-side `Cls.alloc()` is intercepted at the call site (see
|
||||
/// `lowerObjcStaticCall`) and emits the same sequence inline with
|
||||
/// `current_ctx_ref` as the ctx — so `push Context.{ allocator = ... }`
|
||||
/// flows through to per-instance allocator capture without going via
|
||||
/// the IMP.
|
||||
fn emitObjcDefinedClassAllocImp(self: *Lowering, fcd: *const ast.ForeignClassDecl) void {
|
||||
// Save+restore builder state.
|
||||
const saved_func = self.builder.func;
|
||||
const saved_block = self.builder.current_block;
|
||||
const saved_counter = self.builder.inst_counter;
|
||||
@@ -12355,63 +12402,117 @@ pub const Lowering = struct {
|
||||
const entry = self.builder.appendBlock(entry_name, &.{});
|
||||
self.builder.switchToBlock(entry);
|
||||
|
||||
// (1) %instance = class_createInstance(cls, 0)
|
||||
// ctx_addr = &__sx_default_context — IMP runs in Apple's runtime
|
||||
// context, no implicit sx ctx to inherit, so use the process-wide
|
||||
// default allocator. Sx-side callers bypass this IMP entirely
|
||||
// (compiler intercepts Cls.alloc()) and use their own
|
||||
// `context.allocator`.
|
||||
const default_ctx_gi = self.global_names.get("__sx_default_context") orelse {
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassAllocImp: __sx_default_context global missing for class '{s}' (compiler bug — scan pass did not register the default context)", .{fcd.name});
|
||||
}
|
||||
return;
|
||||
};
|
||||
const ctx_addr = self.builder.emit(.{ .global_addr = default_ctx_gi.id }, ptr_void);
|
||||
|
||||
const cls_ref = Ref.fromIndex(0);
|
||||
const instance = self.emitObjcDefinedAllocAndInit(fcd, cls_ref, ctx_addr) orelse return;
|
||||
|
||||
self.builder.ret(instance, ptr_void);
|
||||
self.builder.finalize();
|
||||
}
|
||||
|
||||
/// Shared inline sequence: allocate Obj-C instance + sx state struct,
|
||||
/// capture the allocator, bind to the `__sx_state` ivar. Used by both
|
||||
/// the `+alloc` IMP (ctx_addr = &__sx_default_context) and the sx-side
|
||||
/// `Cls.alloc()` interception (ctx_addr = current_ctx_ref).
|
||||
///
|
||||
/// Returns the new instance pointer, or `null` if a required global is
|
||||
/// missing (compiler bug — should be impossible after scan pass).
|
||||
fn emitObjcDefinedAllocAndInit(
|
||||
self: *Lowering,
|
||||
fcd: *const ast.ForeignClassDecl,
|
||||
cls_ref: Ref,
|
||||
ctx_addr: Ref,
|
||||
) ?Ref {
|
||||
const ptr_void = self.module.types.ptrTo(.void);
|
||||
|
||||
// (1) instance = class_createInstance(cls, 0)
|
||||
const create_fid = self.ensureCRuntimeDecl("class_createInstance", &.{ ptr_void, .u64 }, ptr_void);
|
||||
const create_args = self.alloc.alloc(Ref, 2) catch return;
|
||||
const create_args = self.alloc.alloc(Ref, 2) catch return null;
|
||||
create_args[0] = cls_ref;
|
||||
create_args[1] = self.builder.constInt(0, .u64);
|
||||
const instance = self.builder.emit(.{ .call = .{
|
||||
.callee = create_fid,
|
||||
.args = create_args,
|
||||
} }, ptr_void);
|
||||
const instance = self.builder.emit(.{ .call = .{ .callee = create_fid, .args = create_args } }, ptr_void);
|
||||
|
||||
// STATE_SIZE — compute the layout size of the state struct.
|
||||
// Always at least 1 so we have a non-null pointer to bind.
|
||||
// STATE_SIZE = max(typeSizeBytes(__<Cls>State), 1).
|
||||
const state_struct_ty = self.objcDefinedStateStructType(fcd);
|
||||
const raw_size = self.module.types.typeSizeBytes(state_struct_ty);
|
||||
const state_size: u64 = if (raw_size == 0) 1 else @intCast(raw_size);
|
||||
const size_const = self.builder.constInt(@intCast(state_size), .u64);
|
||||
|
||||
// (2) %state = malloc(STATE_SIZE)
|
||||
const malloc_fid = self.ensureCRuntimeDecl("malloc", &.{.u64}, ptr_void);
|
||||
const malloc_args = self.alloc.alloc(Ref, 1) catch return;
|
||||
malloc_args[0] = size_const;
|
||||
const state = self.builder.emit(.{ .call = .{
|
||||
.callee = malloc_fid,
|
||||
.args = malloc_args,
|
||||
// (2) Dispatch through Context.allocator at ctx_addr:
|
||||
// allocator = (*ctx_addr).field[0]
|
||||
// state = allocator.alloc(size) (via inline-protocol fn-ptr)
|
||||
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedAllocAndInit: Context type not found in module for class '{s}' (compiler bug)", .{fcd.name});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const ctx_info = self.module.types.get(ctx_ty);
|
||||
if (ctx_info != .@"struct" or ctx_info.@"struct".fields.len < 1) {
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedAllocAndInit: Context has unexpected shape for class '{s}' (compiler bug)", .{fcd.name});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const allocator_ty = ctx_info.@"struct".fields[0].ty;
|
||||
const ctx_val = self.builder.load(ctx_addr, ctx_ty);
|
||||
const allocator = self.builder.structGet(ctx_val, 0, allocator_ty);
|
||||
const alloc_ctx = self.builder.structGet(allocator, 0, ptr_void);
|
||||
const alloc_fn_ptr = self.builder.structGet(allocator, 1, ptr_void);
|
||||
const call_args = self.alloc.dupe(Ref, &.{ ctx_addr, alloc_ctx, size_const }) catch return null;
|
||||
const state = self.builder.emit(.{ .call_indirect = .{
|
||||
.callee = alloc_fn_ptr,
|
||||
.args = call_args,
|
||||
} }, ptr_void);
|
||||
|
||||
// (3) memset(state, 0, STATE_SIZE)
|
||||
// (3) memset(state, 0, STATE_SIZE) — zero everything including the
|
||||
// allocator slot; the next store re-writes the allocator slot.
|
||||
const memset_fid = self.ensureCRuntimeDecl("memset", &.{ ptr_void, .s32, .u64 }, ptr_void);
|
||||
const memset_args = self.alloc.alloc(Ref, 3) catch return;
|
||||
const memset_args = self.alloc.alloc(Ref, 3) catch return null;
|
||||
memset_args[0] = state;
|
||||
memset_args[1] = self.builder.constInt(0, .s32);
|
||||
memset_args[2] = size_const;
|
||||
_ = self.builder.emit(.{ .call = .{
|
||||
.callee = memset_fid,
|
||||
.args = memset_args,
|
||||
} }, ptr_void);
|
||||
_ = self.builder.emit(.{ .call = .{ .callee = memset_fid, .args = memset_args } }, ptr_void);
|
||||
|
||||
// (4) object_setIvar(instance, load(@__<Cls>_state_ivar), state)
|
||||
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return;
|
||||
// (4) Capture allocator at state[0] — `-dealloc` reads it back.
|
||||
const state_alloc_addr = self.builder.emit(.{ .struct_gep = .{
|
||||
.base = state,
|
||||
.field_index = 0,
|
||||
.base_type = state_struct_ty,
|
||||
} }, ptr_void);
|
||||
self.builder.store(state_alloc_addr, allocator);
|
||||
|
||||
// (5) object_setIvar(instance, load(@__<Cls>_state_ivar), state)
|
||||
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return null;
|
||||
defer self.alloc.free(ivar_global_name);
|
||||
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return;
|
||||
const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
|
||||
const ivar_handle = self.builder.load(ivar_addr, ptr_void);
|
||||
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse {
|
||||
if (self.diagnostics) |d| {
|
||||
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedAllocAndInit: ivar global '{s}' missing (scan-pass bug)", .{ivar_global_name});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const ivar_addr_v = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
|
||||
const ivar_handle = self.builder.load(ivar_addr_v, ptr_void);
|
||||
const set_ivar_fid = self.ensureCRuntimeDecl("object_setIvar", &.{ ptr_void, ptr_void, ptr_void }, .void);
|
||||
const set_args = self.alloc.alloc(Ref, 3) catch return;
|
||||
const set_args = self.alloc.alloc(Ref, 3) catch return null;
|
||||
set_args[0] = instance;
|
||||
set_args[1] = ivar_handle;
|
||||
set_args[2] = state;
|
||||
_ = self.builder.emit(.{ .call = .{
|
||||
.callee = set_ivar_fid,
|
||||
.args = set_args,
|
||||
} }, .void);
|
||||
_ = self.builder.emit(.{ .call = .{ .callee = set_ivar_fid, .args = set_args } }, .void);
|
||||
|
||||
// (5) ret instance
|
||||
self.builder.ret(instance, ptr_void);
|
||||
self.builder.finalize();
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// Emit a C-ABI IMP trampoline for a CLASS method (no `*Self`
|
||||
|
||||
@@ -800,10 +800,16 @@ declare ptr @object_getIvar(ptr, ptr) #0
|
||||
define ptr @__SxFoo_alloc_imp(ptr %0, ptr %1) #0 {
|
||||
entry:
|
||||
%call = call ptr @class_createInstance(ptr %0, i64 0)
|
||||
%callN = call ptr @malloc(i64 32)
|
||||
%callN = call ptr @memset(ptr %callN, i32 0, i64 32)
|
||||
%load = load ptr, ptr @__SxFoo_state_ivar, align 8
|
||||
call void @object_setIvar(ptr %call, ptr %load, ptr %callN)
|
||||
%load = load { { ptr, ptr, ptr }, ptr }, ptr @__sx_default_context, align 8
|
||||
%sg = extractvalue { { ptr, ptr, ptr }, ptr } %load, 0
|
||||
%sgN = extractvalue { ptr, ptr, ptr } %sg, 0
|
||||
%sgN = extractvalue { ptr, ptr, ptr } %sg, 1
|
||||
%icall = call ptr %sgN(ptr @__sx_default_context, ptr %sgN, i64 32)
|
||||
%callN = call ptr @memset(ptr %icall, i32 0, i64 32)
|
||||
%gep = getelementptr inbounds { { ptr, ptr, ptr }, i32 }, ptr %icall, i32 0, i32 0
|
||||
store { ptr, ptr, ptr } %sg, ptr %gep, align 8
|
||||
%loadN = load ptr, ptr @__SxFoo_state_ivar, align 8
|
||||
call void @object_setIvar(ptr %call, ptr %loadN, ptr %icall)
|
||||
ret ptr %call
|
||||
}
|
||||
|
||||
@@ -882,3 +888,5 @@ declare ptr @object_getClass(ptr)
|
||||
declare ptr @objc_getProtocol(ptr)
|
||||
|
||||
declare i8 @class_addProtocol(ptr, ptr)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user