ffi M2.1(b): class methods on sx-defined #objc_class
Bodied methods without a '*Self' first param (parser marks
is_static=true) are now registered as Obj-C CLASS methods on
the metaclass.
Each such method gets:
- A synthesized FnDecl + body lowering through the existing
M1.2 A.2 path.
- A C-ABI trampoline 'emitObjcDefinedClassStaticImp' — same
shape as the instance trampoline but skips the __sx_state
ivar read (no instance state) and passes only
'__sx_default_context' (plus user args) to the sx body.
- An entry in ObjcDefinedMethodEntry with 'is_class=true'.
emit_llvm's class-pair init constructor now computes the
metaclass once up-front (via object_getClass(cls)) and shares
it between the +alloc IMP registration (M1.2 A.5) and the
M2.1(b) class-method registrations. The per-method registration
loop picks the target via 'method.is_class ? metaclass : cls'.
149-objc-class-method-static-imp.sx end-to-end on macOS:
SxFoo :: #objc_class("SxFoo") {
answer :: () -> s32 { return 42; }
}
// [SxFoo answer] via objc_msgSend → 42
// class_getClassMethod(SxFoo, sel_answer) → non-null
Still TODO for M2.1: the (a) class-LEVEL constant form
'layerClass :: Class = CAEAGLLayer.class();' — needs parser
extension to recognize 'name :: Type = expr;' inside #objc_class
blocks, plus lazy-init-slot synthesis.
179 example tests pass (+1). zig build test green.
This commit is contained in:
126
src/ir/lower.zig
126
src/ir/lower.zig
@@ -9709,27 +9709,24 @@ pub const Lowering = struct {
|
||||
self.fn_ast_map.put(qualified, fd) catch {};
|
||||
self.declareFunction(fd, qualified);
|
||||
|
||||
// Skip class methods (no `*Self` first param) for IMP wiring
|
||||
// — A.4b only covers instance methods. Class-side hooks
|
||||
// (`+layerClass` etc.) land in M2.1 via the metaclass.
|
||||
if (method.is_static) continue;
|
||||
|
||||
// Selector mangling — A.1's deriveObjcSelector handles
|
||||
// `#selector("...")` override + the default rule.
|
||||
const user_arg_count = if (method.params.len > 0) method.params.len - 1 else 0;
|
||||
// `#selector("...")` override + the default rule. Static
|
||||
// methods use the same mangling rule (their first param
|
||||
// ISN'T *Self, so no offset).
|
||||
//
|
||||
// ABI for the IMP signature (both instance + class methods):
|
||||
// `(recv: id|Class, _cmd: SEL, ...user_args) -> ret`
|
||||
// For instance methods the user-declared self is at param[0]
|
||||
// (skipped); class methods have no self in the AST.
|
||||
const user_param_start: usize = if (method.is_static) 0 else 1;
|
||||
const user_arg_count = if (method.params.len > user_param_start) method.params.len - user_param_start else 0;
|
||||
const sel_info = self.deriveObjcSelector(method, user_arg_count);
|
||||
|
||||
// Type encoding for the IMP signature.
|
||||
// ABI: `(self: id, _cmd: SEL, ...user_args) -> ret`.
|
||||
// - return = method.return_type (or void)
|
||||
// - user_args = method.params[1..]
|
||||
// objcTypeEncodingFromSignature emits `<ret> @ : <args...>`
|
||||
// and the helper appends @ + : automatically.
|
||||
const ret_ty: TypeId = if (method.return_type) |rt| self.resolveType(rt) else .void;
|
||||
var arg_tys = std.ArrayList(TypeId).empty;
|
||||
defer arg_tys.deinit(self.alloc);
|
||||
if (method.params.len > 1) {
|
||||
for (method.params[1..]) |p_node| {
|
||||
if (method.params.len > user_param_start) {
|
||||
for (method.params[user_param_start..]) |p_node| {
|
||||
arg_tys.append(self.alloc, self.resolveType(p_node)) catch unreachable;
|
||||
}
|
||||
}
|
||||
@@ -9741,6 +9738,7 @@ pub const Lowering = struct {
|
||||
.sel = sel_info.sel,
|
||||
.encoding = encoding,
|
||||
.imp_name = imp_name,
|
||||
.is_class = method.is_static,
|
||||
}) catch unreachable;
|
||||
}
|
||||
|
||||
@@ -11622,6 +11620,13 @@ pub const Lowering = struct {
|
||||
}
|
||||
|
||||
fn emitObjcDefinedClassImp(self: *Lowering, fcd: *const ast.ForeignClassDecl, md: ast.ForeignMethodDecl) void {
|
||||
// Class methods (no `*Self` first param) skip the ivar read —
|
||||
// they have no instance state to thread through.
|
||||
if (md.is_static) {
|
||||
self.emitObjcDefinedClassStaticImp(fcd, md);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save+restore builder state — we're switching into a new fn
|
||||
// mid-pass and need to restore for the next emit_llvm steps.
|
||||
const saved_func = self.builder.func;
|
||||
@@ -11849,6 +11854,97 @@ pub const Lowering = struct {
|
||||
self.builder.finalize();
|
||||
}
|
||||
|
||||
/// Emit a C-ABI IMP trampoline for a CLASS method (no `*Self`
|
||||
/// first param) on a sx-defined `#objc_class`. M2.1(b).
|
||||
/// Registered on the metaclass by emit_llvm.
|
||||
///
|
||||
/// C-ABI: `(cls: Class, _cmd: SEL, ...user_args) -> ret`
|
||||
///
|
||||
/// Body:
|
||||
/// call @<Cls>.<method>(__sx_default_context, ...user_args)
|
||||
/// ret <result>
|
||||
///
|
||||
/// No ivar read — class methods have no per-instance state.
|
||||
fn emitObjcDefinedClassStaticImp(self: *Lowering, fcd: *const ast.ForeignClassDecl, md: ast.ForeignMethodDecl) void {
|
||||
const saved_func = self.builder.func;
|
||||
const saved_block = self.builder.current_block;
|
||||
const saved_counter = self.builder.inst_counter;
|
||||
defer {
|
||||
self.builder.func = saved_func;
|
||||
self.builder.current_block = saved_block;
|
||||
self.builder.inst_counter = saved_counter;
|
||||
}
|
||||
|
||||
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, md.name }) catch return;
|
||||
const name_id = self.module.types.internString(imp_name);
|
||||
const ptr_void = self.module.types.ptrTo(.void);
|
||||
|
||||
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
||||
params.append(self.alloc, .{ .name = self.module.types.internString("cls"), .ty = ptr_void }) catch return;
|
||||
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
|
||||
|
||||
// current_foreign_class lets `*Self` (if it appears in
|
||||
// user-arg types — rare for class methods) resolve to the
|
||||
// state-struct type. Save+restore.
|
||||
const saved_fc = self.current_foreign_class;
|
||||
self.current_foreign_class = fcd;
|
||||
defer self.current_foreign_class = saved_fc;
|
||||
|
||||
for (md.params, 0..) |p_node, i| {
|
||||
const pty = self.resolveType(p_node);
|
||||
params.append(self.alloc, .{
|
||||
.name = self.module.types.internString(md.param_names[i]),
|
||||
.ty = pty,
|
||||
}) catch return;
|
||||
}
|
||||
|
||||
const ret_ty: TypeId = if (md.return_type) |rt| self.resolveType(rt) else .void;
|
||||
const params_slice = params.toOwnedSlice(self.alloc) catch return;
|
||||
|
||||
_ = self.builder.beginFunction(name_id, params_slice, ret_ty);
|
||||
const func = self.builder.currentFunc();
|
||||
func.linkage = .external;
|
||||
func.call_conv = .c;
|
||||
func.has_implicit_ctx = false;
|
||||
|
||||
const entry_name = self.module.types.internString("entry");
|
||||
const entry = self.builder.appendBlock(entry_name, &.{});
|
||||
self.builder.switchToBlock(entry);
|
||||
|
||||
// Call @<Cls>.<method>(default_ctx, ...user_args).
|
||||
const body_name = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, md.name }) catch return;
|
||||
defer self.alloc.free(body_name);
|
||||
const body_fid = self.resolveFuncByName(body_name) orelse return;
|
||||
|
||||
const ctx_ref: ?Ref = blk: {
|
||||
if (!self.implicit_ctx_enabled) break :blk null;
|
||||
const dctx_gi = self.global_names.get("__sx_default_context") orelse break :blk null;
|
||||
break :blk self.builder.emit(.{ .global_addr = dctx_gi.id }, ptr_void);
|
||||
};
|
||||
|
||||
const num_user_args = params_slice.len - 2; // minus cls + _cmd
|
||||
const num_call_args = (if (ctx_ref != null) @as(usize, 1) else 0) + num_user_args;
|
||||
const call_args = self.alloc.alloc(Ref, num_call_args) catch return;
|
||||
var idx: usize = 0;
|
||||
if (ctx_ref) |c_ref| {
|
||||
call_args[idx] = c_ref;
|
||||
idx += 1;
|
||||
}
|
||||
var ip: usize = 2;
|
||||
while (ip < params_slice.len) : (ip += 1) {
|
||||
call_args[idx] = Ref.fromIndex(@intCast(ip));
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
const call_ref = self.builder.emit(.{ .call = .{
|
||||
.callee = body_fid,
|
||||
.args = call_args,
|
||||
} }, ret_ty);
|
||||
|
||||
if (ret_ty == .void) self.builder.retVoid() else self.builder.ret(call_ref, ret_ty);
|
||||
self.builder.finalize();
|
||||
}
|
||||
|
||||
/// Synthesize the `-dealloc` IMP for an sx-defined `#objc_class`.
|
||||
/// Runs when the Obj-C runtime drops the last retain on an
|
||||
/// instance.
|
||||
|
||||
Reference in New Issue
Block a user