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

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