ffi M1.2 A.4b.ii: emit C-ABI IMP trampolines (dead code pending class_addMethod)
For each bodied instance method on a sx-defined #objc_class,
emit a C-callconv trampoline function '__<Cls>_<method>_imp':
void __SxFoo_bump_imp(ptr obj, ptr _cmd, ...user_args) {
ivar = load @__SxFoo_state_ivar
state = object_getIvar(obj, ivar)
call @SxFoo.bump(__sx_default_context, state, ...user_args)
ret
}
The trampoline bridges the Obj-C runtime's IMP calling convention
('id self, SEL _cmd, ...args' as C ABI) to the sx body's
default-callconv shape ('__sx_ctx ptr, state ptr, ...user_args').
Implicit context comes from '&__sx_default_context'; the body
keeps its sx-side personality intact and can use 'self.field'
through the substituted state-struct pointer (M1.2 A.2b + A.3).
New helpers in lower.zig:
- 'getObjcObjectGetIvarFid' lazily declares object_getIvar.
- 'emitObjcDefinedClassImps' + 'emitObjcDefinedClassImp' walk the
cache and synthesise each trampoline.
- 'lookupGlobalIdByName' for finding the per-class ivar handle
global. Linear scan — same N-is-small rationale as the other
Obj-C caches.
Dead code at this commit: the trampolines exist in the module
but no class_addMethod call registers them with the runtime.
'objc_msgSend(obj, sel_bump)' would still fall through to the
parent class (NSObject 'doesNotRecognizeSelector:') today.
A.4b.iii wires up class_addMethod in emit_llvm's class-pair-init
constructor — that's when the trampolines come alive.
142's IR snapshot refreshed to show the trampoline.
173 example tests pass. zig build test green.
This commit is contained in:
182
src/ir/lower.zig
182
src/ir/lower.zig
@@ -108,6 +108,7 @@ pub const Lowering = struct {
|
||||
implicit_ctx_enabled: bool = false,
|
||||
current_ctx_ref: Ref = Ref.none,
|
||||
sel_register_name_fid: ?FuncId = null, // lazily-declared `sel_registerName` extern (non-literal selector fallback)
|
||||
objc_object_get_ivar_fid: ?FuncId = null, // lazily-declared `object_getIvar` extern (M1.2 A.4b IMP trampoline body)
|
||||
jni_env_stack: std.ArrayList(Ref) = std.ArrayList(Ref).empty, // lexical `#jni_env(env)` Ref stack — top is current scope's env for omitted-env `#jni_call`
|
||||
jni_env_stack_base: usize = 0, // index above which the currently-lowering fn's `#jni_env` scopes live; outer-fn Refs aren't valid in this fn's instruction stream
|
||||
jni_env_tl_get_fid: ?FuncId = null, // extern `sx_jni_env_tl_get` (from library/vendors/sx_jni_runtime/sx_jni_env_tl.c)
|
||||
@@ -11442,7 +11443,9 @@ pub const Lowering = struct {
|
||||
/// lazy lowering, so we walk the cache and force-lower here.
|
||||
/// `lowerFunction` sets `current_foreign_class` automatically based
|
||||
/// on the qualified name, so `*Self` substitutions in the body
|
||||
/// resolve correctly (M1.2 A.2b).
|
||||
/// resolve correctly (M1.2 A.2b). After the bodies are lowered,
|
||||
/// `emitObjcDefinedClassImps` wraps each with a C-ABI trampoline
|
||||
/// (M1.2 A.4b.ii).
|
||||
fn lowerObjcDefinedClassMethods(self: *Lowering) void {
|
||||
for (self.module.objc_defined_class_cache.items) |entry| {
|
||||
const fcd = entry.decl;
|
||||
@@ -11456,6 +11459,183 @@ pub const Lowering = struct {
|
||||
self.lazyLowerFunction(qualified);
|
||||
}
|
||||
}
|
||||
// Now the bodies are lowered — emit the C-ABI IMP trampolines
|
||||
// that bridge `objc_msgSend` invocations to them.
|
||||
self.emitObjcDefinedClassImps();
|
||||
}
|
||||
|
||||
/// Lazily declare `object_getIvar(obj: *void, ivar: *void) -> *void`
|
||||
/// as an extern. Cached so multiple IMP trampolines share one decl.
|
||||
fn getObjcObjectGetIvarFid(self: *Lowering) FuncId {
|
||||
if (self.objc_object_get_ivar_fid) |fid| return fid;
|
||||
const ptr_void = self.module.types.ptrTo(.void);
|
||||
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
||||
params.append(self.alloc, .{ .name = self.module.types.internString("obj"), .ty = ptr_void }) catch unreachable;
|
||||
params.append(self.alloc, .{ .name = self.module.types.internString("ivar"), .ty = ptr_void }) catch unreachable;
|
||||
const fn_name = self.module.types.internString("object_getIvar");
|
||||
const fid = self.builder.declareExtern(fn_name, params.toOwnedSlice(self.alloc) catch unreachable, ptr_void);
|
||||
const func = self.module.getFunctionMut(fid);
|
||||
func.call_conv = .c;
|
||||
self.objc_object_get_ivar_fid = fid;
|
||||
return fid;
|
||||
}
|
||||
|
||||
/// For each bodied instance method on a sx-defined `#objc_class`,
|
||||
/// emit a C-ABI IMP trampoline that the Obj-C runtime calls (after
|
||||
/// the dispatch path from `objc_msgSend`). The trampoline:
|
||||
/// 1. Loads the cached ivar handle from `@__<Cls>_state_ivar`.
|
||||
/// 2. Calls `object_getIvar(obj, ivar)` to get the `*<Cls>State`
|
||||
/// state pointer.
|
||||
/// 3. Calls the sx body `@<Cls>.<method>(__sx_default_context,
|
||||
/// state, ...user_args)` (default sx-callconv).
|
||||
/// 4. Returns the result (or `ret void`).
|
||||
///
|
||||
/// IMP name: `__<ClassName>_<methodName>_imp`. emit_llvm's
|
||||
/// constructor (A.4b.ii companion) registers this via
|
||||
/// `class_addMethod` with a derived selector + type encoding.
|
||||
fn emitObjcDefinedClassImps(self: *Lowering) void {
|
||||
for (self.module.objc_defined_class_cache.items) |entry| {
|
||||
const fcd = entry.decl;
|
||||
for (fcd.members) |m| {
|
||||
const method = switch (m) {
|
||||
.method => |md| md,
|
||||
else => continue,
|
||||
};
|
||||
if (method.body == null) continue;
|
||||
self.emitObjcDefinedClassImp(fcd, method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emitObjcDefinedClassImp(self: *Lowering, fcd: *const ast.ForeignClassDecl, md: ast.ForeignMethodDecl) void {
|
||||
// Save+restore builder state — we're switching into a new fn
|
||||
// mid-pass and need to restore for the next emit_llvm steps.
|
||||
const saved_func = self.builder.func;
|
||||
const saved_block = self.builder.current_block;
|
||||
const saved_counter = self.builder.inst_counter;
|
||||
defer {
|
||||
self.builder.func = saved_func;
|
||||
self.builder.current_block = saved_block;
|
||||
self.builder.inst_counter = saved_counter;
|
||||
}
|
||||
|
||||
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, md.name }) catch return;
|
||||
const name_id = self.module.types.internString(imp_name);
|
||||
const ptr_void = self.module.types.ptrTo(.void);
|
||||
|
||||
// C-ABI signature: (obj: *void, _cmd: *void, ...user_args) -> ret.
|
||||
// User params skip index 0 (which is *Self).
|
||||
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
||||
params.append(self.alloc, .{ .name = self.module.types.internString("obj"), .ty = ptr_void }) catch return;
|
||||
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
|
||||
|
||||
// Set current_foreign_class so *Self in user-param resolution
|
||||
// resolves to *<Cls>State (M1.2 A.2b). Save+restore.
|
||||
const saved_fc = self.current_foreign_class;
|
||||
self.current_foreign_class = fcd;
|
||||
defer self.current_foreign_class = saved_fc;
|
||||
|
||||
const param_start: usize = 1;
|
||||
for (md.params[param_start..], 0..) |p_node, i| {
|
||||
// User params are reflected at the C-ABI boundary AS-IS —
|
||||
// the runtime trampoline forwards them through to the body.
|
||||
// *Self here would be a programming error (only the implicit
|
||||
// self at index 0 is *Self), but we use resolveType to handle
|
||||
// pointer types correctly.
|
||||
const pty = self.resolveType(p_node);
|
||||
params.append(self.alloc, .{
|
||||
.name = self.module.types.internString(md.param_names[param_start + i]),
|
||||
.ty = pty,
|
||||
}) catch return;
|
||||
}
|
||||
|
||||
const ret_ty: TypeId = if (md.return_type) |rt| self.resolveType(rt) else .void;
|
||||
const params_slice = params.toOwnedSlice(self.alloc) catch return;
|
||||
|
||||
_ = self.builder.beginFunction(name_id, params_slice, ret_ty);
|
||||
const func = self.builder.currentFunc();
|
||||
func.linkage = .external;
|
||||
func.call_conv = .c;
|
||||
func.has_implicit_ctx = false;
|
||||
|
||||
const entry_name = self.module.types.internString("entry");
|
||||
const entry = self.builder.appendBlock(entry_name, &.{});
|
||||
self.builder.switchToBlock(entry);
|
||||
|
||||
// (1) Load ivar handle from per-class global.
|
||||
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return;
|
||||
defer self.alloc.free(ivar_global_name);
|
||||
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return;
|
||||
const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
|
||||
const ivar_handle = self.builder.load(ivar_addr, ptr_void);
|
||||
|
||||
// (2) state = object_getIvar(obj, ivar_handle).
|
||||
const get_ivar_fid = self.getObjcObjectGetIvarFid();
|
||||
const obj_ref = Ref.fromIndex(0);
|
||||
const get_ivar_args = self.alloc.alloc(Ref, 2) catch return;
|
||||
get_ivar_args[0] = obj_ref;
|
||||
get_ivar_args[1] = ivar_handle;
|
||||
const state_ptr = self.builder.emit(.{ .call = .{
|
||||
.callee = get_ivar_fid,
|
||||
.args = get_ivar_args,
|
||||
} }, ptr_void);
|
||||
|
||||
// (3) Call sx body `@<Cls>.<method>(default_ctx, state, ...user_args)`.
|
||||
const body_name = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, md.name }) catch return;
|
||||
defer self.alloc.free(body_name);
|
||||
const body_fid = self.resolveFuncByName(body_name) orelse return;
|
||||
|
||||
// Locate __sx_default_context global. When implicit_ctx is off
|
||||
// (no std.sx imported), the body has no __sx_ctx param either —
|
||||
// skip the ctx prepend.
|
||||
const ctx_ref: ?Ref = blk: {
|
||||
if (!self.implicit_ctx_enabled) break :blk null;
|
||||
const dctx_gi = self.global_names.get("__sx_default_context") orelse break :blk null;
|
||||
break :blk self.builder.emit(.{ .global_addr = dctx_gi.id }, ptr_void);
|
||||
};
|
||||
|
||||
// Build arg list: [ctx?] + state + user_args.
|
||||
const num_user_args = params_slice.len - 2; // minus obj + _cmd
|
||||
const num_call_args = (if (ctx_ref != null) @as(usize, 1) else 0) + 1 + num_user_args;
|
||||
const call_args = self.alloc.alloc(Ref, num_call_args) catch return;
|
||||
var idx: usize = 0;
|
||||
if (ctx_ref) |c_ref| {
|
||||
call_args[idx] = c_ref;
|
||||
idx += 1;
|
||||
}
|
||||
call_args[idx] = state_ptr;
|
||||
idx += 1;
|
||||
// User args come from imp params slots 2..N.
|
||||
var ip: usize = 2;
|
||||
while (ip < params_slice.len) : (ip += 1) {
|
||||
call_args[idx] = Ref.fromIndex(@intCast(ip));
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
const call_ref = self.builder.emit(.{ .call = .{
|
||||
.callee = body_fid,
|
||||
.args = call_args,
|
||||
} }, ret_ty);
|
||||
|
||||
// (4) Return.
|
||||
if (ret_ty == .void) {
|
||||
self.builder.retVoid();
|
||||
} else {
|
||||
self.builder.ret(call_ref, ret_ty);
|
||||
}
|
||||
|
||||
self.builder.finalize();
|
||||
}
|
||||
|
||||
/// Linear scan over module globals for a given name. Used for
|
||||
/// looking up the per-class ivar handle global from inside IMP
|
||||
/// trampoline emission.
|
||||
fn lookupGlobalIdByName(self: *Lowering, name: []const u8) ?inst_mod.GlobalId {
|
||||
const name_id = self.module.types.internString(name);
|
||||
for (self.module.globals.items, 0..) |g, i| {
|
||||
if (g.name == name_id) return inst_mod.GlobalId.fromIndex(@intCast(i));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn synthesizeJniMainStubs(self: *Lowering) void {
|
||||
|
||||
@@ -784,6 +784,18 @@ entry:
|
||||
ret { ptr, i64 } %call
|
||||
}
|
||||
|
||||
; Function Attrs: nounwind
|
||||
define void @__SxFoo_bump_imp(ptr %0, ptr %1) #0 {
|
||||
entry:
|
||||
%load = load ptr, ptr @__SxFoo_state_ivar, align 8
|
||||
%call = call ptr @object_getIvar(ptr %0, ptr %load)
|
||||
call void @SxFoo.bump(ptr @__sx_default_context, ptr %call)
|
||||
ret void
|
||||
}
|
||||
|
||||
; Function Attrs: nounwind
|
||||
declare ptr @object_getIvar(ptr, ptr) #0
|
||||
|
||||
declare i64 @write(i32, ptr, i64)
|
||||
|
||||
declare ptr @objc_getClass(ptr)
|
||||
|
||||
Reference in New Issue
Block a user