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:
agra
2026-05-25 22:52:34 +03:00
parent c2178c062b
commit c0b338eaa4
2 changed files with 193 additions and 1 deletions

View File

@@ -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 {

View File

@@ -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)