ffi 1.6: objc_msg_send IR opcode + per-call-site LLVM fn type
102/102 regression tests pass; chess Android + iOS-sim still build
clean. `ffi-objc-call-04-primitive-returns` flips from xfail to
passing with both nil-recv and real-recv flavors of *void / s64
returns exercised.
Key change: a new `objc_msg_send` IR opcode bundles (recv, sel,
extra args) and carries the return type via the `Inst.ty` field.
emit_llvm.zig builds a per-call-site LLVM function type from the
argument Refs' IR types (recv/sel as ptr; extra args through
abiCoerceParamType) and dispatches with LLVMBuildCall2. One
declared `@objc_msgSend` symbol is reused across every return
type — opaque pointers make the function value type-erased, so
each call site picks its own ABI.
before: one (recv, sel) -> ptr LLVM declaration, hard-coded
per call site; only void return wired in 1.3.
after: same declaration, each call site provides a fresh
LLVMBuildCall2 fn-type → s64 / *void / bool / f64
returns all dispatch correctly without separate FuncIds.
Selector init mechanism: stayed with the @llvm.global_ctors
constructor. Investigated clang's
`__DATA,__objc_selrefs` + `externally_initialized` shape — works
for fully-linked binaries (dyld substitutes the SEL at load
time) but **LLVM ORC JIT** (the engine behind `sx run`) doesn't
process Mach-O Obj-C metadata sections, so the slot keeps its
initial value (the method-name string pointer) and dispatch
crashes with "<null selector>". The portable choice: keep the
constructor AND inject a direct call to it at `main`'s entry —
idempotent under dyld (sel_registerName returns the same SEL on
re-registration), required for ORC JIT.
Files touched:
src/ir/inst.zig | new ObjcMsgSend struct + opcode
src/ir/lower.zig | drop the void-only restriction; emit the
new opcode; remove the orphaned
getObjcMsgSendFid path (objc_msgSend
declaration moved to emit_llvm)
src/ir/emit_llvm.zig | objc_msg_send arm (per-call-site
LLVMBuildCall2); lazy `@objc_msgSend`
declaration via getObjcMsgSendValue;
emitObjcSelectorInit refactored to inject
the ctor call at main's entry
src/ir/{print,interp}.zig | switch arms for the new opcode
`ffi-objc-call-03-selector-sharing.ir` snapshot updates to
reflect the new shape (the `call ... @objc_msgSend` call sites
no longer mention a typed wrapper).
This commit is contained in:
@@ -79,6 +79,11 @@ pub const LLVMEmitter = struct {
|
||||
string_struct_type: ?c.LLVMTypeRef,
|
||||
any_struct_type: ?c.LLVMTypeRef,
|
||||
closure_struct_type: ?c.LLVMTypeRef,
|
||||
// The shared `@objc_msgSend` function value. Lazily declared on
|
||||
// first `objc_msg_send` instruction; all `#objc_call` sites
|
||||
// dispatch through it with their own LLVMBuildCall2 function type
|
||||
// (opaque pointers — the function value is just a `ptr`).
|
||||
objc_msg_send_value: ?c.LLVMValueRef,
|
||||
|
||||
// Cached field name arrays for reflection (TypeId → LLVM global)
|
||||
field_name_arrays: std.AutoHashMap(u32, c.LLVMValueRef),
|
||||
@@ -159,6 +164,7 @@ pub const LLVMEmitter = struct {
|
||||
.string_struct_type = null,
|
||||
.any_struct_type = null,
|
||||
.closure_struct_type = null,
|
||||
.objc_msg_send_value = null,
|
||||
.field_name_arrays = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc),
|
||||
.target_config = target_config,
|
||||
.build_config = .{},
|
||||
@@ -209,48 +215,63 @@ pub const LLVMEmitter = struct {
|
||||
}
|
||||
|
||||
/// 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 runs it
|
||||
/// before main. Per `#objc_call` site collapses to a single load
|
||||
/// from the slot — matches clang's `@selector(...)` lowering.
|
||||
/// 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;
|
||||
|
||||
// Look up the `sel_registerName` extern that the lowerer already
|
||||
// declared. If for some reason it's absent (shouldn't happen —
|
||||
// every interned selector got there via the same lowering path),
|
||||
// bail out and let the per-call fallback run.
|
||||
// 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);
|
||||
const sel_reg_fn = c.LLVMGetNamedFunction(self.llvm_module, sel_reg_z.ptr);
|
||||
if (sel_reg_fn == null) return;
|
||||
const sel_reg_ty = c.LLVMGlobalGetValueType(sel_reg_fn);
|
||||
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, ¶ms, 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);
|
||||
}
|
||||
|
||||
// Create the constructor: void __sx_objc_selector_init().
|
||||
const void_ty = self.cached_void;
|
||||
// Constructor: void __sx_objc_selector_init().
|
||||
var no_params: [0]c.LLVMTypeRef = .{};
|
||||
const ctor_ty = c.LLVMFunctionType(void_ty, &no_params, 0, 0);
|
||||
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 each (selector_str, slot_global): emit
|
||||
// %sel = call ptr @sel_registerName(<"selector:">)
|
||||
// store ptr %sel, ptr @OBJC_SELECTOR_REFERENCES_<sel>
|
||||
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;
|
||||
|
||||
// Selector string constant. Make it private so multiple
|
||||
// constructors don't clash. `i8` array with NUL terminator.
|
||||
const sel_str_z = self.alloc.allocSentinel(u8, sel_str.len, 0) catch continue;
|
||||
defer self.alloc.free(sel_str_z);
|
||||
@memcpy(sel_str_z[0..sel_str.len], sel_str);
|
||||
const str_const = c.LLVMConstStringInContext(self.context, sel_str_z.ptr, @intCast(sel_str.len), 0);
|
||||
// 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);
|
||||
@@ -261,13 +282,10 @@ pub const LLVMEmitter = struct {
|
||||
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 in @llvm.global_ctors. Layout per LLVM Language
|
||||
// Reference: `[N x { i32, void()*, i8* }]`. Priority 65535 =
|
||||
// default; the third field carries an "associated data"
|
||||
// pointer (null for our case).
|
||||
// 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 };
|
||||
@@ -284,6 +302,45 @@ pub const LLVMEmitter = struct {
|
||||
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, "");
|
||||
}
|
||||
}
|
||||
|
||||
/// Lazily look up / declare the shared `@objc_msgSend` function.
|
||||
/// Cached on the emitter; all `objc_msg_send` instructions hand
|
||||
/// LLVMBuildCall2 their own per-call-site function type — the
|
||||
/// underlying function value is just an opaque `ptr` symbol.
|
||||
fn getObjcMsgSendValue(self: *LLVMEmitter) c.LLVMValueRef {
|
||||
if (self.objc_msg_send_value) |v| return v;
|
||||
const name_z = "objc_msgSend";
|
||||
if (c.LLVMGetNamedFunction(self.llvm_module, name_z)) |existing| {
|
||||
self.objc_msg_send_value = existing;
|
||||
return existing;
|
||||
}
|
||||
// Seed with a `(ptr, ptr) -> ptr` shape; opaque pointers mean
|
||||
// each call site can override.
|
||||
var params: [2]c.LLVMTypeRef = .{ self.cached_ptr, self.cached_ptr };
|
||||
const fn_ty = c.LLVMFunctionType(self.cached_ptr, ¶ms, 2, 0);
|
||||
const fn_val = c.LLVMAddFunction(self.llvm_module, name_z, fn_ty);
|
||||
c.LLVMSetLinkage(fn_val, c.LLVMExternalLinkage);
|
||||
self.objc_msg_send_value = fn_val;
|
||||
return fn_val;
|
||||
}
|
||||
|
||||
/// Compare IR typeSizeBytes against LLVMABISizeOfType for all user-defined types.
|
||||
@@ -982,6 +1039,47 @@ pub const LLVMEmitter = struct {
|
||||
},
|
||||
|
||||
// ── Calls ─────────────────────────────────────────────
|
||||
.objc_msg_send => |msg| {
|
||||
const msg_send = self.getObjcMsgSendValue();
|
||||
// Per-call-site LLVM function type. The Obj-C ABI uses
|
||||
// the C calling convention: recv + sel in the first
|
||||
// two int registers, additional args follow the C
|
||||
// rules for their types. We hand the precise type to
|
||||
// LLVMBuildCall2 — opaque pointers make the function
|
||||
// value type-agnostic.
|
||||
const ret_ty = self.toLLVMType(instruction.ty);
|
||||
const total_params: usize = 2 + msg.args.len;
|
||||
const param_types = self.alloc.alloc(c.LLVMTypeRef, total_params) catch unreachable;
|
||||
defer self.alloc.free(param_types);
|
||||
const call_args = self.alloc.alloc(c.LLVMValueRef, total_params) catch unreachable;
|
||||
defer self.alloc.free(call_args);
|
||||
|
||||
// recv (typed *void from the IR)
|
||||
param_types[0] = self.cached_ptr;
|
||||
call_args[0] = self.coerceArg(self.resolveRef(msg.recv), self.cached_ptr);
|
||||
// sel (loaded SEL — opaque ptr)
|
||||
param_types[1] = self.cached_ptr;
|
||||
call_args[1] = self.coerceArg(self.resolveRef(msg.sel), self.cached_ptr);
|
||||
// additional args take their IR types, with ABI
|
||||
// coercion applied so structs / strings decay the
|
||||
// same way they do for any C foreign call.
|
||||
for (msg.args, 0..) |arg_ref, i| {
|
||||
const raw_ty = self.getRefIRType(arg_ref) orelse .void;
|
||||
const raw_llvm = self.toLLVMType(raw_ty);
|
||||
const coerced_ty = self.abiCoerceParamType(raw_ty, raw_llvm);
|
||||
param_types[i + 2] = coerced_ty;
|
||||
call_args[i + 2] = self.coerceArg(self.resolveRef(arg_ref), coerced_ty);
|
||||
}
|
||||
|
||||
const fn_ty = c.LLVMFunctionType(ret_ty, param_types.ptr, @intCast(total_params), 0);
|
||||
const call_label: [*:0]const u8 = if (instruction.ty == .void) "" else "objc.msg";
|
||||
const result = c.LLVMBuildCall2(self.builder, fn_ty, msg_send, call_args.ptr, @intCast(total_params), call_label);
|
||||
// Always mapRef — the IR Ref counter for this
|
||||
// instruction advances regardless of return type,
|
||||
// so skipping it would misalign every subsequent
|
||||
// ref lookup in this function.
|
||||
self.mapRef(result);
|
||||
},
|
||||
.call => |call_op| {
|
||||
// Evaluate comptime functions at compile time
|
||||
const callee_func = &self.ir_mod.functions.items[call_op.callee.index()];
|
||||
|
||||
Reference in New Issue
Block a user