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:
@@ -1,28 +1,38 @@
|
|||||||
// Phase 1 step 1.6 (PLAN-FFI.md): non-void return shapes through
|
// Phase 1 step 1.6 (PLAN-FFI.md): non-void return shapes through
|
||||||
// `#objc_call`. The Phase 1.3 lowering hardcoded void+2-arg only;
|
// `#objc_call`. Each return type triggers a distinct LLVMBuildCall2
|
||||||
// this test exercises five primitive return types via real
|
// function-type combination so emit_llvm's per-call-site lowering
|
||||||
// Foundation classes:
|
// has to pick the right ABI per call.
|
||||||
//
|
//
|
||||||
// *void — [NSObject class] returns Class (ptr)
|
// We exercise both nil-recv (libobjc guarantees zero result for
|
||||||
// bool — [NSObject isMemberOfClass: cls] returns BOOL
|
// every shape) and real-recv paths so the ABI is verified beyond
|
||||||
// s32 — [NSString length] returns NSUInteger (treated as s32 here)
|
// "the runtime no-oped the call."
|
||||||
// s64 — same shape, wider
|
|
||||||
// f64 — [NSNumber doubleValue] returns double
|
|
||||||
//
|
|
||||||
// Today (1.6a, xfail): the lowering rejects non-void returns with
|
|
||||||
// a diagnostic. Snapshot pins the diagnostic.
|
|
||||||
// Next (1.6b/c): emit_llvm builds a per-call-site LLVM function
|
|
||||||
// type from (recv, sel, args, ret_ty), shares one declared
|
|
||||||
// `@objc_msgSend` symbol, dispatches with the right ABI.
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
#import "modules/compiler.sx";
|
#import "modules/compiler.sx";
|
||||||
|
#import "modules/std/objc.sx";
|
||||||
|
|
||||||
main :: () -> s32 {
|
main :: () -> s32 {
|
||||||
inline if OS == .macos {
|
inline if OS == .macos {
|
||||||
// *void return: [NSObject class] → Class
|
// ── Nil-recv quick smoke ───────────────────────────────────
|
||||||
cls := #objc_call(*void)(null, "class");
|
nil_cls := #objc_call(*void)(null, "class");
|
||||||
print("class non-null = {}\n", cls != null);
|
print("nil class = {}\n", nil_cls == null);
|
||||||
|
|
||||||
|
nil_n := #objc_call(s64)(null, "hash");
|
||||||
|
print("nil hash = {}\n", nil_n);
|
||||||
|
|
||||||
|
// ── Real-recv: NSObject ────────────────────────────────────
|
||||||
|
// *void return: [NSObject class] -> NSObject's metaclass (non-null,
|
||||||
|
// and conveniently == self when sent to the class itself).
|
||||||
|
ns_object := objc_getClass("NSObject".ptr);
|
||||||
|
meta := #objc_call(*void)(ns_object, "class");
|
||||||
|
print("meta non-null = {}\n", meta != null);
|
||||||
|
|
||||||
|
// s64 return: [obj hash] returns NSUInteger. On the NSObject
|
||||||
|
// class itself the value is implementation-defined but stable
|
||||||
|
// within a process — pinning it as non-zero is enough for ABI
|
||||||
|
// verification.
|
||||||
|
h := #objc_call(s64)(ns_object, "hash");
|
||||||
|
print("hash non-zero = {}\n", h != 0);
|
||||||
}
|
}
|
||||||
inline if OS != .macos {
|
inline if OS != .macos {
|
||||||
print("skipped (not macos)\n");
|
print("skipped (not macos)\n");
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ pub const LLVMEmitter = struct {
|
|||||||
string_struct_type: ?c.LLVMTypeRef,
|
string_struct_type: ?c.LLVMTypeRef,
|
||||||
any_struct_type: ?c.LLVMTypeRef,
|
any_struct_type: ?c.LLVMTypeRef,
|
||||||
closure_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)
|
// Cached field name arrays for reflection (TypeId → LLVM global)
|
||||||
field_name_arrays: std.AutoHashMap(u32, c.LLVMValueRef),
|
field_name_arrays: std.AutoHashMap(u32, c.LLVMValueRef),
|
||||||
@@ -159,6 +164,7 @@ pub const LLVMEmitter = struct {
|
|||||||
.string_struct_type = null,
|
.string_struct_type = null,
|
||||||
.any_struct_type = null,
|
.any_struct_type = null,
|
||||||
.closure_struct_type = null,
|
.closure_struct_type = null,
|
||||||
|
.objc_msg_send_value = null,
|
||||||
.field_name_arrays = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc),
|
.field_name_arrays = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc),
|
||||||
.target_config = target_config,
|
.target_config = target_config,
|
||||||
.build_config = .{},
|
.build_config = .{},
|
||||||
@@ -209,48 +215,63 @@ pub const LLVMEmitter = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Synthesize a module constructor that populates each interned
|
/// Synthesize a module constructor that populates each interned
|
||||||
/// Obj-C selector slot via `sel_registerName`, once at module load.
|
/// Obj-C selector slot via `sel_registerName`, once at module
|
||||||
/// Registered in `@llvm.global_ctors` so dyld / ld.so runs it
|
/// load. Registered in `@llvm.global_ctors` so dyld / ld.so / the
|
||||||
/// before main. Per `#objc_call` site collapses to a single load
|
/// LLVM ORC JIT all run it before main. Per `#objc_call` site
|
||||||
/// from the slot — matches clang's `@selector(...)` lowering.
|
/// 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 {
|
fn emitObjcSelectorInit(self: *LLVMEmitter) void {
|
||||||
if (self.ir_mod.objc_selector_cache.items.len == 0) return;
|
if (self.ir_mod.objc_selector_cache.items.len == 0) return;
|
||||||
|
|
||||||
// Look up the `sel_registerName` extern that the lowerer already
|
// Lazy-declare sel_registerName for the constructor body —
|
||||||
// declared. If for some reason it's absent (shouldn't happen —
|
// lower.zig only declares it when a non-literal selector
|
||||||
// every interned selector got there via the same lowering path),
|
// appears, which the constructor doesn't depend on.
|
||||||
// bail out and let the per-call fallback run.
|
|
||||||
const sel_reg_name = "sel_registerName";
|
const sel_reg_name = "sel_registerName";
|
||||||
const sel_reg_z = self.alloc.dupeZ(u8, sel_reg_name) catch unreachable;
|
const sel_reg_z = self.alloc.dupeZ(u8, sel_reg_name) catch unreachable;
|
||||||
defer self.alloc.free(sel_reg_z);
|
defer self.alloc.free(sel_reg_z);
|
||||||
const sel_reg_fn = c.LLVMGetNamedFunction(self.llvm_module, sel_reg_z.ptr);
|
var sel_reg_fn = c.LLVMGetNamedFunction(self.llvm_module, sel_reg_z.ptr);
|
||||||
if (sel_reg_fn == null) return;
|
var sel_reg_ty: c.LLVMTypeRef = undefined;
|
||||||
const sel_reg_ty = c.LLVMGlobalGetValueType(sel_reg_fn);
|
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().
|
// Constructor: void __sx_objc_selector_init().
|
||||||
const void_ty = self.cached_void;
|
|
||||||
var no_params: [0]c.LLVMTypeRef = .{};
|
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);
|
const ctor = c.LLVMAddFunction(self.llvm_module, "__sx_objc_selector_init", ctor_ty);
|
||||||
c.LLVMSetLinkage(ctor, c.LLVMInternalLinkage);
|
c.LLVMSetLinkage(ctor, c.LLVMInternalLinkage);
|
||||||
|
|
||||||
const entry = c.LLVMAppendBasicBlockInContext(self.context, ctor, "entry");
|
const entry = c.LLVMAppendBasicBlockInContext(self.context, ctor, "entry");
|
||||||
c.LLVMPositionBuilderAtEnd(self.builder, 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| {
|
for (self.ir_mod.objc_selector_cache.items) |entry_kv| {
|
||||||
const sel_str = entry_kv.sel;
|
const sel_str = entry_kv.sel;
|
||||||
const slot_gid = entry_kv.slot;
|
const slot_gid = entry_kv.slot;
|
||||||
const slot_global = self.global_map.get(@intCast(slot_gid.index())) orelse continue;
|
const slot_global = self.global_map.get(@intCast(slot_gid.index())) orelse continue;
|
||||||
|
|
||||||
// Selector string constant. Make it private so multiple
|
// Method-name C-string — names match clang's convention
|
||||||
// constructors don't clash. `i8` array with NUL terminator.
|
// so debuggers / nm / dyld see the same symbols, even
|
||||||
const sel_str_z = self.alloc.allocSentinel(u8, sel_str.len, 0) catch continue;
|
// though the surrounding section tagging isn't load-
|
||||||
defer self.alloc.free(sel_str_z);
|
// bearing in our JIT path.
|
||||||
@memcpy(sel_str_z[0..sel_str.len], sel_str);
|
const meth_str_z = self.alloc.allocSentinel(u8, sel_str.len, 0) catch continue;
|
||||||
const str_const = c.LLVMConstStringInContext(self.context, sel_str_z.ptr, @intCast(sel_str.len), 0);
|
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_");
|
const str_global = c.LLVMAddGlobal(self.llvm_module, c.LLVMTypeOf(str_const), "OBJC_METH_VAR_NAME_");
|
||||||
c.LLVMSetInitializer(str_global, str_const);
|
c.LLVMSetInitializer(str_global, str_const);
|
||||||
c.LLVMSetLinkage(str_global, c.LLVMPrivateLinkage);
|
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");
|
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.LLVMBuildStore(self.builder, sel_val, slot_global);
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = c.LLVMBuildRetVoid(self.builder);
|
_ = c.LLVMBuildRetVoid(self.builder);
|
||||||
|
|
||||||
// Register in @llvm.global_ctors. Layout per LLVM Language
|
// Register the constructor in @llvm.global_ctors. dyld picks
|
||||||
// Reference: `[N x { i32, void()*, i8* }]`. Priority 65535 =
|
// this up for a fully-linked binary at load time.
|
||||||
// default; the third field carries an "associated data"
|
|
||||||
// pointer (null for our case).
|
|
||||||
const i32_ty = self.cached_i32;
|
const i32_ty = self.cached_i32;
|
||||||
const ptr_ty = self.cached_ptr;
|
const ptr_ty = self.cached_ptr;
|
||||||
var ctor_field_types: [3]c.LLVMTypeRef = .{ i32_ty, ptr_ty, ptr_ty };
|
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");
|
const ctors_global = c.LLVMAddGlobal(self.llvm_module, ctors_arr_ty, "llvm.global_ctors");
|
||||||
c.LLVMSetInitializer(ctors_global, ctors_init);
|
c.LLVMSetInitializer(ctors_global, ctors_init);
|
||||||
c.LLVMSetLinkage(ctors_global, c.LLVMAppendingLinkage);
|
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.
|
/// Compare IR typeSizeBytes against LLVMABISizeOfType for all user-defined types.
|
||||||
@@ -982,6 +1039,47 @@ pub const LLVMEmitter = struct {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ── Calls ─────────────────────────────────────────────
|
// ── 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| {
|
.call => |call_op| {
|
||||||
// Evaluate comptime functions at compile time
|
// Evaluate comptime functions at compile time
|
||||||
const callee_func = &self.ir_mod.functions.items[call_op.callee.index()];
|
const callee_func = &self.ir_mod.functions.items[call_op.callee.index()];
|
||||||
|
|||||||
@@ -179,6 +179,15 @@ pub const Op = union(enum) {
|
|||||||
call_builtin: BuiltinCall,
|
call_builtin: BuiltinCall,
|
||||||
compiler_call: CompilerCall,
|
compiler_call: CompilerCall,
|
||||||
|
|
||||||
|
/// `#objc_call(ReturnT)(recv, sel, args...)` — dispatched through
|
||||||
|
/// `objc_msgSend`. emit_llvm.zig synthesizes a per-call-site LLVM
|
||||||
|
/// function type from the arg/result Refs and reuses a single
|
||||||
|
/// declared `@objc_msgSend` symbol across all return-type
|
||||||
|
/// variants. Encoded as its own opcode (instead of `.call` /
|
||||||
|
/// `.call_indirect`) so the IR doesn't need a separate FuncId
|
||||||
|
/// per signature shape.
|
||||||
|
objc_msg_send: ObjcMsgSend,
|
||||||
|
|
||||||
// ── Protocol dispatch ───────────────────────────────────────────
|
// ── Protocol dispatch ───────────────────────────────────────────
|
||||||
protocol_call_dynamic: ProtocolCall, // vtable/inline dispatch
|
protocol_call_dynamic: ProtocolCall, // vtable/inline dispatch
|
||||||
protocol_erase: ProtocolErase, // concrete → protocol value (xx)
|
protocol_erase: ProtocolErase, // concrete → protocol value (xx)
|
||||||
@@ -284,6 +293,17 @@ pub const CallIndirect = struct {
|
|||||||
args: []const Ref,
|
args: []const Ref,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// `#objc_call` dispatch through `objc_msgSend`. emit_llvm reads
|
||||||
|
/// `recv`/`sel`/each arg's IR type to build the per-call-site LLVM
|
||||||
|
/// function type; the instruction's own `ty` field (`Inst.ty`) is the
|
||||||
|
/// Obj-C return type. One declared `@objc_msgSend` symbol is shared
|
||||||
|
/// across every distinct signature shape.
|
||||||
|
pub const ObjcMsgSend = struct {
|
||||||
|
recv: Ref,
|
||||||
|
sel: Ref,
|
||||||
|
args: []const Ref, // additional args after recv + sel
|
||||||
|
};
|
||||||
|
|
||||||
pub const BuiltinCall = struct {
|
pub const BuiltinCall = struct {
|
||||||
builtin: BuiltinId,
|
builtin: BuiltinId,
|
||||||
args: []const Ref,
|
args: []const Ref,
|
||||||
|
|||||||
@@ -530,6 +530,11 @@ pub const Interpreter = struct {
|
|||||||
return .{ .value = result };
|
return .{ .value = result };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// The Obj-C runtime isn't available at comptime; any
|
||||||
|
// `#objc_call` reached during `#run` execution can't
|
||||||
|
// resolve. Fail fast so callers see a useful diagnostic.
|
||||||
|
.objc_msg_send => return error.CannotEvalComptime,
|
||||||
|
|
||||||
// ── Block params ────────────────────────────────────
|
// ── Block params ────────────────────────────────────
|
||||||
.block_param => {
|
.block_param => {
|
||||||
// Block params are pushed at the start of block execution.
|
// Block params are pushed at the start of block execution.
|
||||||
|
|||||||
@@ -95,8 +95,7 @@ pub const Lowering = struct {
|
|||||||
module_scopes: ?*std.StringHashMap(std.StringHashMap(void)) = null, // per-module visible names (from import resolution)
|
module_scopes: ?*std.StringHashMap(std.StringHashMap(void)) = null, // per-module visible names (from import resolution)
|
||||||
import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null, // module path → set of directly imported paths (used by param_impl_map visibility filter)
|
import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null, // module path → set of directly imported paths (used by param_impl_map visibility filter)
|
||||||
current_source_file: ?[]const u8 = null, // source file of function currently being lowered
|
current_source_file: ?[]const u8 = null, // source file of function currently being lowered
|
||||||
objc_msg_send_fid: ?FuncId = null, // lazily-declared `objc_msgSend` extern (for #objc_call lowering)
|
sel_register_name_fid: ?FuncId = null, // lazily-declared `sel_registerName` extern (non-literal selector fallback)
|
||||||
sel_register_name_fid: ?FuncId = null, // lazily-declared `sel_registerName` extern
|
|
||||||
type_bindings: ?std.StringHashMap(TypeId) = null, // generic type param bindings ($T → concrete TypeId)
|
type_bindings: ?std.StringHashMap(TypeId) = null, // generic type param bindings ($T → concrete TypeId)
|
||||||
current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch)
|
current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch)
|
||||||
force_block_value: bool = false, // set by lowerBlockValue to extract if-else values
|
force_block_value: bool = false, // set by lowerBlockValue to extract if-else values
|
||||||
@@ -3745,11 +3744,6 @@ pub const Lowering = struct {
|
|||||||
fn internObjcSelector(self: *Lowering, sel_str: []const u8) inst_mod.GlobalId {
|
fn internObjcSelector(self: *Lowering, sel_str: []const u8) inst_mod.GlobalId {
|
||||||
if (self.module.lookupObjcSelector(sel_str)) |gid| return gid;
|
if (self.module.lookupObjcSelector(sel_str)) |gid| return gid;
|
||||||
|
|
||||||
// First interned selector → ensure `sel_registerName` is declared
|
|
||||||
// so emit_llvm.zig's constructor pass can find it and populate
|
|
||||||
// every cached SEL slot at module load.
|
|
||||||
_ = self.getSelRegisterNameFid();
|
|
||||||
|
|
||||||
// Mangle selector: replace colons with underscores. Apple's
|
// Mangle selector: replace colons with underscores. Apple's
|
||||||
// toolchain does the same (foo:bar: → foo_bar_).
|
// toolchain does the same (foo:bar: → foo_bar_).
|
||||||
var mangled = std.ArrayList(u8).empty;
|
var mangled = std.ArrayList(u8).empty;
|
||||||
@@ -3789,26 +3783,6 @@ pub const Lowering = struct {
|
|||||||
return fid;
|
return fid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lazily declare `objc_msgSend(recv: *void, sel: *void) -> *void`.
|
|
||||||
/// Cast at the call site by the LLVM lowering (the `coerceArg` /
|
|
||||||
/// type-equivalence path). For Phase 1.3 the only return shape
|
|
||||||
/// exercised is void; the *void return is discarded.
|
|
||||||
fn getObjcMsgSendFid(self: *Lowering) FuncId {
|
|
||||||
if (self.objc_msg_send_fid) |fid| return fid;
|
|
||||||
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
||||||
const recv_str = self.module.types.internString("recv");
|
|
||||||
const sel_str = self.module.types.internString("sel");
|
|
||||||
const vptr = self.module.types.ptrTo(.void);
|
|
||||||
params.append(self.alloc, .{ .name = recv_str, .ty = vptr }) catch unreachable;
|
|
||||||
params.append(self.alloc, .{ .name = sel_str, .ty = vptr }) catch unreachable;
|
|
||||||
const fn_name = self.module.types.internString("objc_msgSend");
|
|
||||||
const fid = self.builder.declareExtern(fn_name, params.toOwnedSlice(self.alloc) catch unreachable, vptr);
|
|
||||||
const func = self.module.getFunctionMut(fid);
|
|
||||||
func.call_conv = .c;
|
|
||||||
self.objc_msg_send_fid = fid;
|
|
||||||
return fid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lower `#objc_call(T)(recv, "sel:", args...)` to:
|
/// Lower `#objc_call(T)(recv, "sel:", args...)` to:
|
||||||
/// %sel = call ptr @sel_registerName(<"sel:">)
|
/// %sel = call ptr @sel_registerName(<"sel:">)
|
||||||
/// %ret = call <ABI(T)> @objc_msgSend(recv, %sel, args...)
|
/// %ret = call <ABI(T)> @objc_msgSend(recv, %sel, args...)
|
||||||
@@ -3833,13 +3807,9 @@ pub const Lowering = struct {
|
|||||||
// Resolve the return type from the syntactic slot.
|
// Resolve the return type from the syntactic slot.
|
||||||
const ret_ty = self.resolveType(fic.return_type);
|
const ret_ty = self.resolveType(fic.return_type);
|
||||||
|
|
||||||
// For Phase 1.3 the only supported return-type / arity combo is
|
if (fic.args.len < 2) {
|
||||||
// (void, recv + selector). Anything else falls through to undef
|
|
||||||
// for now — the next phase-1 steps fill these in one shape at
|
|
||||||
// a time.
|
|
||||||
if (ret_ty != .void or fic.args.len != 2) {
|
|
||||||
if (self.diagnostics) |d| {
|
if (self.diagnostics) |d| {
|
||||||
d.add(.err, "#objc_call: only `void` return + (recv, selector) is lowered today; non-void / arg-bearing arities land in later phase-1 steps", null);
|
d.add(.err, "#objc_call requires at least a receiver and a selector", null);
|
||||||
}
|
}
|
||||||
return Ref.none;
|
return Ref.none;
|
||||||
}
|
}
|
||||||
@@ -3847,12 +3817,12 @@ pub const Lowering = struct {
|
|||||||
// Receiver expression.
|
// Receiver expression.
|
||||||
const recv = self.lowerExpr(fic.args[0]);
|
const recv = self.lowerExpr(fic.args[0]);
|
||||||
|
|
||||||
// Selector. If it's a literal at parse time, intern into a
|
// Selector. Literal selectors get interned into a module-
|
||||||
// module-scoped `SEL*` slot that emit_llvm.zig populates once
|
// scoped `SEL*` slot — emit_llvm.zig tags the slot into
|
||||||
// at module init (Phase 1.5). Per call site collapses to a
|
// `__DATA,__objc_selrefs` so dyld populates it at load time
|
||||||
// single load — matches clang's `@selector(...)` lowering.
|
// (matches clang's `@selector(...)` lowering exactly).
|
||||||
// Non-literal selectors keep the per-call sel_registerName
|
// Non-literal selectors keep the per-call `sel_registerName`
|
||||||
// fallback for now.
|
// fallback.
|
||||||
const sel_arg_node = fic.args[1];
|
const sel_arg_node = fic.args[1];
|
||||||
const vptr_ty = self.module.types.ptrTo(.void);
|
const vptr_ty = self.module.types.ptrTo(.void);
|
||||||
const sel = blk: {
|
const sel = blk: {
|
||||||
@@ -3862,7 +3832,6 @@ pub const Lowering = struct {
|
|||||||
const slot_ptr = self.builder.emit(.{ .global_addr = slot_gid }, self.module.types.ptrTo(vptr_ty));
|
const slot_ptr = self.builder.emit(.{ .global_addr = slot_gid }, self.module.types.ptrTo(vptr_ty));
|
||||||
break :blk self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty);
|
break :blk self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty);
|
||||||
}
|
}
|
||||||
// Fallback: non-literal selector → runtime lookup per call.
|
|
||||||
const sel_ref = self.lowerExpr(sel_arg_node);
|
const sel_ref = self.lowerExpr(sel_arg_node);
|
||||||
const sel_fid = self.getSelRegisterNameFid();
|
const sel_fid = self.getSelRegisterNameFid();
|
||||||
var sel_args = std.ArrayList(Ref).empty;
|
var sel_args = std.ArrayList(Ref).empty;
|
||||||
@@ -3871,17 +3840,19 @@ pub const Lowering = struct {
|
|||||||
break :blk self.builder.emit(.{ .call = .{ .callee = sel_fid, .args = sel_owned } }, vptr_ty);
|
break :blk self.builder.emit(.{ .call = .{ .callee = sel_fid, .args = sel_owned } }, vptr_ty);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dispatch through objc_msgSend.
|
// Additional args after recv + selector.
|
||||||
const msg_fid = self.getObjcMsgSendFid();
|
var extra = std.ArrayList(Ref).empty;
|
||||||
var call_args = std.ArrayList(Ref).empty;
|
var ai: usize = 2;
|
||||||
call_args.append(self.alloc, recv) catch unreachable;
|
while (ai < fic.args.len) : (ai += 1) {
|
||||||
call_args.append(self.alloc, sel) catch unreachable;
|
extra.append(self.alloc, self.lowerExpr(fic.args[ai])) catch unreachable;
|
||||||
const owned = call_args.toOwnedSlice(self.alloc) catch unreachable;
|
}
|
||||||
// Result type is `*void` here (objc_msgSend's declared shape).
|
const extra_owned = extra.toOwnedSlice(self.alloc) catch unreachable;
|
||||||
// For `void` user-facing returns we just discard the Ref —
|
|
||||||
// the IR keeps the side-effecting call instruction either way.
|
return self.builder.emit(.{ .objc_msg_send = .{
|
||||||
_ = self.builder.emit(.{ .call = .{ .callee = msg_fid, .args = owned } }, self.module.types.ptrTo(.void));
|
.recv = recv,
|
||||||
return Ref.none;
|
.sel = sel,
|
||||||
|
.args = extra_owned,
|
||||||
|
} }, ret_ty);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Calls ───────────────────────────────────────────────────────
|
// ── Calls ───────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -316,6 +316,11 @@ fn printInst(instruction: *const Inst, ref_idx: u32, tt: *const TypeTable, write
|
|||||||
try writeArgs(c.args, writer);
|
try writeArgs(c.args, writer);
|
||||||
try writer.writeAll(") : ");
|
try writer.writeAll(") : ");
|
||||||
},
|
},
|
||||||
|
.objc_msg_send => |c| {
|
||||||
|
try writer.print("objc_msg_send recv=%{d} sel=%{d}(", .{ c.recv.index(), c.sel.index() });
|
||||||
|
try writeArgs(c.args, writer);
|
||||||
|
try writer.writeAll(") : ");
|
||||||
|
},
|
||||||
.compiler_call => |cc| {
|
.compiler_call => |cc| {
|
||||||
const name = tt.getString(@enumFromInt(cc.name));
|
const name = tt.getString(@enumFromInt(cc.name));
|
||||||
try writer.print("compiler_call \"{s}\"(", .{name});
|
try writer.print("compiler_call \"{s}\"(", .{name});
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ declare i64 @build_options() #0
|
|||||||
; Function Attrs: nounwind
|
; Function Attrs: nounwind
|
||||||
define i32 @main() #0 {
|
define i32 @main() #0 {
|
||||||
entry:
|
entry:
|
||||||
|
call void @__sx_objc_selector_init()
|
||||||
%alloca = alloca { i64 }, align 8
|
%alloca = alloca { i64 }, align 8
|
||||||
store { i64 } zeroinitializer, ptr %alloca, align 8
|
store { i64 } zeroinitializer, ptr %alloca, align 8
|
||||||
%si = insertvalue { ptr, ptr, ptr } undef, ptr %alloca, 0
|
%si = insertvalue { ptr, ptr, ptr } undef, ptr %alloca, 0
|
||||||
@@ -227,13 +228,13 @@ entry:
|
|||||||
%siN = insertvalue { { ptr, ptr, ptr }, ptr } %siN, ptr null, 1
|
%siN = insertvalue { { ptr, ptr, ptr }, ptr } %siN, ptr null, 1
|
||||||
store { { ptr, ptr, ptr }, ptr } %siN, ptr @context, align 8
|
store { { ptr, ptr, ptr }, ptr } %siN, ptr @context, align 8
|
||||||
%load = load ptr, ptr @OBJC_SELECTOR_REFERENCES_init, align 8
|
%load = load ptr, ptr @OBJC_SELECTOR_REFERENCES_init, align 8
|
||||||
%call = call ptr @objc_msgSend(ptr null, ptr %load)
|
call void @objc_msgSend(ptr null, ptr %load)
|
||||||
%loadN = load ptr, ptr @OBJC_SELECTOR_REFERENCES_init, align 8
|
%loadN = load ptr, ptr @OBJC_SELECTOR_REFERENCES_init, align 8
|
||||||
%callN = call ptr @objc_msgSend(ptr null, ptr %loadN)
|
call void @objc_msgSend(ptr null, ptr %loadN)
|
||||||
%loadN = load ptr, ptr @OBJC_SELECTOR_REFERENCES_init, align 8
|
%loadN = load ptr, ptr @OBJC_SELECTOR_REFERENCES_init, align 8
|
||||||
%callN = call ptr @objc_msgSend(ptr null, ptr %loadN)
|
call void @objc_msgSend(ptr null, ptr %loadN)
|
||||||
%loadN = load ptr, ptr @OBJC_SELECTOR_REFERENCES_release, align 8
|
%loadN = load ptr, ptr @OBJC_SELECTOR_REFERENCES_release, align 8
|
||||||
%callN = call ptr @objc_msgSend(ptr null, ptr %loadN)
|
call void @objc_msgSend(ptr null, ptr %loadN)
|
||||||
%allocaN = alloca { ptr, i64 }, align 8
|
%allocaN = alloca { ptr, i64 }, align 8
|
||||||
%gep = getelementptr inbounds { ptr, i64 }, ptr %allocaN, i32 0, i32 0
|
%gep = getelementptr inbounds { ptr, i64 }, ptr %allocaN, i32 0, i32 0
|
||||||
store ptr null, ptr %gep, align 8
|
store ptr null, ptr %gep, align 8
|
||||||
@@ -245,8 +246,8 @@ entry:
|
|||||||
store { ptr, i64 } { ptr @str.1, i64 0 }, ptr %allocaN, align 8
|
store { ptr, i64 } { ptr @str.1, i64 0 }, ptr %allocaN, align 8
|
||||||
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
|
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
|
||||||
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
|
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
|
||||||
%callN = call { ptr, i64 } @substr({ ptr, i64 } %loadN, i64 0, i64 3)
|
%call = call { ptr, i64 } @substr({ ptr, i64 } %loadN, i64 0, i64 3)
|
||||||
%callN = call { ptr, i64 } @concat({ ptr, i64 } %loadN, { ptr, i64 } %callN)
|
%callN = call { ptr, i64 } @concat({ ptr, i64 } %loadN, { ptr, i64 } %call)
|
||||||
store { ptr, i64 } %callN, ptr %allocaN, align 8
|
store { ptr, i64 } %callN, ptr %allocaN, align 8
|
||||||
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
|
%loadN = load { ptr, i64 }, ptr %allocaN, align 8
|
||||||
%str.ptr = extractvalue { ptr, i64 } %loadN, 0
|
%str.ptr = extractvalue { ptr, i64 } %loadN, 0
|
||||||
@@ -269,14 +270,12 @@ entry:
|
|||||||
ret void
|
ret void
|
||||||
}
|
}
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
declare ptr @objc_msgSend(ptr, ptr)
|
||||||
declare ptr @sel_registerName(ptr) #0
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
declare ptr @objc_msgSend(ptr, ptr) #0
|
|
||||||
|
|
||||||
declare i64 @write(i32, ptr, i64)
|
declare i64 @write(i32, ptr, i64)
|
||||||
|
|
||||||
|
declare ptr @sel_registerName(ptr)
|
||||||
|
|
||||||
define internal void @__sx_objc_selector_init() {
|
define internal void @__sx_objc_selector_init() {
|
||||||
entry:
|
entry:
|
||||||
%sel = call ptr @sel_registerName(ptr @OBJC_METH_VAR_NAME_)
|
%sel = call ptr @sel_registerName(ptr @OBJC_METH_VAR_NAME_)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1
|
0
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
/Users/agra/projects/sx/examples/ffi-objc-call-04-primitive-returns.sx: error: #objc_call: only `void` return + (recv, selector) is lowered today; non-void / arg-bearing arities land in later phase-1 steps
|
nil class = true
|
||||||
|
nil hash = 0
|
||||||
|
meta non-null = true
|
||||||
|
hash non-zero = true
|
||||||
|
|||||||
Reference in New Issue
Block a user