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:
51
examples/145-objc-class-method-dispatch.sx
Normal file
51
examples/145-objc-class-method-dispatch.sx
Normal 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;
|
||||
}
|
||||
@@ -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, ®_args, 1, "");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
1
tests/expected/145-objc-class-method-dispatch.exit
Normal file
1
tests/expected/145-objc-class-method-dispatch.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
tests/expected/145-objc-class-method-dispatch.txt
Normal file
1
tests/expected/145-objc-class-method-dispatch.txt
Normal file
@@ -0,0 +1 @@
|
||||
IMP: bump ok, add: ok
|
||||
Reference in New Issue
Block a user