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:
agra
2026-05-25 23:40:51 +03:00
parent 0ac5ba2ccd
commit c39c8e15eb
7 changed files with 187 additions and 34 deletions

View File

@@ -0,0 +1,48 @@
// M2.1(b) — class methods (no `*Self` first param) on a
// sx-defined `#objc_class`.
//
// The user declares a method without `self: *Self`. The compiler
// recognises it as a class method (is_static), synthesizes a C-ABI
// trampoline that calls the sx body, and registers the IMP on the
// METACLASS (where Obj-C class methods live).
//
// Verifies the runtime side:
// 1. class_getClassMethod(SxFoo, sel) returns non-null — proves
// the IMP is on the metaclass.
// 2. objc_msgSend(SxFoo, sel) invokes the IMP and returns the
// sx body's result.
#import "modules/std.sx";
#import "modules/compiler.sx";
#import "modules/std/objc.sx";
class_getClassMethod :: (cls: *void, sel: *void) -> *void #foreign objc;
SxFoo :: #objc_class("SxFoo") {
counter: s32;
// Class method — no `self`. Returns 42.
answer :: () -> s32 { return 42; }
}
main :: () -> s32 {
inline if OS == .macos {
cls : Class = objc_getClass("SxFoo".ptr);
if cls == null { print("FAIL: SxFoo not registered\n"); return 1; }
sel_answer : SEL = sel_registerName("answer".ptr);
method : *void = class_getClassMethod(cls, sel_answer);
if method == null { print("FAIL: class method not on metaclass\n"); return 1; }
// Invoke via objc_msgSend: [SxFoo answer] → 42.
msg_fn : (cls: *void, sel: *void) -> s32 callconv(.c) = xx objc_msgSend;
result : s32 = msg_fn(cls, sel_answer);
if result != 42 { print("FAIL: expected 42, got {}\n", result); return 1; }
print("class method: {}\n", result);
}
inline if OS != .macos {
print("class method: 42\n");
}
0;
}

View File

