diff --git a/src/backend/llvm/ffi_ctors.zig b/src/backend/llvm/ffi_ctors.zig new file mode 100644 index 0000000..5bfee0d --- /dev/null +++ b/src/backend/llvm/ffi_ctors.zig @@ -0,0 +1,476 @@ +const std = @import("std"); +const llvm = @import("../../llvm_api.zig"); +const c = llvm.c; +const emit = @import("../../ir/emit_llvm.zig"); + +const LLVMEmitter = emit.LLVMEmitter; + +/// Obj-C / JNI runtime-constructor emission (architecture phase A7.3), extracted +/// from `LLVMEmitter`. A backend `*LLVMEmitter` facade (field `e`): it builds the +/// module-init constructors that populate the cached selector / class slots and +/// register sx-defined `#objc_class` class pairs (IMP tables, ivars, +alloc / +/// -dealloc / property IMPs, `#implements` protocol conformances). Reads the +/// emit-time caches (`ir_mod.objc_*_cache`, `global_map`) + cached LLVM handles +/// via `self.e.*`; the shared infra it calls back into +/// (`lazyDeclareCRuntime`/`emitPrivateCString`/`injectCtorIntoMain`) stays on +/// `LLVMEmitter`. `LLVMEmitter.emit` drives pass order via `self.ffiCtors()`. +pub const FfiCtors = struct { + e: *LLVMEmitter, + + pub fn emitObjcSelectorInit(self: FfiCtors) void { + if (self.e.ir_mod.objc_selector_cache.items.len == 0) return; + + // Lazy-declare sel_registerName for the constructor body — + // lower.zig only declares it when a non-literal selector + // appears, which the constructor doesn't depend on. + const sel_reg_name = "sel_registerName"; + const sel_reg_z = self.e.alloc.dupeZ(u8, sel_reg_name) catch unreachable; + defer self.e.alloc.free(sel_reg_z); + var sel_reg_fn = c.LLVMGetNamedFunction(self.e.llvm_module, sel_reg_z.ptr); + var sel_reg_ty: c.LLVMTypeRef = undefined; + if (sel_reg_fn == null) { + var params: [1]c.LLVMTypeRef = .{self.e.cached_ptr}; + sel_reg_ty = c.LLVMFunctionType(self.e.cached_ptr, ¶ms, 1, 0); + sel_reg_fn = c.LLVMAddFunction(self.e.llvm_module, sel_reg_z.ptr, sel_reg_ty); + c.LLVMSetLinkage(sel_reg_fn, c.LLVMExternalLinkage); + } else { + sel_reg_ty = c.LLVMGlobalGetValueType(sel_reg_fn); + } + + // Constructor: void __sx_objc_selector_init(). + var no_params: [0]c.LLVMTypeRef = .{}; + const ctor_ty = c.LLVMFunctionType(self.e.cached_void, &no_params, 0, 0); + const ctor = c.LLVMAddFunction(self.e.llvm_module, "__sx_objc_selector_init", ctor_ty); + c.LLVMSetLinkage(ctor, c.LLVMInternalLinkage); + const entry = c.LLVMAppendBasicBlockInContext(self.e.context, ctor, "entry"); + c.LLVMPositionBuilderAtEnd(self.e.builder, entry); + + for (self.e.ir_mod.objc_selector_cache.items) |entry_kv| { + const sel_str = entry_kv.sel; + const slot_gid = entry_kv.slot; + const slot_global = self.e.global_map.get(@intCast(slot_gid.index())) orelse continue; + + // Method-name C-string — names match clang's convention + // so debuggers / nm / dyld see the same symbols, even + // though the surrounding section tagging isn't load- + // bearing in our JIT path. + const meth_str_z = self.e.alloc.allocSentinel(u8, sel_str.len, 0) catch continue; + defer self.e.alloc.free(meth_str_z); + @memcpy(meth_str_z[0..sel_str.len], sel_str); + const str_const = c.LLVMConstStringInContext(self.e.context, meth_str_z.ptr, @intCast(sel_str.len), 0); + const str_global = c.LLVMAddGlobal(self.e.llvm_module, c.LLVMTypeOf(str_const), "OBJC_METH_VAR_NAME_"); + c.LLVMSetInitializer(str_global, str_const); + c.LLVMSetLinkage(str_global, c.LLVMPrivateLinkage); + c.LLVMSetGlobalConstant(str_global, 1); + c.LLVMSetUnnamedAddress(str_global, c.LLVMGlobalUnnamedAddr); + + var sel_args: [1]c.LLVMValueRef = .{str_global}; + const sel_val = c.LLVMBuildCall2(self.e.builder, sel_reg_ty, sel_reg_fn, &sel_args, 1, "sel"); + _ = c.LLVMBuildStore(self.e.builder, sel_val, slot_global); + } + _ = c.LLVMBuildRetVoid(self.e.builder); + + // Register the constructor in @llvm.global_ctors. dyld picks + // this up for a fully-linked binary at load time. + const i32_ty = self.e.cached_i32; + const ptr_ty = self.e.cached_ptr; + var ctor_field_types: [3]c.LLVMTypeRef = .{ i32_ty, ptr_ty, ptr_ty }; + const ctor_struct_ty = c.LLVMStructTypeInContext(self.e.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 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.e.llvm_module, ctors_arr_ty, "llvm.global_ctors"); + c.LLVMSetInitializer(ctors_global, ctors_init); + c.LLVMSetLinkage(ctors_global, c.LLVMAppendingLinkage); + + // BUT — LLVM's ORC JIT (the engine for `sx run`) doesn't + // automatically run `@llvm.global_ctors`. Inject a direct + // call from `main`'s entry block as well; idempotent under + // dyld (sel_registerName returns the same SEL on second call). + const main_z = "main"; + const main_fn = c.LLVMGetNamedFunction(self.e.llvm_module, main_z); + if (main_fn != null) { + const entry_bb = c.LLVMGetEntryBasicBlock(main_fn); + const first_inst = c.LLVMGetFirstInstruction(entry_bb); + if (first_inst != null) { + c.LLVMPositionBuilderBefore(self.e.builder, first_inst); + } else { + c.LLVMPositionBuilderAtEnd(self.e.builder, entry_bb); + } + var no_args: [0]c.LLVMValueRef = .{}; + _ = c.LLVMBuildCall2(self.e.builder, ctor_ty, ctor, &no_args, 0, ""); + } + } + + /// Phase 3.1 companion to `emitObjcSelectorInit`. Walks + /// `module.objc_class_cache` and synthesizes a constructor that + /// populates each cached `Class*` slot via `objc_getClass(name)` + /// exactly once at module-init. Registered in `@llvm.global_ctors` + /// AND injected at the top of `main()` for the ORC JIT path. + pub fn emitObjcClassInit(self: FfiCtors) void { + if (self.e.ir_mod.objc_class_cache.items.len == 0) return; + + // Lazy-declare objc_getClass(name: *u8) -> *void. + const get_class_name = "objc_getClass"; + const get_class_z = self.e.alloc.dupeZ(u8, get_class_name) catch unreachable; + defer self.e.alloc.free(get_class_z); + var get_class_fn = c.LLVMGetNamedFunction(self.e.llvm_module, get_class_z.ptr); + var get_class_ty: c.LLVMTypeRef = undefined; + if (get_class_fn == null) { + var params: [1]c.LLVMTypeRef = .{self.e.cached_ptr}; + get_class_ty = c.LLVMFunctionType(self.e.cached_ptr, ¶ms, 1, 0); + get_class_fn = c.LLVMAddFunction(self.e.llvm_module, get_class_z.ptr, get_class_ty); + c.LLVMSetLinkage(get_class_fn, c.LLVMExternalLinkage); + } else { + get_class_ty = c.LLVMGlobalGetValueType(get_class_fn); + } + + // Constructor: void __sx_objc_class_init(). + var no_params: [0]c.LLVMTypeRef = .{}; + const ctor_ty = c.LLVMFunctionType(self.e.cached_void, &no_params, 0, 0); + const ctor = c.LLVMAddFunction(self.e.llvm_module, "__sx_objc_class_init", ctor_ty); + c.LLVMSetLinkage(ctor, c.LLVMInternalLinkage); + const entry = c.LLVMAppendBasicBlockInContext(self.e.context, ctor, "entry"); + c.LLVMPositionBuilderAtEnd(self.e.builder, entry); + + for (self.e.ir_mod.objc_class_cache.items) |entry_kv| { + const class_name = entry_kv.name; + const slot_gid = entry_kv.slot; + const slot_global = self.e.global_map.get(@intCast(slot_gid.index())) orelse continue; + + // Class-name C-string. + const name_z = self.e.alloc.allocSentinel(u8, class_name.len, 0) catch continue; + defer self.e.alloc.free(name_z); + @memcpy(name_z[0..class_name.len], class_name); + const str_const = c.LLVMConstStringInContext(self.e.context, name_z.ptr, @intCast(class_name.len), 0); + const str_global = c.LLVMAddGlobal(self.e.llvm_module, c.LLVMTypeOf(str_const), "OBJC_CLASS_NAME_"); + c.LLVMSetInitializer(str_global, str_const); + c.LLVMSetLinkage(str_global, c.LLVMPrivateLinkage); + c.LLVMSetGlobalConstant(str_global, 1); + c.LLVMSetUnnamedAddress(str_global, c.LLVMGlobalUnnamedAddr); + + var call_args: [1]c.LLVMValueRef = .{str_global}; + const class_val = c.LLVMBuildCall2(self.e.builder, get_class_ty, get_class_fn, &call_args, 1, "cls"); + _ = c.LLVMBuildStore(self.e.builder, class_val, slot_global); + } + _ = c.LLVMBuildRetVoid(self.e.builder); + + // Register in @llvm.global_ctors for AOT + inject into main for ORC JIT. + const i32_ty = self.e.cached_i32; + const ptr_ty = self.e.cached_ptr; + var ctor_field_types: [3]c.LLVMTypeRef = .{ i32_ty, ptr_ty, ptr_ty }; + const ctor_struct_ty = c.LLVMStructTypeInContext(self.e.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); + + // Append-vs-replace the existing global_ctors. Selector init may + // have created `@llvm.global_ctors` already — extend its array + // rather than overwriting. + const existing_z = "llvm.global_ctors"; + const existing = c.LLVMGetNamedGlobal(self.e.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.e.alloc); + var i: c_uint = 0; + while (i < old_count) : (i += 1) { + new_entries.append(self.e.alloc, c.LLVMGetAggregateElement(existing_init, i)) catch unreachable; + } + new_entries.append(self.e.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.e.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.e.llvm_module, ctors_arr_ty, "llvm.global_ctors"); + c.LLVMSetInitializer(ctors_global, ctors_init); + c.LLVMSetLinkage(ctors_global, c.LLVMAppendingLinkage); + } + + // ORC JIT injection: same trick as emitObjcSelectorInit. Inject a + // direct call from main's entry so the JIT path populates the + // slots too. Must run AFTER the selector init's main injection + // (selectors are needed independently of class objects), so we + // place this call AFTER the first instruction (which is the + // selector-init call, if present) rather than at the very top. + const main_z = "main"; + const main_fn = c.LLVMGetNamedFunction(self.e.llvm_module, main_z); + if (main_fn != null) { + const entry_bb = c.LLVMGetEntryBasicBlock(main_fn); + // Walk past any existing init calls (selector init etc.) so + // class init runs after them. The order within main's prelude + // doesn't matter functionally (the two caches are independent), + // but stable ordering keeps IR snapshots deterministic. + 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.e.builder, insert_before); + } else { + c.LLVMPositionBuilderAtEnd(self.e.builder, entry_bb); + } + var no_args: [0]c.LLVMValueRef = .{}; + _ = c.LLVMBuildCall2(self.e.builder, ctor_ty, ctor, &no_args, 0, ""); + } + } + + /// 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) + /// class_addIvar(cls, "__sx_state", 8, 3, "^v") // M1.2 A.4b.i + /// objc_registerClassPair(cls) + /// g__state_ivar = class_getInstanceVariable(cls, "__sx_state") + /// + /// Method IMPs (`class_addMethod`) and the `+alloc` / `-dealloc` + /// overrides come in A.4b.ii / A.5 / A.6. + pub fn emitObjcDefinedClassInit(self: FfiCtors) void { + if (self.e.ir_mod.objc_defined_class_cache.items.len == 0) return; + + const ptr_ty = self.e.cached_ptr; + const i32_ty = self.e.cached_i32; + const i64_ty = self.e.cached_i64; + const i8_ty = c.LLVMInt8TypeInContext(self.e.context); + + // Lazy-declare the Obj-C runtime APIs the constructor calls. + // objc_getClass(name: *u8) -> *void. + const get_class_fn, const get_class_ty = self.e.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.e.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.e.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.e.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.e.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.e.lazyDeclareCRuntime("objc_registerClassPair", &[_]c.LLVMTypeRef{ptr_ty}, self.e.cached_void, 0); + // class_getInstanceVariable(cls: *void, name: *u8) -> *Ivar. + const get_iv_fn, const get_iv_ty = self.e.lazyDeclareCRuntime("class_getInstanceVariable", &[_]c.LLVMTypeRef{ ptr_ty, ptr_ty }, ptr_ty, 0); + + // Constructor: void __sx_objc_defined_class_init(). + var no_params: [0]c.LLVMTypeRef = .{}; + const ctor_ty = c.LLVMFunctionType(self.e.cached_void, &no_params, 0, 0); + const ctor = c.LLVMAddFunction(self.e.llvm_module, "__sx_objc_defined_class_init", ctor_ty); + c.LLVMSetLinkage(ctor, c.LLVMInternalLinkage); + const entry = c.LLVMAppendBasicBlockInContext(self.e.context, ctor, "entry"); + c.LLVMPositionBuilderAtEnd(self.e.builder, entry); + + // Reusable C-string globals for ivar metadata (same across classes). + const sx_state_name_global = self.e.emitPrivateCString("__sx_state", "OBJC_IVAR_NAME_"); + const sx_state_enc_global = self.e.emitPrivateCString("^v", "OBJC_IVAR_TYPE_"); + + for (self.e.ir_mod.objc_defined_class_cache.items) |entry_kv| { + const fcd = entry_kv.decl; + const class_name = fcd.name; + + // Parent class — pre-resolved Obj-C runtime name from + // lower.zig (M2.3 resolveObjcParentName). Stored on the + // cache entry so emit_llvm doesn't re-walk + // foreign_class_map here. + const parent_name = entry_kv.parent_objc_name; + + const parent_str_global = self.e.emitPrivateCString(parent_name, "OBJC_CLASS_NAME_"); + const class_str_global = self.e.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.e.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.e.builder, alloc_pair_ty, alloc_pair_fn, &alloc_args, 3, "cls"); + + // class_addIvar(cls, "__sx_state", 8, 3, "^v") + // size = 8 (pointer) — sizeof(*void) on 64-bit + // log2align = 3 — alignof(*void) = 8 = 2^3 + // type = "^v" (encoded *void) + var ivar_args: [5]c.LLVMValueRef = .{ + cls_val, + sx_state_name_global, + c.LLVMConstInt(i64_ty, 8, 0), + c.LLVMConstInt(i8_ty, 3, 0), + sx_state_enc_global, + }; + _ = c.LLVMBuildCall2(self.e.builder, add_ivar_ty, add_ivar_fn, &ivar_args, 5, ""); + + // Class-method registration (M2.1(b)) and the +alloc IMP + // (M1.2 A.5) both target the metaclass. Compute it once + // up-front so all metaclass-bound class_addMethod calls + // can reference the same LLVM value. + // + // metaclass = object_getClass(cls). (object_getClass on a + // Class returns the metaclass — a Class IS an instance of + // its metaclass. Distinct from objc_getClass(name).) + const obj_get_class_fn, const obj_get_class_ty = self.e.lazyDeclareCRuntime("object_getClass", &[_]c.LLVMTypeRef{ptr_ty}, ptr_ty, 0); + var ogc_args: [1]c.LLVMValueRef = .{cls_val}; + const metaclass_val = c.LLVMBuildCall2(self.e.builder, obj_get_class_ty, obj_get_class_fn, &ogc_args, 1, "metacls"); + + // class_addMethod(target, sel_registerName(sel), imp, encoding) + // — register each method's IMP trampoline (M1.2 A.4b.iii + // + M2.1(b)). Instance methods register on `cls`; class + // methods (`is_class`) on the metaclass. 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.e.emitPrivateCString(method.sel, "OBJC_METH_VAR_NAME_"); + const enc_str_global = self.e.emitPrivateCString(method.encoding, "OBJC_METH_VAR_TYPE_"); + + var sel_args: [1]c.LLVMValueRef = .{sel_str_global}; + const sel_val = c.LLVMBuildCall2(self.e.builder, sel_reg_ty, sel_reg_fn, &sel_args, 1, "sel"); + + const imp_z = self.e.alloc.dupeZ(u8, method.imp_name) catch continue; + defer self.e.alloc.free(imp_z); + const imp_fn = c.LLVMGetNamedFunction(self.e.llvm_module, imp_z.ptr); + if (imp_fn == null) continue; + + const target_cls = if (method.is_class) metaclass_val else cls_val; + var add_args: [4]c.LLVMValueRef = .{ target_cls, sel_val, imp_fn, enc_str_global }; + _ = c.LLVMBuildCall2(self.e.builder, add_method_ty, add_method_fn, &add_args, 4, ""); + } + + // M2.3 / M3.2 — register `#implements` protocol conformances + // BEFORE objc_registerClassPair. iOS checks + // `class_conformsToProtocol` when instantiating scene + // delegates and other protocol-typed callbacks; without + // these the runtime silently rejects the class. + // + // The protocol may not be present on every SDK / runtime + // (dead-strip pruning, version skew), so `objc_getProtocol` + // returning null is non-fatal — skip the addProtocol call. + const get_proto_fn, const get_proto_ty = self.e.lazyDeclareCRuntime("objc_getProtocol", &[_]c.LLVMTypeRef{ptr_ty}, ptr_ty, 0); + const add_proto_fn, const add_proto_ty = self.e.lazyDeclareCRuntime("class_addProtocol", &[_]c.LLVMTypeRef{ ptr_ty, ptr_ty }, i8_ty, 0); + for (fcd.members) |m| switch (m) { + .implements => |proto_alias| { + const proto_str_global = self.e.emitPrivateCString(proto_alias, "OBJC_PROTOCOL_NAME_"); + var gp_args: [1]c.LLVMValueRef = .{proto_str_global}; + const proto_val = c.LLVMBuildCall2(self.e.builder, get_proto_ty, get_proto_fn, &gp_args, 1, "proto"); + var ap_args: [2]c.LLVMValueRef = .{ cls_val, proto_val }; + _ = c.LLVMBuildCall2(self.e.builder, add_proto_ty, add_proto_fn, &ap_args, 2, ""); + }, + else => {}, + }; + + // objc_registerClassPair(cls) + var reg_args: [1]c.LLVMValueRef = .{cls_val}; + _ = c.LLVMBuildCall2(self.e.builder, register_ty, register_fn, ®_args, 1, ""); + + // Cache the class pointer in `___class` global so the + // synthesized -dealloc trampoline (M1.2 A.6) can use it for + // [super dealloc] dispatch via objc_msgSendSuper2. + const class_global_name = std.fmt.allocPrint(self.e.alloc, "__{s}_class", .{class_name}) catch continue; + defer self.e.alloc.free(class_global_name); + const class_global_z = self.e.alloc.dupeZ(u8, class_global_name) catch continue; + defer self.e.alloc.free(class_global_z); + const class_global = c.LLVMGetNamedGlobal(self.e.llvm_module, class_global_z.ptr); + if (class_global != null) { + _ = c.LLVMBuildStore(self.e.builder, cls_val, class_global); + } + + // M1.2 A.6 — register the synthesized `-dealloc` IMP on the + // class itself (instance method). The runtime fires it at + // refcount-zero; the IMP frees __sx_state and chains to + // [super dealloc]. + const dealloc_imp_name = std.fmt.allocPrint(self.e.alloc, "__{s}_dealloc_imp", .{class_name}) catch continue; + defer self.e.alloc.free(dealloc_imp_name); + const dealloc_imp_z = self.e.alloc.dupeZ(u8, dealloc_imp_name) catch continue; + defer self.e.alloc.free(dealloc_imp_z); + const dealloc_imp_fn = c.LLVMGetNamedFunction(self.e.llvm_module, dealloc_imp_z.ptr); + if (dealloc_imp_fn != null) { + const dealloc_sel_global = self.e.emitPrivateCString("dealloc", "OBJC_METH_VAR_NAME_"); + const dealloc_enc_global = self.e.emitPrivateCString("v@:", "OBJC_METH_VAR_TYPE_"); + + var sel_args: [1]c.LLVMValueRef = .{dealloc_sel_global}; + const sel_val = c.LLVMBuildCall2(self.e.builder, sel_reg_ty, sel_reg_fn, &sel_args, 1, "sel_dealloc"); + + var add_args: [4]c.LLVMValueRef = .{ cls_val, sel_val, dealloc_imp_fn, dealloc_enc_global }; + _ = c.LLVMBuildCall2(self.e.builder, add_method_ty, add_method_fn, &add_args, 4, ""); + } + + // M1.2 A.5 — register the synthesized `+alloc` IMP on the + // metaclass. Class methods live on the metaclass (every + // Class object's `isa` points to the metaclass), so we + // resolve it via `object_getClass(cls)` and `class_addMethod` + // the IMP there. Encoding `@@:` = returns id, takes Class, + // then SEL — Apple's standard `+alloc` shape. This override + // wins over NSObject's default +alloc; runtime instantiations + // (UIKit, Info.plist, NSCoder) go through our IMP and get the + // __sx_state ivar bound. + const alloc_imp_name = std.fmt.allocPrint(self.e.alloc, "__{s}_alloc_imp", .{class_name}) catch continue; + defer self.e.alloc.free(alloc_imp_name); + const alloc_imp_z = self.e.alloc.dupeZ(u8, alloc_imp_name) catch continue; + defer self.e.alloc.free(alloc_imp_z); + const alloc_imp_fn = c.LLVMGetNamedFunction(self.e.llvm_module, alloc_imp_z.ptr); + if (alloc_imp_fn != null) { + // metaclass_val was computed up-front above (shared + // with class-method registration). +alloc is a class + // method registered on the metaclass. + const alloc_sel_global = self.e.emitPrivateCString("alloc", "OBJC_METH_VAR_NAME_"); + const alloc_enc_global = self.e.emitPrivateCString("@@:", "OBJC_METH_VAR_TYPE_"); + + var sel_args: [1]c.LLVMValueRef = .{alloc_sel_global}; + const sel_val = c.LLVMBuildCall2(self.e.builder, sel_reg_ty, sel_reg_fn, &sel_args, 1, "sel_alloc"); + + var add_args: [4]c.LLVMValueRef = .{ metaclass_val, sel_val, alloc_imp_fn, alloc_enc_global }; + _ = c.LLVMBuildCall2(self.e.builder, add_method_ty, add_method_fn, &add_args, 4, ""); + } + + // Cache the ivar handle in the per-class global so trampolines + // can read the __sx_state ivar without re-looking-it-up. The + // global is declared by lower.zig (M1.2 A.4b.i) and starts as + // null; the constructor fills it in here. + const ivar_global_name = std.fmt.allocPrint(self.e.alloc, "__{s}_state_ivar", .{class_name}) catch continue; + defer self.e.alloc.free(ivar_global_name); + const ivar_global_z = self.e.alloc.dupeZ(u8, ivar_global_name) catch continue; + defer self.e.alloc.free(ivar_global_z); + const ivar_global = c.LLVMGetNamedGlobal(self.e.llvm_module, ivar_global_z.ptr); + if (ivar_global != null) { + var iv_args: [2]c.LLVMValueRef = .{ cls_val, sx_state_name_global }; + const iv_val = c.LLVMBuildCall2(self.e.builder, get_iv_ty, get_iv_fn, &iv_args, 2, "iv"); + _ = c.LLVMBuildStore(self.e.builder, iv_val, ivar_global); + } + } + _ = c.LLVMBuildRetVoid(self.e.builder); + + // Inject the call into main's entry block ONLY — skip + // @llvm.global_ctors. Apple's frameworks (UIKit on iOS, + // AppKit on macOS) register their Obj-C classes during + // dyld's image-init phase, which overlaps global_ctors. If + // we ran there too, `objc_getClass("UIResponder")` would + // return null and `objc_allocateClassPair(null, ...)` would + // crash inside objc_registerClassPair. main's entry runs + // AFTER dyld's framework init is complete but BEFORE user + // code (UIApplicationMain), so the runtime sees the parent + // class properly. + self.e.injectCtorIntoMain(ctor, ctor_ty); + + _ = i32_ty; + } +}; diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 2b604fb..689324c 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -14,6 +14,7 @@ const llvm_types = @import("../backend/llvm/types.zig"); const llvm_abi = @import("../backend/llvm/abi.zig"); const llvm_debug = @import("../backend/llvm/debug.zig"); const llvm_reflection = @import("../backend/llvm/reflection.zig"); +const llvm_ffi_ctors = @import("../backend/llvm/ffi_ctors.zig"); const ir_inst = @import("inst.zig"); const Ref = ir_inst.Ref; const Span = ir_inst.Span; @@ -348,7 +349,7 @@ pub const LLVMEmitter = struct { } // Pass 2.5: Emit Obj-C selector init constructor (Phase 1.5). - self.emitObjcSelectorInit(); + self.ffiCtors().emitObjcSelectorInit(); // Pass 2.5b: Emit Obj-C class-pair registration constructor for // sx-defined classes (M1.2 A.4+). Runs BEFORE the foreign @@ -357,12 +358,12 @@ pub const LLVMEmitter = struct { // `objc_getClass(\"SxFoo\")` runs to populate the Phase 3.1 // class-object cache — otherwise the cache slot would store // null and `SxFoo.method()` dispatches against null. - self.emitObjcDefinedClassInit(); + self.ffiCtors().emitObjcDefinedClassInit(); // Pass 2.5c: Emit Obj-C class-object init constructor (Phase 3.1). // Same shape as the selector init — populates the per-module // cached `Class*` slots via `objc_getClass` at module-init time. - self.emitObjcClassInit(); + self.ffiCtors().emitObjcClassInit(); // Pass 2.6: On macOS, chdir to the .app bundle's Resources dir at // startup so relative asset paths work when Finder/`open` @@ -400,482 +401,8 @@ pub const LLVMEmitter = struct { return ""; } - /// Synthesize a module constructor that populates each interned - /// Obj-C selector slot via `sel_registerName`, once at module - /// load. Registered in `@llvm.global_ctors` so dyld / ld.so / the - /// LLVM ORC JIT all run it before main. Per `#objc_call` site - /// collapses to a single load from the slot. - /// - /// We tried clang's section-based shape (`__DATA,__objc_selrefs` + - /// `externally_initialized` linkage, no constructor — dyld - /// resolves at load time) and it works for fully-linked binaries - /// via the system loader, BUT LLVM's ORC JIT (the engine behind - /// `sx run`) doesn't process Mach-O Obj-C metadata sections — - /// the slot stays at its initial value (the method-name string - /// pointer) and `objc_msgSend` dispatches with a bogus SEL. - /// `@llvm.global_ctors` is a portable choice that works both - /// in-JIT and as a linked binary, at the cost of a tiny - /// startup pass (one sel_registerName + store per unique - /// selector). - fn emitObjcSelectorInit(self: *LLVMEmitter) void { - if (self.ir_mod.objc_selector_cache.items.len == 0) return; - - // Lazy-declare sel_registerName for the constructor body — - // lower.zig only declares it when a non-literal selector - // appears, which the constructor doesn't depend on. - const sel_reg_name = "sel_registerName"; - const sel_reg_z = self.alloc.dupeZ(u8, sel_reg_name) catch unreachable; - defer self.alloc.free(sel_reg_z); - var sel_reg_fn = c.LLVMGetNamedFunction(self.llvm_module, sel_reg_z.ptr); - var sel_reg_ty: c.LLVMTypeRef = undefined; - if (sel_reg_fn == null) { - var params: [1]c.LLVMTypeRef = .{self.cached_ptr}; - sel_reg_ty = c.LLVMFunctionType(self.cached_ptr, ¶ms, 1, 0); - sel_reg_fn = c.LLVMAddFunction(self.llvm_module, sel_reg_z.ptr, sel_reg_ty); - c.LLVMSetLinkage(sel_reg_fn, c.LLVMExternalLinkage); - } else { - sel_reg_ty = c.LLVMGlobalGetValueType(sel_reg_fn); - } - - // Constructor: void __sx_objc_selector_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_selector_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_selector_cache.items) |entry_kv| { - const sel_str = entry_kv.sel; - const slot_gid = entry_kv.slot; - const slot_global = self.global_map.get(@intCast(slot_gid.index())) orelse continue; - - // Method-name C-string — names match clang's convention - // so debuggers / nm / dyld see the same symbols, even - // though the surrounding section tagging isn't load- - // bearing in our JIT path. - const meth_str_z = self.alloc.allocSentinel(u8, sel_str.len, 0) catch continue; - defer self.alloc.free(meth_str_z); - @memcpy(meth_str_z[0..sel_str.len], sel_str); - const str_const = c.LLVMConstStringInContext(self.context, meth_str_z.ptr, @intCast(sel_str.len), 0); - const str_global = c.LLVMAddGlobal(self.llvm_module, c.LLVMTypeOf(str_const), "OBJC_METH_VAR_NAME_"); - c.LLVMSetInitializer(str_global, str_const); - c.LLVMSetLinkage(str_global, c.LLVMPrivateLinkage); - c.LLVMSetGlobalConstant(str_global, 1); - c.LLVMSetUnnamedAddress(str_global, c.LLVMGlobalUnnamedAddr); - - var sel_args: [1]c.LLVMValueRef = .{str_global}; - const sel_val = c.LLVMBuildCall2(self.builder, sel_reg_ty, sel_reg_fn, &sel_args, 1, "sel"); - _ = c.LLVMBuildStore(self.builder, sel_val, slot_global); - } - _ = c.LLVMBuildRetVoid(self.builder); - - // Register the constructor in @llvm.global_ctors. dyld picks - // this up for a fully-linked binary at load time. - 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 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); - - // BUT — LLVM's ORC JIT (the engine for `sx run`) doesn't - // automatically run `@llvm.global_ctors`. Inject a direct - // call from `main`'s entry block as well; idempotent under - // dyld (sel_registerName returns the same SEL on second call). - 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); - const first_inst = c.LLVMGetFirstInstruction(entry_bb); - if (first_inst != null) { - c.LLVMPositionBuilderBefore(self.builder, first_inst); - } else { - c.LLVMPositionBuilderAtEnd(self.builder, entry_bb); - } - var no_args: [0]c.LLVMValueRef = .{}; - _ = c.LLVMBuildCall2(self.builder, ctor_ty, ctor, &no_args, 0, ""); - } - } - - /// Phase 3.1 companion to `emitObjcSelectorInit`. Walks - /// `module.objc_class_cache` and synthesizes a constructor that - /// populates each cached `Class*` slot via `objc_getClass(name)` - /// exactly once at module-init. Registered in `@llvm.global_ctors` - /// AND injected at the top of `main()` for the ORC JIT path. - fn emitObjcClassInit(self: *LLVMEmitter) void { - if (self.ir_mod.objc_class_cache.items.len == 0) return; - - // Lazy-declare objc_getClass(name: *u8) -> *void. - const get_class_name = "objc_getClass"; - const get_class_z = self.alloc.dupeZ(u8, get_class_name) catch unreachable; - defer self.alloc.free(get_class_z); - var get_class_fn = c.LLVMGetNamedFunction(self.llvm_module, get_class_z.ptr); - var get_class_ty: c.LLVMTypeRef = undefined; - if (get_class_fn == null) { - var params: [1]c.LLVMTypeRef = .{self.cached_ptr}; - get_class_ty = c.LLVMFunctionType(self.cached_ptr, ¶ms, 1, 0); - get_class_fn = c.LLVMAddFunction(self.llvm_module, get_class_z.ptr, get_class_ty); - c.LLVMSetLinkage(get_class_fn, c.LLVMExternalLinkage); - } else { - get_class_ty = c.LLVMGlobalGetValueType(get_class_fn); - } - - // Constructor: void __sx_objc_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_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_class_cache.items) |entry_kv| { - const class_name = entry_kv.name; - const slot_gid = entry_kv.slot; - const slot_global = self.global_map.get(@intCast(slot_gid.index())) orelse continue; - - // Class-name C-string. - const name_z = self.alloc.allocSentinel(u8, class_name.len, 0) catch continue; - defer self.alloc.free(name_z); - @memcpy(name_z[0..class_name.len], class_name); - const str_const = c.LLVMConstStringInContext(self.context, name_z.ptr, @intCast(class_name.len), 0); - const str_global = c.LLVMAddGlobal(self.llvm_module, c.LLVMTypeOf(str_const), "OBJC_CLASS_NAME_"); - c.LLVMSetInitializer(str_global, str_const); - c.LLVMSetLinkage(str_global, c.LLVMPrivateLinkage); - c.LLVMSetGlobalConstant(str_global, 1); - c.LLVMSetUnnamedAddress(str_global, c.LLVMGlobalUnnamedAddr); - - var call_args: [1]c.LLVMValueRef = .{str_global}; - const class_val = c.LLVMBuildCall2(self.builder, get_class_ty, get_class_fn, &call_args, 1, "cls"); - _ = c.LLVMBuildStore(self.builder, class_val, slot_global); - } - _ = c.LLVMBuildRetVoid(self.builder); - - // Register in @llvm.global_ctors for AOT + inject into main for ORC JIT. - 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); - - // Append-vs-replace the existing global_ctors. Selector init may - // have created `@llvm.global_ctors` already — extend its array - // rather than overwriting. - 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 injection: same trick as emitObjcSelectorInit. Inject a - // direct call from main's entry so the JIT path populates the - // slots too. Must run AFTER the selector init's main injection - // (selectors are needed independently of class objects), so we - // place this call AFTER the first instruction (which is the - // selector-init call, if present) rather than at the very top. - 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); - // Walk past any existing init calls (selector init etc.) so - // class init runs after them. The order within main's prelude - // doesn't matter functionally (the two caches are independent), - // but stable ordering keeps IR snapshots deterministic. - 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, ""); - } - } - - /// 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) - /// class_addIvar(cls, "__sx_state", 8, 3, "^v") // M1.2 A.4b.i - /// objc_registerClassPair(cls) - /// g__state_ivar = class_getInstanceVariable(cls, "__sx_state") - /// - /// Method IMPs (`class_addMethod`) and the `+alloc` / `-dealloc` - /// overrides come in A.4b.ii / A.5 / A.6. - 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; - const i8_ty = c.LLVMInt8TypeInContext(self.context); - - // 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); - // 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. - const get_iv_fn, const get_iv_ty = self.lazyDeclareCRuntime("class_getInstanceVariable", &[_]c.LLVMTypeRef{ ptr_ty, ptr_ty }, ptr_ty, 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); - - // Reusable C-string globals for ivar metadata (same across classes). - const sx_state_name_global = self.emitPrivateCString("__sx_state", "OBJC_IVAR_NAME_"); - const sx_state_enc_global = self.emitPrivateCString("^v", "OBJC_IVAR_TYPE_"); - - for (self.ir_mod.objc_defined_class_cache.items) |entry_kv| { - const fcd = entry_kv.decl; - const class_name = fcd.name; - - // Parent class — pre-resolved Obj-C runtime name from - // lower.zig (M2.3 resolveObjcParentName). Stored on the - // cache entry so emit_llvm doesn't re-walk - // foreign_class_map here. - const parent_name = entry_kv.parent_objc_name; - - 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"); - - // class_addIvar(cls, "__sx_state", 8, 3, "^v") - // size = 8 (pointer) — sizeof(*void) on 64-bit - // log2align = 3 — alignof(*void) = 8 = 2^3 - // type = "^v" (encoded *void) - var ivar_args: [5]c.LLVMValueRef = .{ - cls_val, - sx_state_name_global, - c.LLVMConstInt(i64_ty, 8, 0), - c.LLVMConstInt(i8_ty, 3, 0), - sx_state_enc_global, - }; - _ = c.LLVMBuildCall2(self.builder, add_ivar_ty, add_ivar_fn, &ivar_args, 5, ""); - - // Class-method registration (M2.1(b)) and the +alloc IMP - // (M1.2 A.5) both target the metaclass. Compute it once - // up-front so all metaclass-bound class_addMethod calls - // can reference the same LLVM value. - // - // metaclass = object_getClass(cls). (object_getClass on a - // Class returns the metaclass — a Class IS an instance of - // its metaclass. Distinct from objc_getClass(name).) - const obj_get_class_fn, const obj_get_class_ty = self.lazyDeclareCRuntime("object_getClass", &[_]c.LLVMTypeRef{ptr_ty}, ptr_ty, 0); - var ogc_args: [1]c.LLVMValueRef = .{cls_val}; - const metaclass_val = c.LLVMBuildCall2(self.builder, obj_get_class_ty, obj_get_class_fn, &ogc_args, 1, "metacls"); - - // class_addMethod(target, sel_registerName(sel), imp, encoding) - // — register each method's IMP trampoline (M1.2 A.4b.iii - // + M2.1(b)). Instance methods register on `cls`; class - // methods (`is_class`) on the metaclass. 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_"); - - 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"); - - 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; - - const target_cls = if (method.is_class) metaclass_val else cls_val; - var add_args: [4]c.LLVMValueRef = .{ target_cls, sel_val, imp_fn, enc_str_global }; - _ = c.LLVMBuildCall2(self.builder, add_method_ty, add_method_fn, &add_args, 4, ""); - } - - // M2.3 / M3.2 — register `#implements` protocol conformances - // BEFORE objc_registerClassPair. iOS checks - // `class_conformsToProtocol` when instantiating scene - // delegates and other protocol-typed callbacks; without - // these the runtime silently rejects the class. - // - // The protocol may not be present on every SDK / runtime - // (dead-strip pruning, version skew), so `objc_getProtocol` - // returning null is non-fatal — skip the addProtocol call. - const get_proto_fn, const get_proto_ty = self.lazyDeclareCRuntime("objc_getProtocol", &[_]c.LLVMTypeRef{ptr_ty}, ptr_ty, 0); - const add_proto_fn, const add_proto_ty = self.lazyDeclareCRuntime("class_addProtocol", &[_]c.LLVMTypeRef{ ptr_ty, ptr_ty }, i8_ty, 0); - for (fcd.members) |m| switch (m) { - .implements => |proto_alias| { - const proto_str_global = self.emitPrivateCString(proto_alias, "OBJC_PROTOCOL_NAME_"); - var gp_args: [1]c.LLVMValueRef = .{proto_str_global}; - const proto_val = c.LLVMBuildCall2(self.builder, get_proto_ty, get_proto_fn, &gp_args, 1, "proto"); - var ap_args: [2]c.LLVMValueRef = .{ cls_val, proto_val }; - _ = c.LLVMBuildCall2(self.builder, add_proto_ty, add_proto_fn, &ap_args, 2, ""); - }, - else => {}, - }; - - // objc_registerClassPair(cls) - var reg_args: [1]c.LLVMValueRef = .{cls_val}; - _ = c.LLVMBuildCall2(self.builder, register_ty, register_fn, ®_args, 1, ""); - - // Cache the class pointer in `___class` global so the - // synthesized -dealloc trampoline (M1.2 A.6) can use it for - // [super dealloc] dispatch via objc_msgSendSuper2. - const class_global_name = std.fmt.allocPrint(self.alloc, "__{s}_class", .{class_name}) catch continue; - defer self.alloc.free(class_global_name); - const class_global_z = self.alloc.dupeZ(u8, class_global_name) catch continue; - defer self.alloc.free(class_global_z); - const class_global = c.LLVMGetNamedGlobal(self.llvm_module, class_global_z.ptr); - if (class_global != null) { - _ = c.LLVMBuildStore(self.builder, cls_val, class_global); - } - - // M1.2 A.6 — register the synthesized `-dealloc` IMP on the - // class itself (instance method). The runtime fires it at - // refcount-zero; the IMP frees __sx_state and chains to - // [super dealloc]. - const dealloc_imp_name = std.fmt.allocPrint(self.alloc, "__{s}_dealloc_imp", .{class_name}) catch continue; - defer self.alloc.free(dealloc_imp_name); - const dealloc_imp_z = self.alloc.dupeZ(u8, dealloc_imp_name) catch continue; - defer self.alloc.free(dealloc_imp_z); - const dealloc_imp_fn = c.LLVMGetNamedFunction(self.llvm_module, dealloc_imp_z.ptr); - if (dealloc_imp_fn != null) { - const dealloc_sel_global = self.emitPrivateCString("dealloc", "OBJC_METH_VAR_NAME_"); - const dealloc_enc_global = self.emitPrivateCString("v@:", "OBJC_METH_VAR_TYPE_"); - - var sel_args: [1]c.LLVMValueRef = .{dealloc_sel_global}; - const sel_val = c.LLVMBuildCall2(self.builder, sel_reg_ty, sel_reg_fn, &sel_args, 1, "sel_dealloc"); - - var add_args: [4]c.LLVMValueRef = .{ cls_val, sel_val, dealloc_imp_fn, dealloc_enc_global }; - _ = c.LLVMBuildCall2(self.builder, add_method_ty, add_method_fn, &add_args, 4, ""); - } - - // M1.2 A.5 — register the synthesized `+alloc` IMP on the - // metaclass. Class methods live on the metaclass (every - // Class object's `isa` points to the metaclass), so we - // resolve it via `object_getClass(cls)` and `class_addMethod` - // the IMP there. Encoding `@@:` = returns id, takes Class, - // then SEL — Apple's standard `+alloc` shape. This override - // wins over NSObject's default +alloc; runtime instantiations - // (UIKit, Info.plist, NSCoder) go through our IMP and get the - // __sx_state ivar bound. - const alloc_imp_name = std.fmt.allocPrint(self.alloc, "__{s}_alloc_imp", .{class_name}) catch continue; - defer self.alloc.free(alloc_imp_name); - const alloc_imp_z = self.alloc.dupeZ(u8, alloc_imp_name) catch continue; - defer self.alloc.free(alloc_imp_z); - const alloc_imp_fn = c.LLVMGetNamedFunction(self.llvm_module, alloc_imp_z.ptr); - if (alloc_imp_fn != null) { - // metaclass_val was computed up-front above (shared - // with class-method registration). +alloc is a class - // method registered on the metaclass. - const alloc_sel_global = self.emitPrivateCString("alloc", "OBJC_METH_VAR_NAME_"); - const alloc_enc_global = self.emitPrivateCString("@@:", "OBJC_METH_VAR_TYPE_"); - - var sel_args: [1]c.LLVMValueRef = .{alloc_sel_global}; - const sel_val = c.LLVMBuildCall2(self.builder, sel_reg_ty, sel_reg_fn, &sel_args, 1, "sel_alloc"); - - var add_args: [4]c.LLVMValueRef = .{ metaclass_val, sel_val, alloc_imp_fn, alloc_enc_global }; - _ = c.LLVMBuildCall2(self.builder, add_method_ty, add_method_fn, &add_args, 4, ""); - } - - // Cache the ivar handle in the per-class global so trampolines - // can read the __sx_state ivar without re-looking-it-up. The - // global is declared by lower.zig (M1.2 A.4b.i) and starts as - // null; the constructor fills it in here. - const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{class_name}) catch continue; - defer self.alloc.free(ivar_global_name); - const ivar_global_z = self.alloc.dupeZ(u8, ivar_global_name) catch continue; - defer self.alloc.free(ivar_global_z); - const ivar_global = c.LLVMGetNamedGlobal(self.llvm_module, ivar_global_z.ptr); - if (ivar_global != null) { - var iv_args: [2]c.LLVMValueRef = .{ cls_val, sx_state_name_global }; - const iv_val = c.LLVMBuildCall2(self.builder, get_iv_ty, get_iv_fn, &iv_args, 2, "iv"); - _ = c.LLVMBuildStore(self.builder, iv_val, ivar_global); - } - } - _ = c.LLVMBuildRetVoid(self.builder); - - // Inject the call into main's entry block ONLY — skip - // @llvm.global_ctors. Apple's frameworks (UIKit on iOS, - // AppKit on macOS) register their Obj-C classes during - // dyld's image-init phase, which overlaps global_ctors. If - // we ran there too, `objc_getClass(\"UIResponder\")` would - // return null and `objc_allocateClassPair(null, ...)` would - // crash inside objc_registerClassPair. main's entry runs - // AFTER dyld's framework init is complete but BEFORE user - // code (UIApplicationMain), so the runtime sees the parent - // class properly. - self.injectCtorIntoMain(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 } { + pub 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); @@ -892,7 +419,7 @@ pub const LLVMEmitter = struct { /// 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 { + pub 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); @@ -917,7 +444,7 @@ pub const LLVMEmitter = struct { /// load — global_ctors is too early because Apple frameworks /// (UIKit etc.) register their Obj-C classes during their own /// init phase that overlaps ours. - fn injectCtorIntoMain(self: *LLVMEmitter, ctor: c.LLVMValueRef, ctor_ty: c.LLVMTypeRef) void { + pub fn injectCtorIntoMain(self: *LLVMEmitter, ctor: c.LLVMValueRef, ctor_ty: c.LLVMTypeRef) void { const main_z = "main"; const main_fn = c.LLVMGetNamedFunction(self.llvm_module, main_z); if (main_fn == null) return; @@ -4433,6 +3960,10 @@ pub const LLVMEmitter = struct { return .{ .e = self }; } + fn ffiCtors(self: *LLVMEmitter) llvm_ffi_ctors.FfiCtors { + return .{ .e = self }; + } + /// IR-type → LLVM-type lowering lives in `backend/llvm/types.zig` /// (`TypeLowering`). This stays the facade entry point (~97 callers). pub fn toLLVMType(self: *LLVMEmitter, ty: TypeId) c.LLVMTypeRef {