From c39c8e15ebbf526285b37231ca57aa66a4390114 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 25 May 2026 23:40:51 +0300 Subject: [PATCH] ffi M2.1(b): class methods on sx-defined #objc_class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- examples/149-objc-class-method-static-imp.sx | 48 +++++++ src/ir/emit_llvm.zig | 42 +++--- src/ir/lower.zig | 126 +++++++++++++++--- src/ir/module.zig | 1 + .../142-objc-class-method-lowering.ir | 2 +- .../149-objc-class-method-static-imp.exit | 1 + .../149-objc-class-method-static-imp.txt | 1 + 7 files changed, 187 insertions(+), 34 deletions(-) create mode 100644 examples/149-objc-class-method-static-imp.sx create mode 100644 tests/expected/149-objc-class-method-static-imp.exit create mode 100644 tests/expected/149-objc-class-method-static-imp.txt diff --git a/examples/149-objc-class-method-static-imp.sx b/examples/149-objc-class-method-static-imp.sx new file mode 100644 index 0000000..0f9e88e --- /dev/null +++ b/examples/149-objc-class-method-static-imp.sx @@ -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; +} diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 6b9ac78..c0f1dcc 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -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 = @____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_"); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index b57e067..f2e0d30 100644 --- a/src/ir/lower.zig +++ b/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 ` @ : ` - // 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 @.(__sx_default_context, ...user_args) + /// ret + /// + /// 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 @.(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. diff --git a/src/ir/module.zig b/src/ir/module.zig index 39a3b37..363740e 100644 --- a/src/ir/module.zig +++ b/src/ir/module.zig @@ -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 { diff --git a/tests/expected/142-objc-class-method-lowering.ir b/tests/expected/142-objc-class-method-lowering.ir index 019e53b..7978b48 100644 --- a/tests/expected/142-objc-class-method-lowering.ir +++ b/tests/expected/142-objc-class-method-lowering.ir @@ -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_) diff --git a/tests/expected/149-objc-class-method-static-imp.exit b/tests/expected/149-objc-class-method-static-imp.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/149-objc-class-method-static-imp.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/149-objc-class-method-static-imp.txt b/tests/expected/149-objc-class-method-static-imp.txt new file mode 100644 index 0000000..c259c6d --- /dev/null +++ b/tests/expected/149-objc-class-method-static-imp.txt @@ -0,0 +1 @@ +class method: 42