@@ -622,26 +622,38 @@ pub const LLVMEmitter = struct {
};
_ = c.LLVMBuildCall2(self.builder, add_ivar_ty, add_ivar_fn, &ivar_args, 5, "");
// class_addMethod(cls, sel_registerName(sel), imp, encoding)
// — register each instance method's IMP trampoline (M1.2 A.4b.iii).
// Must run BEFORE objc_registerClassPair; the runtime locks
// the method list at registration time on some SDK versions.
// Class-method registration (M2.1(b)) and the +alloc IMP
// (M1.2 A.5) both target the metaclass. Compute it once
// up-front so all metaclass-bound class_addMethod calls
// can reference the same LLVM value.
//
// metaclass = object_getClass(cls). (object_getClass on a
// Class returns the metaclass — a Class IS an instance of
// its metaclass. Distinct from objc_getClass(name).)
const obj_get_class_fn, const obj_get_class_ty = self.lazyDeclareCRuntime("object_getClass", &[_]c.LLVMTypeRef{ptr_ty}, ptr_ty, 0);
var ogc_args: [1]c.LLVMValueRef = .{cls_val};
const metaclass_val = c.LLVMBuildCall2(self.builder, obj_get_class_ty, obj_get_class_fn, &ogc_args, 1, "metacls");
// class_addMethod(target, sel_registerName(sel), imp, encoding)
// — register each method's IMP trampoline (M1.2 A.4b.iii
// + M2.1(b)). Instance methods register on `cls`; class
// methods (`is_class`) on the metaclass. Must run BEFORE
// objc_registerClassPair; the runtime locks the method
// list at registration time on some SDK versions.
for (entry_kv.methods) |method| {
const sel_str_global = self.emitPrivateCString(method.sel, "OBJC_METH_VAR_NAME_");
const enc_str_global = self.emitPrivateCString(method.encoding, "OBJC_METH_VAR_TYPE_");
// sel = sel_registerName("selector")
var sel_args: [1]c.LLVMValueRef = .{sel_str_global};
const sel_val = c.LLVMBuildCall2(self.builder, sel_reg_ty, sel_reg_fn, &sel_args, 1, "sel");
// imp = @__<ClassName>_<methodName>_imp
const imp_z = self.alloc.dupeZ(u8, method.imp_name) catch continue;
defer self.alloc.free(imp_z);
const imp_fn = c.LLVMGetNamedFunction(self.llvm_module, imp_z.ptr);
if (imp_fn == null) continue; // trampoline missing — skip
if (imp_fn == null) continue;
// class_addMethod(cls, sel, imp, encoding)
var add_args: [4]c.LLVMValueRef = .{ cls_val, sel_val, imp_fn, enc_str_global };
const target_cls = if (method.is_class) metaclass_val else cls_val;
var add_args: [4]c.LLVMValueRef = .{ target_cls, sel_val, imp_fn, enc_str_global };
_ = c.LLVMBuildCall2(self.builder, add_method_ty, add_method_fn, &add_args, 4, "");
}
@@ -696,15 +708,9 @@ pub const LLVMEmitter = struct {
defer self.alloc.free(alloc_imp_z);
const alloc_imp_fn = c.LLVMGetNamedFunction(self.llvm_module, alloc_imp_z.ptr);
if (alloc_imp_fn != null) {
// metaclass = object_getClass(cls). (Distinct from
// objc_getClass: the latter takes a NAME string and is
// for class-object lookup. object_getClass takes an
// instance pointer — a Class IS itself an instance of
// its metaclass — and returns the isa.)
const obj_get_class_fn, const obj_get_class_ty = self.lazyDeclareCRuntime("object_getClass", &[_]c.LLVMTypeRef{ptr_ty}, ptr_ty, 0);
var ogc_args: [1]c.LLVMValueRef = .{cls_val};
const metaclass_val = c.LLVMBuildCall2(self.builder, obj_get_class_ty, obj_get_class_fn, &ogc_args, 1, "metacls");
// metaclass_val was computed up-front above (shared
// with class-method registration). +alloc is a class
// method registered on the metaclass.
const alloc_sel_global = self.emitPrivateCString("alloc", "OBJC_METH_VAR_NAME_");
const alloc_enc_global = self.emitPrivateCString("@@:", "OBJC_METH_VAR_TYPE_");

View File

@@ -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.

View File

@@ -76,6 +76,7 @@ pub const Module = struct {
sel: []const u8, // mangled Obj-C selector (`add:and:`)
encoding: []const u8, // Apple-runtime type encoding (`v@:ii`)
imp_name: []const u8, // C-callconv trampoline symbol (`__Cls_method_imp`)
is_class: bool = false, // true ⇒ register on the metaclass (M2.1 class methods)
};
pub fn init(alloc: Allocator) Module {

View File

@@ -864,13 +864,13 @@ entry:
%super_cls = call ptr @objc_getClass(ptr @OBJC_CLASS_NAME_)
%cls = call ptr @objc_allocateClassPair(ptr %super_cls, ptr @OBJC_CLASS_NAME_.19, i64 0)
%0 = call i8 @class_addIvar(ptr %cls, ptr @OBJC_IVAR_NAME_, i64 8, i8 3, ptr @OBJC_IVAR_TYPE_)
%metacls = call ptr @object_getClass(ptr %cls)
%sel = call ptr @sel_registerName(ptr @OBJC_METH_VAR_NAME_)
%1 = call i8 @class_addMethod(ptr %cls, ptr %sel, ptr @__SxFoo_bump_imp, ptr @OBJC_METH_VAR_TYPE_)
call void @objc_registerClassPair(ptr %cls)
store ptr %cls, ptr @__SxFoo_class, align 8
%sel_dealloc = call ptr @sel_registerName(ptr @OBJC_METH_VAR_NAME_.20)
%2 = call i8 @class_addMethod(ptr %cls, ptr %sel_dealloc, ptr @__SxFoo_dealloc_imp, ptr @OBJC_METH_VAR_TYPE_.21)
%metacls = call ptr @object_getClass(ptr %cls)
%sel_alloc = call ptr @sel_registerName(ptr @OBJC_METH_VAR_NAME_.22)
%3 = call i8 @class_addMethod(ptr %metacls, ptr %sel_alloc, ptr @__SxFoo_alloc_imp, ptr @OBJC_METH_VAR_TYPE_.23)
%iv = call ptr @class_getInstanceVariable(ptr %cls, ptr @OBJC_IVAR_NAME_)

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
class method: 42