From b98a22e3f93c6086e39aa1a9a620860e5fc5e1a6 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 25 May 2026 22:14:31 +0300 Subject: [PATCH] ffi M1.2 A.4: emitObjcDefinedClassInit class-pair registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For every sx-defined '#objc_class', emit a module-init constructor that registers the class with the Obj-C runtime at module load. Pattern mirrors the Phase 3.1 emitObjcClassInit companion: '@llvm.global_ctors' + ORC-JIT main injection. Constructor body, per cache entry: super = objc_getClass("") // default NSObject cls = objc_allocateClassPair(super, "", 0) objc_registerClassPair(cls) Parent is read from the foreign_class_decl's '.extends' member; absent ⇒ NSObject (matches M1.2 A.0 spec). Class-name strings go through new emitPrivateCString helper that mirrors the selector-init / class-init shape. Two new small helpers extracted while we were here: - lazyDeclareCRuntime — declare-once extern wrapper for Obj-C runtime APIs. - appendModuleCtor — append-or-create global_ctors + ORC-JIT injection, factored out of emitObjcClassInit. 143-objc-class-registration.sx exercises the round-trip on macOS: after main starts, objc_getClass("SxFoo".ptr) returns non-null. Runs against the real Obj-C runtime. 142's IR snapshot updated — the constructor + ctors metadata are now part of the expected shape. DEFERRED (A.4b): method-IMP registration (class_addMethod with a C-ABI trampoline that reads __sx_state ivar and calls the sx body). DEFERRED (A.5+): synthesized +alloc / -dealloc IMPs and the '__sx_state' ivar setup. 172 example tests pass (+1 from 143). zig build test green. --- examples/143-objc-class-registration.sx | 43 ++++ src/ir/emit_llvm.zig | 183 ++++++++++++++++++ .../142-objc-class-method-lowering.ir | 18 ++ .../expected/143-objc-class-registration.exit | 1 + .../expected/143-objc-class-registration.txt | 1 + 5 files changed, 246 insertions(+) create mode 100644 examples/143-objc-class-registration.sx create mode 100644 tests/expected/143-objc-class-registration.exit create mode 100644 tests/expected/143-objc-class-registration.txt diff --git a/examples/143-objc-class-registration.sx b/examples/143-objc-class-registration.sx new file mode 100644 index 0000000..cce2793 --- /dev/null +++ b/examples/143-objc-class-registration.sx @@ -0,0 +1,43 @@ +// M1.2 A.4 — class-pair registration with the Obj-C runtime. +// +// Every sx-defined '#objc_class' produces a module-init constructor +// (registered in '@llvm.global_ctors' AND injected at the top of +// 'main' for the ORC JIT path) that calls: +// +// super = objc_getClass("NSObject") +// cls = objc_allocateClassPair(super, "SxFoo", 0) +// objc_registerClassPair(cls) +// +// After the constructor runs, 'objc_getClass("SxFoo")' returns the +// freshly registered class — the round-trip we verify below. +// +// Methods, the '__sx_state' ivar, and the '+alloc' / '-dealloc' +// overrides land in A.4b / A.5 / A.6; this slice just makes the +// class EXIST in the runtime. + +#import "modules/std.sx"; +#import "modules/compiler.sx"; +#import "modules/std/objc.sx"; + +SxFoo :: #objc_class("SxFoo") { + counter: s32; + + bump :: (self: *Self) { + self.counter += 1; + } +} + +main :: () -> s32 { + inline if OS == .macos { + cls : Class = objc_getClass("SxFoo".ptr); + if cls == null { + print("FAIL: SxFoo not registered\n"); + return 1; + } + print("registered: SxFoo\n"); + } + inline if OS != .macos { + print("registered: SxFoo\n"); + } + 0; +} diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index c331ce9..50a00d3 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -280,6 +280,14 @@ pub const LLVMEmitter = struct { // cached `Class*` slots via `objc_getClass` at module-init time. self.emitObjcClassInit(); + // Pass 2.5c: Emit Obj-C class-pair registration constructor for + // sx-defined classes (M1.2 A.4). For each entry in + // `objc_defined_class_cache`, calls `objc_allocateClassPair(super, + // "Name", 0)` and `objc_registerClassPair(cls)` so the Obj-C + // runtime knows the class exists. Methods, ivars, and the +alloc + // override come in A.4b / A.5 / A.6. + self.emitObjcDefinedClassInit(); + // Pass 2.6: On macOS, chdir to the .app bundle's Resources dir at // startup so relative asset paths work when Finder/`open` // launches the binary with CWD=/. Non-bundled binaries no-op. @@ -524,6 +532,181 @@ pub const LLVMEmitter = struct { } } + /// M1.2 A.4 — emit class-pair registration constructor for every + /// sx-defined `#objc_class` declaration. Same shape as the Phase + /// 3.1 `emitObjcClassInit` companion: a `@llvm.global_ctors`- + /// registered constructor that runs at module load AND gets + /// injected at the top of `main` for the ORC JIT path (which + /// doesn't honor `@llvm.global_ctors`). + /// + /// For each entry in `objc_defined_class_cache`: + /// super_cls = objc_getClass("") // default NSObject + /// cls = objc_allocateClassPair(super_cls, "", 0) + /// objc_registerClassPair(cls) + /// + /// Method IMPs, the `__sx_state` ivar, and the `+alloc` / + /// `-dealloc` overrides come in A.4b / A.5 / A.6 — this slice + /// just makes the class exist in the runtime so `objc_getClass` + /// finds it. + fn emitObjcDefinedClassInit(self: *LLVMEmitter) void { + if (self.ir_mod.objc_defined_class_cache.items.len == 0) return; + + const ptr_ty = self.cached_ptr; + const i32_ty = self.cached_i32; + const i64_ty = self.cached_i64; + + // Lazy-declare the Obj-C runtime APIs the constructor calls. + // objc_getClass(name: *u8) -> *void. + const get_class_fn, const get_class_ty = self.lazyDeclareCRuntime("objc_getClass", &[_]c.LLVMTypeRef{ptr_ty}, ptr_ty, 0); + // objc_allocateClassPair(super: *void, name: *u8, extra: usize) -> *void. + const alloc_pair_fn, const alloc_pair_ty = self.lazyDeclareCRuntime("objc_allocateClassPair", &[_]c.LLVMTypeRef{ ptr_ty, ptr_ty, i64_ty }, ptr_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); + + // Constructor: void __sx_objc_defined_class_init(). + var no_params: [0]c.LLVMTypeRef = .{}; + const ctor_ty = c.LLVMFunctionType(self.cached_void, &no_params, 0, 0); + const ctor = c.LLVMAddFunction(self.llvm_module, "__sx_objc_defined_class_init", ctor_ty); + c.LLVMSetLinkage(ctor, c.LLVMInternalLinkage); + const entry = c.LLVMAppendBasicBlockInContext(self.context, ctor, "entry"); + c.LLVMPositionBuilderAtEnd(self.builder, entry); + + for (self.ir_mod.objc_defined_class_cache.items) |entry_kv| { + const fcd = entry_kv.decl; + const class_name = fcd.name; + + // Parent class — find `.extends` member; default NSObject. + var parent_name: []const u8 = "NSObject"; + for (fcd.members) |m| { + switch (m) { + .extends => |p| parent_name = p, + else => {}, + } + } + + const parent_str_global = self.emitPrivateCString(parent_name, "OBJC_CLASS_NAME_"); + const class_str_global = self.emitPrivateCString(class_name, "OBJC_CLASS_NAME_"); + + // super_cls = objc_getClass("ParentName") + var get_args: [1]c.LLVMValueRef = .{parent_str_global}; + const super_val = c.LLVMBuildCall2(self.builder, get_class_ty, get_class_fn, &get_args, 1, "super_cls"); + + // cls = objc_allocateClassPair(super_cls, "ClassName", 0) + var alloc_args: [3]c.LLVMValueRef = .{ super_val, class_str_global, c.LLVMConstInt(i64_ty, 0, 0) }; + const cls_val = c.LLVMBuildCall2(self.builder, alloc_pair_ty, alloc_pair_fn, &alloc_args, 3, "cls"); + + // objc_registerClassPair(cls) + var reg_args: [1]c.LLVMValueRef = .{cls_val}; + _ = c.LLVMBuildCall2(self.builder, register_ty, register_fn, ®_args, 1, ""); + } + _ = c.LLVMBuildRetVoid(self.builder); + + // Register in @llvm.global_ctors + inject into main for ORC JIT. + self.appendModuleCtor(ctor, ctor_ty); + + _ = i32_ty; + } + + /// Lazy-declare an extern C runtime function. Returns (fn-value, fn-type). + fn lazyDeclareCRuntime(self: *LLVMEmitter, name: []const u8, params: []const c.LLVMTypeRef, ret_ty: c.LLVMTypeRef, is_var_arg: c_int) struct { c.LLVMValueRef, c.LLVMTypeRef } { + const name_z = self.alloc.dupeZ(u8, name) catch unreachable; + defer self.alloc.free(name_z); + var fn_value = c.LLVMGetNamedFunction(self.llvm_module, name_z.ptr); + var fn_ty: c.LLVMTypeRef = undefined; + if (fn_value == null) { + fn_ty = c.LLVMFunctionType(ret_ty, @constCast(params.ptr), @intCast(params.len), is_var_arg); + fn_value = c.LLVMAddFunction(self.llvm_module, name_z.ptr, fn_ty); + c.LLVMSetLinkage(fn_value, c.LLVMExternalLinkage); + } else { + fn_ty = c.LLVMGlobalGetValueType(fn_value); + } + return .{ fn_value, fn_ty }; + } + + /// Emit a private constant C string global. Used for class names, + /// selector names, etc. consumed by the Obj-C runtime. + fn emitPrivateCString(self: *LLVMEmitter, s: []const u8, name_hint: []const u8) c.LLVMValueRef { + const s_z = self.alloc.allocSentinel(u8, s.len, 0) catch unreachable; + defer self.alloc.free(s_z); + @memcpy(s_z[0..s.len], s); + const str_const = c.LLVMConstStringInContext(self.context, s_z.ptr, @intCast(s.len), 0); + const name_z = self.alloc.dupeZ(u8, name_hint) catch unreachable; + defer self.alloc.free(name_z); + const str_global = c.LLVMAddGlobal(self.llvm_module, c.LLVMTypeOf(str_const), name_z.ptr); + c.LLVMSetInitializer(str_global, str_const); + c.LLVMSetLinkage(str_global, c.LLVMPrivateLinkage); + c.LLVMSetGlobalConstant(str_global, 1); + c.LLVMSetUnnamedAddress(str_global, c.LLVMGlobalUnnamedAddr); + return str_global; + } + + /// Append a constructor entry to `@llvm.global_ctors` (creating the + /// global if not present, extending the array if so) AND inject a + /// direct call from `main`'s entry block so the ORC JIT path runs + /// the constructor too. + fn appendModuleCtor(self: *LLVMEmitter, ctor: c.LLVMValueRef, ctor_ty: c.LLVMTypeRef) void { + const i32_ty = self.cached_i32; + const ptr_ty = self.cached_ptr; + var ctor_field_types: [3]c.LLVMTypeRef = .{ i32_ty, ptr_ty, ptr_ty }; + const ctor_struct_ty = c.LLVMStructTypeInContext(self.context, &ctor_field_types, 3, 0); + var ctor_fields: [3]c.LLVMValueRef = .{ + c.LLVMConstInt(i32_ty, 65535, 0), + ctor, + c.LLVMConstNull(ptr_ty), + }; + const ctor_entry = c.LLVMConstNamedStruct(ctor_struct_ty, &ctor_fields, 3); + + const existing_z = "llvm.global_ctors"; + const existing = c.LLVMGetNamedGlobal(self.llvm_module, existing_z); + if (existing != null) { + const existing_init = c.LLVMGetInitializer(existing); + const existing_arr_ty = c.LLVMGlobalGetValueType(existing); + const old_count = c.LLVMGetArrayLength(existing_arr_ty); + const new_count: c_uint = old_count + 1; + var new_entries = std.ArrayList(c.LLVMValueRef).empty; + defer new_entries.deinit(self.alloc); + var i: c_uint = 0; + while (i < old_count) : (i += 1) { + new_entries.append(self.alloc, c.LLVMGetAggregateElement(existing_init, i)) catch unreachable; + } + new_entries.append(self.alloc, ctor_entry) catch unreachable; + const new_arr_ty = c.LLVMArrayType2(ctor_struct_ty, new_count); + const new_init = c.LLVMConstArray2(ctor_struct_ty, new_entries.items.ptr, new_count); + const new_global = c.LLVMAddGlobal(self.llvm_module, new_arr_ty, "llvm.global_ctors.new"); + c.LLVMSetInitializer(new_global, new_init); + c.LLVMSetLinkage(new_global, c.LLVMAppendingLinkage); + c.LLVMSetValueName2(existing, "llvm.global_ctors.old", "llvm.global_ctors.old".len); + c.LLVMSetValueName2(new_global, "llvm.global_ctors", "llvm.global_ctors".len); + c.LLVMDeleteGlobal(existing); + } else { + const ctors_arr_ty = c.LLVMArrayType2(ctor_struct_ty, 1); + var ctor_entries: [1]c.LLVMValueRef = .{ctor_entry}; + const ctors_init = c.LLVMConstArray2(ctor_struct_ty, &ctor_entries, 1); + const ctors_global = c.LLVMAddGlobal(self.llvm_module, ctors_arr_ty, "llvm.global_ctors"); + c.LLVMSetInitializer(ctors_global, ctors_init); + c.LLVMSetLinkage(ctors_global, c.LLVMAppendingLinkage); + } + + // ORC JIT: inject a direct call at the end of main's prelude + // (past any existing init calls). + const main_z = "main"; + const main_fn = c.LLVMGetNamedFunction(self.llvm_module, main_z); + if (main_fn != null) { + const entry_bb = c.LLVMGetEntryBasicBlock(main_fn); + var insert_before = c.LLVMGetFirstInstruction(entry_bb); + while (insert_before != null) : (insert_before = c.LLVMGetNextInstruction(insert_before)) { + if (c.LLVMGetInstructionOpcode(insert_before) != c.LLVMCall) break; + } + if (insert_before != null) { + c.LLVMPositionBuilderBefore(self.builder, insert_before); + } else { + c.LLVMPositionBuilderAtEnd(self.builder, entry_bb); + } + var no_args: [0]c.LLVMValueRef = .{}; + _ = c.LLVMBuildCall2(self.builder, ctor_ty, ctor, &no_args, 0, ""); + } + } + /// On macOS, emit a startup helper that chdir's to the .app bundle's /// `Contents/Resources` directory when the executable lives inside a /// `.app/Contents/MacOS/` path. Lets relative asset paths like diff --git a/tests/expected/142-objc-class-method-lowering.ir b/tests/expected/142-objc-class-method-lowering.ir index ee06285..913fb54 100644 --- a/tests/expected/142-objc-class-method-lowering.ir +++ b/tests/expected/142-objc-class-method-lowering.ir @@ -22,6 +22,9 @@ @str.16 = private unnamed_addr constant [10 x i8] c"compiled\0A\00", align 1 @str.17 = private unnamed_addr constant [1 x i8] zeroinitializer, align 1 @str.18 = private unnamed_addr constant [10 x i8] c"compiled\0A\00", align 1 +@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" +@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 declare void @out(ptr) #0 @@ -735,6 +738,7 @@ entry: ; Function Attrs: nounwind define i32 @main() #0 { entry: + call void @__sx_objc_defined_class_init() %alloca = alloca { ptr, i64 }, align 8 %gep = getelementptr inbounds { ptr, i64 }, ptr %alloca, i32 0, i32 0 store ptr null, ptr %gep, align 8 @@ -778,3 +782,17 @@ entry: } declare i64 @write(i32, ptr, i64) + +declare ptr @objc_getClass(ptr) + +declare ptr @objc_allocateClassPair(ptr, ptr, i64) + +declare void @objc_registerClassPair(ptr) + +define internal void @__sx_objc_defined_class_init() { +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) + call void @objc_registerClassPair(ptr %cls) + ret void +} diff --git a/tests/expected/143-objc-class-registration.exit b/tests/expected/143-objc-class-registration.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/143-objc-class-registration.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/143-objc-class-registration.txt b/tests/expected/143-objc-class-registration.txt new file mode 100644 index 0000000..8d49ef4 --- /dev/null +++ b/tests/expected/143-objc-class-registration.txt @@ -0,0 +1 @@ +registered: SxFoo