refactor(backend): extract Obj-C runtime constructors into src/backend/llvm/ffi_ctors.zig (A7.3 slice 1)

Move the Obj-C module-init constructor emission out of emit_llvm.zig into a
FfiCtors backend *LLVMEmitter facade (field `e`). Behavior-preserving relocation
— self.* -> self.e.* only.

- src/backend/llvm/ffi_ctors.zig (FfiCtors): emitObjcSelectorInit (cached SEL
  init), emitObjcClassInit (objc_getClass class-object cache), and
  emitObjcDefinedClassInit (class-pair registration: ivars, method IMP table,
  +alloc/-dealloc IMPs, #implements protocol conformances). Emit-time caches
  (ir_mod.objc_*_cache) + global_map + cached LLVM handles read via self.e.*.
- 3 call sites in LLVMEmitter.emit routed via a new ffiCtors() accessor.
- Shared infra stays in emit_llvm.zig, widened to pub (the facade calls back):
  lazyDeclareCRuntime (11 callers), emitPrivateCString (11 callers),
  injectCtorIntoMain (the moved defined-class ctor's callee). No @llvm.global_ctors
  shape / IMP-table / ivar / protocol-conformance change.

Pins: 1309 (class-method lowering), 1319 (property getter/setter IMPs), 1314
(alloc/dealloc IMPs), 1332 (sret + addMethod) all green.

Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0 (no churn).
This commit is contained in:
agra
2026-06-03 10:35:15 +03:00
parent 836583d7f7
commit e8c33bfc00
2 changed files with 487 additions and 480 deletions

View File

@@ -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, &params, 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, &params, 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("<ParentName>") // default NSObject
/// cls = objc_allocateClassPair(super_cls, "<ClassName>", 0)
/// class_addIvar(cls, "__sx_state", 8, 3, "^v") // M1.2 A.4b.i
/// objc_registerClassPair(cls)
/// g_<ClassName>_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, &reg_args, 1, "");
// Cache the class pointer in `__<Cls>_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;
}
};

View File

@@ -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, &params, 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, &params, 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("<ParentName>") // default NSObject
/// cls = objc_allocateClassPair(super_cls, "<ClassName>", 0)
/// class_addIvar(cls, "__sx_state", 8, 3, "^v") // M1.2 A.4b.i
/// objc_registerClassPair(cls)
/// g_<ClassName>_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, &reg_args, 1, "");
// Cache the class pointer in `__<Cls>_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 {