ffi 3.1: Cls.static_method(args) lowers to objc_msg_send on the class object

Implementation half of the Phase 3.1 cadence step.
`lowerForeignStaticCall` for `#objc_class` / `#objc_protocol` runtimes
no longer bails; it routes through a new `lowerObjcStaticCall` helper
that loads the class object from a module-scoped cached slot (populated
once per module via `objc_getClass`) and dispatches `objc_msg_send`
with the same selector-mangling as Phase 3.0's instance dispatch.

Three pieces:

1. `Module.objc_class_cache` — parallel to `objc_selector_cache`,
   insertion-ordered list of (class_name, slot_GlobalId) so the
   constructor that calls `objc_getClass` per slot at module load
   is deterministic. `lookupObjcClass` / `appendObjcClass` accessors.
2. `internObjcClassObject` in lower.zig — get-or-create a
   `OBJC_CLASSLIST_REFERENCES_<Cls>` global pointer; matches clang's
   naming convention. `lowerObjcStaticCall` reuses
   `deriveObjcSelector` from 3.0 for the selector, loads the class
   slot, and emits `objc_msg_send(class_obj, sel, args)`.
3. `emitObjcClassInit` in emit_llvm.zig — companion to
   `emitObjcSelectorInit`. Walks `objc_class_cache`, synthesizes a
   constructor `__sx_objc_class_init` that calls `objc_getClass(name)`
   per slot, registers in `@llvm.global_ctors` for AOT (extending the
   existing array if the selector init already created it), and
   injects a direct call into main's prelude after any prior init
   calls so the ORC JIT path runs it too.

Surface form is `.` (`NSObject.class()`) matching JNI's `Alias.new(...)`
convention rather than the plan's notional `::` — avoids extending the
parser for a new postfix operator with no other use case.

Test `examples/ffi-objc-dsl-05-static.sx` exercises NSObject's
`+class` and `+description` class methods via the new syntax, asserts
both return non-null. NSObject is always available at module-load,
unlike runtime-created test classes that wouldn't exist yet when
the class-init constructor runs.

164/164 tests; chess builds + runs clean on all three platforms.
This commit is contained in:
agra
2026-05-25 16:23:24 +03:00
parent b07ee53a39
commit 8406cc1fed
7 changed files with 281 additions and 38 deletions

View File

@@ -275,6 +275,11 @@ pub const LLVMEmitter = struct {
// Pass 2.5: Emit Obj-C selector init constructor (Phase 1.5).
self.emitObjcSelectorInit();
// Pass 2.5b: 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();
// Pass 2.6: On macOS, chdir to the .app bundle's Resources dir at
// startup so relative asset paths work when Finder/`open`
// launches the binary with CWD=/. Non-bundled binaries no-op.
@@ -392,6 +397,133 @@ pub const LLVMEmitter = struct {
}
}
/// 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, "");
}
}
/// On macOS, emit a startup helper that chdir's to the .app bundle's
/// `Contents/Resources` directory when the executable lives inside a
/// `.app/Contents/MacOS/` path. Lets relative asset paths like