ffi M1.2 A.4b.iii: class_addMethod wires IMPs to the Obj-C runtime

For each instance method on a sx-defined '#objc_class', the
class-pair init constructor now:

  sel = sel_registerName("selector_string")
  imp = @__<Cls>_<method>_imp                  (M1.2 A.4b.ii)
  class_addMethod(cls, sel, imp, "<encoding>")

before objc_registerClassPair. The IMP trampoline (A.4b.ii)
already bridges C-ABI -> sx body. With registration in place,
'objc_msgSend(obj, sel_bump)' now routes to the trampoline,
which reads __sx_state ivar and forwards to '@<Cls>.<method>'.

To get selector + type-encoding strings out of lower.zig and
into emit_llvm, ObjcDefinedClassEntry gains a 'methods' slice:

  pub const ObjcDefinedMethodEntry = struct {
      sel: []const u8,       // mangled selector (M1.2 A.1's deriveObjcSelector)
      encoding: []const u8,  // type encoding (M1.2 A.1's objcTypeEncodingFromSignature)
      imp_name: []const u8,  // C-callconv trampoline symbol
  };

registerObjcDefinedClassMethods populates this when it declares
each method's body function; Module.setObjcDefinedClassMethods
attaches the slice to the cache entry by name. Static (class-
side) methods are skipped — A.4b only covers instance methods;
class-method hooks like '+layerClass' land in M2.1.

emit_llvm reads entry.methods and emits class_addMethod inside
the per-class init block, before objc_registerClassPair (the
runtime locks the method list at register time on some SDK
versions).

145-objc-class-method-dispatch.sx verifies end-to-end:
class_getMethodImplementation(SxFoo, sel_registerName("bump"))
returns non-null after main starts. Both niladic ('bump') and
single-arg ('add:') selectors checked.

Still gated (A.7): sx-side 'obj.bump()' calls. The dispatch
gate at lower.zig:4407 hasn't opened — A.5 (+alloc) and A.6
(-dealloc) need to land first so the integration test
ffi-objc-defined-class-01-instance.sx (full state round-trip)
can exercise the full lifecycle.

174 example tests pass (+1 from 145). zig build test green.
This commit is contained in:
agra
2026-05-25 22:58:20 +03:00
parent c0b338eaa4
commit 87572579b4
7 changed files with 168 additions and 11 deletions

View File

@@ -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;
}

View File

@@ -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 = @__<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
// 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, &reg_args, 1, "");

View File

@@ -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 `<ClassName>.<methodName>`, 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 `<ClassName>.<methodName>`, 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 `<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| {
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);
}
}

View File

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

View File

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

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
IMP: bump ok, add: ok