diff --git a/examples/145-objc-class-method-dispatch.sx b/examples/145-objc-class-method-dispatch.sx new file mode 100644 index 0000000..e7d6d8d --- /dev/null +++ b/examples/145-objc-class-method-dispatch.sx @@ -0,0 +1,51 @@ +// M1.2 A.4b.iii — instance-method dispatch through the Obj-C +// runtime. Each instance method now gets a C-ABI IMP trampoline +// registered via 'class_addMethod' at class-pair init time. The +// runtime can dispatch 'objc_msgSend(obj, sel)' to the +// trampoline, which reads the '__sx_state' ivar to find the +// state struct and forwards to the sx body. +// +// End-to-end (verifies registration only — sx-side +// 'obj.bump()' calls still bail at the M1.2 A.7 dispatch gate +// until +alloc/-dealloc (A.5/A.6) land too): +// 1. class_getMethodImplementation(SxFoo, sel_registerName("bump")) +// returns a non-null IMP — proves the trampoline is wired. + +#import "modules/std.sx"; +#import "modules/compiler.sx"; +#import "modules/std/objc.sx"; + +class_getMethodImplementation :: (cls: *void, sel: *void) -> *void #foreign objc; + +SxFoo :: #objc_class("SxFoo") { + counter: s32; + + bump :: (self: *Self) { + self.counter += 1; + } + + add :: (self: *Self, n: s32) { + self.counter += n; + } +} + +main :: () -> s32 { + inline if OS == .macos { + cls : Class = objc_getClass("SxFoo".ptr); + if cls == null { print("FAIL: SxFoo not registered\n"); return 1; } + + sel_bump : SEL = sel_registerName("bump".ptr); + imp_bump : *void = class_getMethodImplementation(cls, sel_bump); + if imp_bump == null { print("FAIL: bump IMP missing\n"); return 1; } + + sel_add : SEL = sel_registerName("add:".ptr); + imp_add : *void = class_getMethodImplementation(cls, sel_add); + if imp_add == null { print("FAIL: add: IMP missing\n"); return 1; } + + print("IMP: bump ok, add: ok\n"); + } + inline if OS != .macos { + print("IMP: bump ok, add: ok\n"); + } + 0; +} diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 18d1025..049607c 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -563,6 +563,10 @@ pub const LLVMEmitter = struct { const alloc_pair_fn, const alloc_pair_ty = self.lazyDeclareCRuntime("objc_allocateClassPair", &[_]c.LLVMTypeRef{ ptr_ty, ptr_ty, i64_ty }, ptr_ty, 0); // class_addIvar(cls: *void, name: *u8, size: u64, log2align: u8, type: *u8) -> bool. const add_ivar_fn, const add_ivar_ty = self.lazyDeclareCRuntime("class_addIvar", &[_]c.LLVMTypeRef{ ptr_ty, ptr_ty, i64_ty, i8_ty, ptr_ty }, i8_ty, 0); + // sel_registerName(name: *u8) -> *void. + const sel_reg_fn, const sel_reg_ty = self.lazyDeclareCRuntime("sel_registerName", &[_]c.LLVMTypeRef{ptr_ty}, ptr_ty, 0); + // class_addMethod(cls: *void, sel: *void, imp: *void, types: *u8) -> bool. + const add_method_fn, const add_method_ty = self.lazyDeclareCRuntime("class_addMethod", &[_]c.LLVMTypeRef{ ptr_ty, ptr_ty, ptr_ty, ptr_ty }, i8_ty, 0); // objc_registerClassPair(cls: *void) -> void. const register_fn, const register_ty = self.lazyDeclareCRuntime("objc_registerClassPair", &[_]c.LLVMTypeRef{ptr_ty}, self.cached_void, 0); // class_getInstanceVariable(cls: *void, name: *u8) -> *Ivar. @@ -617,6 +621,29 @@ 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. + 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 + + // class_addMethod(cls, sel, imp, encoding) + var add_args: [4]c.LLVMValueRef = .{ cls_val, sel_val, imp_fn, enc_str_global }; + _ = c.LLVMBuildCall2(self.builder, add_method_ty, add_method_fn, &add_args, 4, ""); + } + // objc_registerClassPair(cls) var reg_args: [1]c.LLVMValueRef = .{cls_val}; _ = c.LLVMBuildCall2(self.builder, register_ty, register_fn, ®_args, 1, ""); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 84bf05c..6de6200 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -9645,11 +9645,21 @@ pub const Lowering = struct { /// For each bodied instance method on an sx-defined `#objc_class`, /// synthesize an `FnDecl` from the `ForeignMethodDecl`, register it - /// in `fn_ast_map` under `.`, and declare - /// the IR function so callers can resolve the name. Bodyless - /// declarations are skipped — they reference inherited / external - /// methods, not sx-side bodies. + /// in `fn_ast_map` under `.`, declare the IR + /// function, AND collect per-method registration data (selector + /// mangling + type encoding + IMP symbol name) into the class's + /// cache entry so emit_llvm can wire up `class_addMethod` calls + /// (M1.2 A.4b.iii). Bodyless declarations are skipped — they + /// reference inherited / external methods, not sx-side bodies. fn registerObjcDefinedClassMethods(self: *Lowering, fcd: *const ast.ForeignClassDecl) void { + // Set current_foreign_class so `*Self` substitutions in + // declareFunction's type resolution find the state struct. + const saved = self.current_foreign_class; + self.current_foreign_class = fcd; + defer self.current_foreign_class = saved; + + var method_infos = std.ArrayList(Module.ObjcDefinedMethodEntry).empty; + for (fcd.members) |m| { const method = switch (m) { .method => |md| md, @@ -9659,12 +9669,46 @@ pub const Lowering = struct { const fd = self.synthesizeFnDeclFromObjcMethod(method, body) orelse continue; const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, method.name }) catch continue; self.fn_ast_map.put(qualified, fd) catch {}; - // Set current_foreign_class while declaring so `*Self` in - // the signature resolves to the state struct (M1.2 A.2b). - const saved = self.current_foreign_class; - self.current_foreign_class = fcd; - defer self.current_foreign_class = saved; 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; + 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| { + arg_tys.append(self.alloc, self.resolveType(p_node)) catch unreachable; + } + } + const encoding = self.objcTypeEncodingFromSignature(ret_ty, arg_tys.items, null) catch continue; + + const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, method.name }) catch continue; + + method_infos.append(self.alloc, .{ + .sel = sel_info.sel, + .encoding = encoding, + .imp_name = imp_name, + }) catch unreachable; + } + + if (method_infos.items.len > 0) { + const methods_slice = method_infos.toOwnedSlice(self.alloc) catch return; + self.module.setObjcDefinedClassMethods(fcd.name, methods_slice); } } diff --git a/src/ir/module.zig b/src/ir/module.zig index c6f6e1c..39a3b37 100644 --- a/src/ir/module.zig +++ b/src/ir/module.zig @@ -62,8 +62,21 @@ pub const Module = struct { pub const ObjcClassEntry = struct { name: []const u8, slot: GlobalId }; /// Pointer back to the AST node lets later passes re-walk `members` /// for fields / methods / `#extends` / `#implements` without - /// duplicating that data here. - pub const ObjcDefinedClassEntry = struct { name: []const u8, decl: *const ast.ForeignClassDecl }; + /// duplicating that data here. `methods` holds emit-time registration + /// info derived in lower.zig (selector mangling + type encoding + + /// IMP symbol name) so emit_llvm can call `class_addMethod` per + /// instance method without re-resolving types from the AST. + pub const ObjcDefinedClassEntry = struct { + name: []const u8, + decl: *const ast.ForeignClassDecl, + methods: []const ObjcDefinedMethodEntry = &.{}, + }; + + pub const ObjcDefinedMethodEntry = 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`) + }; pub fn init(alloc: Allocator) Module { return .{ @@ -129,6 +142,18 @@ pub const Module = struct { self.objc_defined_class_cache.append(self.alloc, .{ .name = name, .decl = decl }) catch unreachable; } + /// Attach derived method-registration data to an existing + /// `objc_defined_class_cache` entry. emit_llvm reads this slice to + /// emit `class_addMethod` calls per instance method. + pub fn setObjcDefinedClassMethods(self: *Module, name: []const u8, methods: []const ObjcDefinedMethodEntry) void { + for (self.objc_defined_class_cache.items) |*entry| { + if (std.mem.eql(u8, entry.name, name)) { + entry.methods = methods; + return; + } + } + } + pub fn addFunction(self: *Module, func: Function) FuncId { const id = FuncId.fromIndex(@intCast(self.functions.items.len)); self.functions.append(self.alloc, func) catch unreachable; diff --git a/tests/expected/142-objc-class-method-lowering.ir b/tests/expected/142-objc-class-method-lowering.ir index 15750bc..29f3b91 100644 --- a/tests/expected/142-objc-class-method-lowering.ir +++ b/tests/expected/142-objc-class-method-lowering.ir @@ -27,6 +27,8 @@ @OBJC_IVAR_TYPE_ = private unnamed_addr constant [3 x i8] c"^v\00" @OBJC_CLASS_NAME_ = private unnamed_addr constant [9 x i8] c"NSObject\00" @OBJC_CLASS_NAME_.19 = private unnamed_addr constant [6 x i8] c"SxFoo\00" +@OBJC_METH_VAR_NAME_ = private unnamed_addr constant [5 x i8] c"bump\00" +@OBJC_METH_VAR_TYPE_ = private unnamed_addr constant [4 x i8] c"v@:\00" @llvm.global_ctors = appending global [1 x { i32, ptr, ptr }] [{ i32, ptr, ptr } { i32 65535, ptr @__sx_objc_defined_class_init, ptr null }] ; Function Attrs: nounwind @@ -804,6 +806,10 @@ declare ptr @objc_allocateClassPair(ptr, ptr, i64) declare i8 @class_addIvar(ptr, ptr, i64, i8, ptr) +declare ptr @sel_registerName(ptr) + +declare i8 @class_addMethod(ptr, ptr, ptr, ptr) + declare void @objc_registerClassPair(ptr) declare ptr @class_getInstanceVariable(ptr, ptr) @@ -813,6 +819,8 @@ 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_) + %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) %iv = call ptr @class_getInstanceVariable(ptr %cls, ptr @OBJC_IVAR_NAME_) store ptr %iv, ptr @__SxFoo_state_ivar, align 8 diff --git a/tests/expected/145-objc-class-method-dispatch.exit b/tests/expected/145-objc-class-method-dispatch.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/145-objc-class-method-dispatch.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/145-objc-class-method-dispatch.txt b/tests/expected/145-objc-class-method-dispatch.txt new file mode 100644 index 0000000..9694bbf --- /dev/null +++ b/tests/expected/145-objc-class-method-dispatch.txt @@ -0,0 +1 @@ +IMP: bump ok, add: ok