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