Files
sx/src/backend/llvm/ffi_ctors.zig
agra 2f7c99fd11 refactor(backend): extract JNI slot cache into ffi_ctors.zig (A7.3 slice 2a)
Move getOrCreateJniSlots (the cls/methodid slot-cache builder) out of
emit_llvm.zig into the FfiCtors backend *LLVMEmitter facade. Behavior-preserving
— self.* -> self.e.* only.

- FfiCtors gains getOrCreateJniSlots (pub). The jni_slots cache + mangleJniKey
  stay on LLVMEmitter; mangleJniKey is widened to pub (the facade calls it back,
  like lazyDeclareCRuntime/emitPrivateCString), and JniSlotPair is widened to pub
  (the facade returns it; the call site consumes it). 1 call site routed via
  ffiCtors().
- emitJniConstructor intentionally NOT moved in this slice: it is emission-heavy
  (resolveRef/mapRef/coerceArg/getRefIRType/extractSlicePtr/loadJniFn/
  emitCStringGlobal — 100+ internal callers for the first two), so relocating it
  would pub-expose the emitter's core value-emission machinery. Consistent with
  A7.2 keeping emitFieldValueGet in emit_llvm.zig. Pending an explicit decision.

Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(JNI anchors 1402/1408/1418/1425 green, no churn).
2026-06-03 10:42:58 +03:00

510 lines
30 KiB
Zig

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;
const JniSlotPair = LLVMEmitter.JniSlotPair;
/// 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;
}
/// Return `{cls_slot, mid_slot}` global pair for the
/// `(name, sig)` literal — created on first lookup, shared across
/// later `#jni_call` sites with the same literal pair. Both
/// slots are zero-initialized `ptr`; the call-site lowering does
/// lazy population on first dispatch. The cache (`jni_slots`) +
/// `mangleJniKey` stay on `LLVMEmitter`.
pub fn getOrCreateJniSlots(self: FfiCtors, name: []const u8, sig: []const u8) JniSlotPair {
// Compose the key from name + a separator + sig. The separator
// is a byte that can't appear in a JNI method name or signature
// (NUL), so the same key never collides across distinct pairs.
const key = std.fmt.allocPrint(self.e.alloc, "{s}\x00{s}", .{ name, sig }) catch unreachable;
if (self.e.jni_slots.get(key)) |existing| {
self.e.alloc.free(key);
return existing;
}
const mangled = self.e.mangleJniKey(name, sig);
defer self.e.alloc.free(mangled);
const cls_name = std.fmt.allocPrintSentinel(self.e.alloc, "SX_JNI_CLS_{s}", .{mangled}, 0) catch unreachable;
defer self.e.alloc.free(cls_name);
const mid_name = std.fmt.allocPrintSentinel(self.e.alloc, "SX_JNI_MID_{s}", .{mangled}, 0) catch unreachable;
defer self.e.alloc.free(mid_name);
const cls_slot = c.LLVMAddGlobal(self.e.llvm_module, self.e.cached_ptr, cls_name.ptr);
c.LLVMSetLinkage(cls_slot, c.LLVMInternalLinkage);
c.LLVMSetInitializer(cls_slot, c.LLVMConstNull(self.e.cached_ptr));
const mid_slot = c.LLVMAddGlobal(self.e.llvm_module, self.e.cached_ptr, mid_name.ptr);
c.LLVMSetLinkage(mid_slot, c.LLVMInternalLinkage);
c.LLVMSetInitializer(mid_slot, c.LLVMConstNull(self.e.cached_ptr));
const pair = JniSlotPair{ .cls_slot = cls_slot, .mid_slot = mid_slot };
self.e.jni_slots.put(key, pair) catch unreachable;
return pair;
}
};