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_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);
|
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.
|
// Load the SEL from its slot.
|
||||||
const sel_slot_gid = self.internObjcSelector(derived.sel);
|
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));
|
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`.
|
/// Synthesize the `+alloc` IMP for an sx-defined `#objc_class`.
|
||||||
/// Class method (registered on the metaclass by emit_llvm) — when
|
/// Class method registered on the metaclass — when `[SxFoo alloc]`
|
||||||
/// `[SxFoo alloc]` runs (from sx, UIKit, Info.plist, ...), this
|
/// runs from Apple's runtime (Info.plist principal class,
|
||||||
/// IMP fires and returns a fully-initialised instance whose
|
/// NSCoder unarchive, UIKit reflection), this IMP fires.
|
||||||
/// `__sx_state` ivar points at a zero-init state struct.
|
|
||||||
///
|
///
|
||||||
/// 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)
|
/// %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)
|
/// memset(state, 0, STATE_SIZE)
|
||||||
/// %iv = load @__<Cls>_state_ivar
|
/// state[0] = allocator ← capture for -dealloc
|
||||||
/// object_setIvar(instance, iv, state)
|
/// object_setIvar(instance, __sx_state_ivar, state)
|
||||||
/// ret instance
|
/// ret instance
|
||||||
///
|
///
|
||||||
/// STATE_SIZE = max(typeSizeBytes(__<Cls>State), 1) — we always
|
/// Sx-side `Cls.alloc()` is intercepted at the call site (see
|
||||||
/// allocate at least one byte so the ivar is never null. State
|
/// `lowerObjcStaticCall`) and emits the same sequence inline with
|
||||||
/// is freed in `-dealloc` (M1.2 A.6).
|
/// `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 {
|
fn emitObjcDefinedClassAllocImp(self: *Lowering, fcd: *const ast.ForeignClassDecl) void {
|
||||||
// Save+restore builder state.
|
|
||||||
const saved_func = self.builder.func;
|
const saved_func = self.builder.func;
|
||||||
const saved_block = self.builder.current_block;
|
const saved_block = self.builder.current_block;
|
||||||
const saved_counter = self.builder.inst_counter;
|
const saved_counter = self.builder.inst_counter;
|
||||||
@@ -12355,63 +12402,117 @@ pub const Lowering = struct {
|
|||||||
const entry = self.builder.appendBlock(entry_name, &.{});
|
const entry = self.builder.appendBlock(entry_name, &.{});
|
||||||
self.builder.switchToBlock(entry);
|
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 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_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[0] = cls_ref;
|
||||||
create_args[1] = self.builder.constInt(0, .u64);
|
create_args[1] = self.builder.constInt(0, .u64);
|
||||||
const instance = self.builder.emit(.{ .call = .{
|
const instance = self.builder.emit(.{ .call = .{ .callee = create_fid, .args = create_args } }, ptr_void);
|
||||||
.callee = create_fid,
|
|
||||||
.args = create_args,
|
|
||||||
} }, ptr_void);
|
|
||||||
|
|
||||||
// STATE_SIZE — compute the layout size of the state struct.
|
// STATE_SIZE = max(typeSizeBytes(__<Cls>State), 1).
|
||||||
// Always at least 1 so we have a non-null pointer to bind.
|
|
||||||
const state_struct_ty = self.objcDefinedStateStructType(fcd);
|
const state_struct_ty = self.objcDefinedStateStructType(fcd);
|
||||||
const raw_size = self.module.types.typeSizeBytes(state_struct_ty);
|
const raw_size = self.module.types.typeSizeBytes(state_struct_ty);
|
||||||
const state_size: u64 = if (raw_size == 0) 1 else @intCast(raw_size);
|
const state_size: u64 = if (raw_size == 0) 1 else @intCast(raw_size);
|
||||||
const size_const = self.builder.constInt(@intCast(state_size), .u64);
|
const size_const = self.builder.constInt(@intCast(state_size), .u64);
|
||||||
|
|
||||||
// (2) %state = malloc(STATE_SIZE)
|
// (2) Dispatch through Context.allocator at ctx_addr:
|
||||||
const malloc_fid = self.ensureCRuntimeDecl("malloc", &.{.u64}, ptr_void);
|
// allocator = (*ctx_addr).field[0]
|
||||||
const malloc_args = self.alloc.alloc(Ref, 1) catch return;
|
// state = allocator.alloc(size) (via inline-protocol fn-ptr)
|
||||||
malloc_args[0] = size_const;
|
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
||||||
const state = self.builder.emit(.{ .call = .{
|
if (self.diagnostics) |d| {
|
||||||
.callee = malloc_fid,
|
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedAllocAndInit: Context type not found in module for class '{s}' (compiler bug)", .{fcd.name});
|
||||||
.args = malloc_args,
|
}
|
||||||
|
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);
|
} }, 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_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[0] = state;
|
||||||
memset_args[1] = self.builder.constInt(0, .s32);
|
memset_args[1] = self.builder.constInt(0, .s32);
|
||||||
memset_args[2] = size_const;
|
memset_args[2] = size_const;
|
||||||
_ = self.builder.emit(.{ .call = .{
|
_ = self.builder.emit(.{ .call = .{ .callee = memset_fid, .args = memset_args } }, ptr_void);
|
||||||
.callee = memset_fid,
|
|
||||||
.args = memset_args,
|
|
||||||
} }, ptr_void);
|
|
||||||
|
|
||||||
// (4) object_setIvar(instance, load(@__<Cls>_state_ivar), state)
|
// (4) Capture allocator at state[0] — `-dealloc` reads it back.
|
||||||
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return;
|
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);
|
defer self.alloc.free(ivar_global_name);
|
||||||
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return;
|
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse {
|
||||||
const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
|
if (self.diagnostics) |d| {
|
||||||
const ivar_handle = self.builder.load(ivar_addr, ptr_void);
|
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_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[0] = instance;
|
||||||
set_args[1] = ivar_handle;
|
set_args[1] = ivar_handle;
|
||||||
set_args[2] = state;
|
set_args[2] = state;
|
||||||
_ = self.builder.emit(.{ .call = .{
|
_ = self.builder.emit(.{ .call = .{ .callee = set_ivar_fid, .args = set_args } }, .void);
|
||||||
.callee = set_ivar_fid,
|
|
||||||
.args = set_args,
|
|
||||||
} }, .void);
|
|
||||||
|
|
||||||
// (5) ret instance
|
return instance;
|
||||||
self.builder.ret(instance, ptr_void);
|
|
||||||
self.builder.finalize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emit a C-ABI IMP trampoline for a CLASS method (no `*Self`
|
/// 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 {
|
define ptr @__SxFoo_alloc_imp(ptr %0, ptr %1) #0 {
|
||||||
entry:
|
entry:
|
||||||
%call = call ptr @class_createInstance(ptr %0, i64 0)
|
%call = call ptr @class_createInstance(ptr %0, i64 0)
|
||||||
%callN = call ptr @malloc(i64 32)
|
%load = load { { ptr, ptr, ptr }, ptr }, ptr @__sx_default_context, align 8
|
||||||
%callN = call ptr @memset(ptr %callN, i32 0, i64 32)
|
%sg = extractvalue { { ptr, ptr, ptr }, ptr } %load, 0
|
||||||
%load = load ptr, ptr @__SxFoo_state_ivar, align 8
|
%sgN = extractvalue { ptr, ptr, ptr } %sg, 0
|
||||||
call void @object_setIvar(ptr %call, ptr %load, ptr %callN)
|
%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
|
ret ptr %call
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -882,3 +888,5 @@ declare ptr @object_getClass(ptr)
|
|||||||
declare ptr @objc_getProtocol(ptr)
|
declare ptr @objc_getProtocol(ptr)
|
||||||
|
|
||||||
declare i8 @class_addProtocol(ptr, ptr)
|
declare i8 @class_addProtocol(ptr, ptr)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user