diff --git a/examples/144-objc-class-ivar-registration.sx b/examples/144-objc-class-ivar-registration.sx new file mode 100644 index 0000000..d8c0835 --- /dev/null +++ b/examples/144-objc-class-ivar-registration.sx @@ -0,0 +1,51 @@ +// M1.2 A.4b.i — '__sx_state' ivar registration on sx-defined +// '#objc_class'. +// +// Class-pair init now: +// 1. allocs the class pair +// 2. registers a single '__sx_state : *void' ivar +// 3. finalises the class +// 4. stores the runtime Ivar handle in a per-class global +// ('___state_ivar') so IMP trampolines can later +// 'object_getIvar' the state struct pointer. +// +// Round-trip below: after main starts, look up SxFoo, then ask +// the runtime if SxFoo has an Ivar named '__sx_state'. Returns +// non-null iff registration succeeded. +// +// IMP trampolines (A.4b.ii) and the '+alloc' / '-dealloc' +// overrides (A.5 / A.6) come next. + +#import "modules/std.sx"; +#import "modules/compiler.sx"; +#import "modules/std/objc.sx"; + +class_getInstanceVariable :: (cls: *void, name: [*]u8) -> *void #foreign objc; + +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; + } + iv := class_getInstanceVariable(cls, "__sx_state".ptr); + if iv == null { + print("FAIL: __sx_state ivar missing\n"); + return 1; + } + print("ivar: __sx_state\n"); + } + inline if OS != .macos { + print("ivar: __sx_state\n"); + } + 0; +} diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 50a00d3..18d1025 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -542,26 +542,31 @@ pub const LLVMEmitter = struct { /// For each entry in `objc_defined_class_cache`: /// super_cls = objc_getClass("") // default NSObject /// cls = objc_allocateClassPair(super_cls, "", 0) + /// class_addIvar(cls, "__sx_state", 8, 3, "^v") // M1.2 A.4b.i /// objc_registerClassPair(cls) + /// g__state_ivar = class_getInstanceVariable(cls, "__sx_state") /// - /// Method IMPs, 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. + /// 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); // 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 = .{}; @@ -571,6 +576,10 @@ pub const LLVMEmitter = struct { 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; @@ -595,9 +604,37 @@ pub const LLVMEmitter = struct { 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, ""); + // objc_registerClassPair(cls) var reg_args: [1]c.LLVMValueRef = .{cls_val}; _ = c.LLVMBuildCall2(self.builder, register_ty, register_fn, ®_args, 1, ""); + + // 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); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 96aca5b..4c8c10b 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -9617,11 +9617,31 @@ pub const Lowering = struct { if (!fcd.is_foreign and fcd.runtime == .objc_class) { if (self.module.lookupObjcDefinedClass(fcd.name) == null) { self.module.appendObjcDefinedClass(fcd.name, fcd); + // M1.2 A.4b.i: per-class ivar handle global. The class-pair + // init constructor (emit_llvm) populates it via + // class_getInstanceVariable after the class is registered; + // IMP trampolines read it to find the __sx_state ivar. + self.declareObjcDefinedStateIvarGlobal(fcd.name); } self.registerObjcDefinedClassMethods(fcd); } } + /// Declare a per-class global `___state_ivar : *void = null`. + /// emit_llvm's `emitObjcDefinedClassInit` constructor fills it in via + /// `class_getInstanceVariable(cls, "__sx_state")` once per module load. + fn declareObjcDefinedStateIvarGlobal(self: *Lowering, class_name: []const u8) void { + const gname = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{class_name}) catch return; + const name_id = self.module.types.internString(gname); + _ = self.module.addGlobal(.{ + .name = name_id, + .ty = self.module.types.ptrTo(.void), + .init_val = .null_val, + .is_extern = false, + .is_const = false, + }); + } + /// For each bodied instance method on an sx-defined `#objc_class`, /// synthesize an `FnDecl` from the `ForeignMethodDecl`, register it /// in `fn_ast_map` under `.`, and declare diff --git a/tests/expected/142-objc-class-method-lowering.ir b/tests/expected/142-objc-class-method-lowering.ir index 913fb54..3d9f4ae 100644 --- a/tests/expected/142-objc-class-method-lowering.ir +++ b/tests/expected/142-objc-class-method-lowering.ir @@ -2,6 +2,7 @@ @OS = internal global i64 0 @ARCH = internal global i64 0 @POINTER_SIZE = internal global i64 8 +@__SxFoo_state_ivar = internal global ptr null @__sx_default_context = internal global { { ptr, ptr, ptr }, ptr } { { ptr, ptr, ptr } { ptr null, ptr @__thunk_CAllocator_Allocator_alloc, ptr @__thunk_CAllocator_Allocator_dealloc }, ptr null } @str = private unnamed_addr constant [2 x i8] c"0\00", align 1 @str.1 = private unnamed_addr constant [15 x i8] c"result := \22\22; \00", align 1 @@ -22,6 +23,8 @@ @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.18 = private unnamed_addr constant [10 x i8] c"compiled\0A\00", align 1 +@OBJC_IVAR_NAME_ = private unnamed_addr constant [11 x i8] c"__sx_state\00" +@OBJC_IVAR_TYPE_ = private unnamed_addr constant [3 x i8] c"^v\00" @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 }] @@ -787,12 +790,19 @@ declare ptr @objc_getClass(ptr) declare ptr @objc_allocateClassPair(ptr, ptr, i64) +declare i8 @class_addIvar(ptr, ptr, i64, i8, ptr) + declare void @objc_registerClassPair(ptr) +declare ptr @class_getInstanceVariable(ptr, 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) + %0 = call i8 @class_addIvar(ptr %cls, ptr @OBJC_IVAR_NAME_, i64 8, i8 3, ptr @OBJC_IVAR_TYPE_) call void @objc_registerClassPair(ptr %cls) + %iv = call ptr @class_getInstanceVariable(ptr %cls, ptr @OBJC_IVAR_NAME_) + store ptr %iv, ptr @__SxFoo_state_ivar, align 8 ret void } diff --git a/tests/expected/144-objc-class-ivar-registration.exit b/tests/expected/144-objc-class-ivar-registration.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/144-objc-class-ivar-registration.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/144-objc-class-ivar-registration.txt b/tests/expected/144-objc-class-ivar-registration.txt new file mode 100644 index 0000000..2449267 --- /dev/null +++ b/tests/expected/144-objc-class-ivar-registration.txt @@ -0,0 +1 @@ +ivar: __sx_state