ffi #jni_main: sx-side super.method(args) dispatch via CallNonvirtual<T>Method

Inside a `#jni_main` (or any sx-defined `#jni_class`) bodied method,
`super.method(args)` lowers to JNI's nonvirtual dispatch against the
parent class resolved via `#extends` (default `android.app.Activity`).

  - lower.zig: tracks `current_foreign_class` + `current_foreign_method`
    around each `synthesizeJniMainStub` body; pushes the JNIEnv* arg
    onto the lexical `#jni_env` stack so omitted-env JNI calls inside
    the body see env without a wrapper. New `lowerSuperCall` handles
    the `super.method(args)` receiver pattern: derives parent path,
    reuses the enclosing method's signature when names match (the
    common `super.<override>(args)` case), or looks up the method on
    the parent class declared as `#foreign #jni_class`.
  - inst.zig: `JniMsgSend` gains `is_nonvirtual: bool` and
    `parent_class_path: ?[]const u8` — the dispatch tag + super class
    foreign path. Mutually exclusive with `is_static`.
  - emit_llvm.zig: new `CallNonvirtual<T>Method` vtable slots + a
    fourth dispatch arm. Resolves the parent jclass via
    `FindClass(env, parent_path)` (per-call; caching is follow-up),
    then `GetMethodID(env, parent_cls, name, sig)`, then
    `CallNonvirtual<T>Method(env, obj, parent_cls, mid, args...)`.

Disassembly on the smoke confirms the chain:
`ldr [env+0x30]` (FindClass) → `ldr [env+0x108]` (GetMethodID) →
`ldr [env+0x2d8]` (CallNonvirtualVoidMethod) with `(env, self,
parent_cls, mid, bundle)`.

132 host / 5 cross / zig build test all green. The slice unblocks
Activity lifecycle overrides (onCreate, onResume, onPause) calling
their required `super.<method>(args)` without raw `#jni_call`
boilerplate.
This commit is contained in:
agra
2026-05-20 16:57:30 +03:00
parent d43f21f39e
commit d946e3d577
7 changed files with 288 additions and 2 deletions

View File

@@ -35,6 +35,7 @@ fn isIdentByte(b: u8) bool {
/// `<jni.h>`. Slot numbers here MUST match the order of fields in
/// the C `JNINativeInterface_` struct.
const Jni = struct {
const FindClass: u32 = 6;
const NewGlobalRef: u32 = 21;
const GetObjectClass: u32 = 31;
const GetMethodID: u32 = 33;
@@ -48,6 +49,18 @@ const Jni = struct {
const CallFloatMethod: u32 = 55;
const CallDoubleMethod: u32 = 58;
const CallVoidMethod: u32 = 61;
// CallNonvirtual<T>Method (instance, super-dispatch variant). Used by
// `super.method(args)` from inside a `#jni_main` Activity method body:
// dispatch is bound to a specific class rather than going through the
// vtable, so subclass overrides don't intercept the call. Signature:
// `(JNIEnv*, jobject obj, jclass clazz, jmethodID, args...)`.
const CallNonvirtualObjectMethod: u32 = 64;
const CallNonvirtualBooleanMethod: u32 = 67;
const CallNonvirtualIntMethod: u32 = 79;
const CallNonvirtualLongMethod: u32 = 82;
const CallNonvirtualFloatMethod: u32 = 85;
const CallNonvirtualDoubleMethod: u32 = 88;
const CallNonvirtualVoidMethod: u32 = 91;
// Static-dispatch siblings — `target` IS already a `jclass`, so
// no `GetObjectClass` step. `GetStaticMethodID` returns a
// method-ID that's bound to a class+method+sig like the instance
@@ -1271,6 +1284,19 @@ pub const LLVMEmitter = struct {
return;
},
};
} else if (msg.is_nonvirtual) blk: {
if (is_pointer_ret) break :blk Jni.CallNonvirtualObjectMethod;
break :blk switch (ret_ty_id) {
.void => Jni.CallNonvirtualVoidMethod,
.s32 => Jni.CallNonvirtualIntMethod,
.s64 => Jni.CallNonvirtualLongMethod,
.f64 => Jni.CallNonvirtualDoubleMethod,
.bool => Jni.CallNonvirtualBooleanMethod,
else => {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
return;
},
};
} else blk: {
if (is_pointer_ret) break :blk Jni.CallObjectMethod;
break :blk switch (ret_ty_id) {
@@ -1350,7 +1376,20 @@ pub const LLVMEmitter = struct {
c.LLVMAddIncoming(phi, &phi_vals, &phi_blocks, 2);
break :blk phi;
} else blk: {
const cls = if (msg.is_static) target else inst_cls: {
const cls = if (msg.is_static) target else if (msg.is_nonvirtual) nonvirt_cls: {
// `super.method(args)`: dispatch is bound to a
// specific class (the parent), not subclass-override.
// Resolve via FindClass(parent_path). No caching yet —
// per-call lookup. The parent path is a NUL-terminated
// C string emitted as a private LLVM global.
const path = msg.parent_class_path orelse "";
const path_global = self.emitCStringGlobal(path, "jni.parent.path");
const find_class = self.loadJniFn(ifs, Jni.FindClass, "jni.FindClass");
var fc_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr };
const fc_ty = c.LLVMFunctionType(self.cached_ptr, &fc_params, 2, 0);
var fc_args = [_]c.LLVMValueRef{ env, path_global };
break :nonvirt_cls c.LLVMBuildCall2(self.builder, fc_ty, find_class, &fc_args, 2, "jni.parent.cls");
} else inst_cls: {
const get_obj_cls = self.loadJniFn(ifs, Jni.GetObjectClass, "jni.GetObjectClass");
var gocls_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr };
const gocls_ty = c.LLVMFunctionType(self.cached_ptr, &gocls_params, 2, 0);
@@ -1361,7 +1400,49 @@ pub const LLVMEmitter = struct {
var gmid_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr, self.cached_ptr, self.cached_ptr };
const gmid_ty = c.LLVMFunctionType(self.cached_ptr, &gmid_params, 4, 0);
var gmid_args = [_]c.LLVMValueRef{ env, cls, name_ptr, sig_ptr };
break :blk c.LLVMBuildCall2(self.builder, gmid_ty, get_mid, &gmid_args, 4, "jni.mid");
const mid_val = c.LLVMBuildCall2(self.builder, gmid_ty, get_mid, &gmid_args, 4, "jni.mid");
if (msg.is_nonvirtual) {
// Stash cls in a dummy slot so the call site below
// can pick it up. Easiest path: do the call right
// here and return Ref.none, but we need to keep the
// outer phi shape. Instead, return both via tuple
// through an auxiliary local — simplest is to attach
// `cls` to a per-invocation slot. Use a stack alloca.
const cls_slot = c.LLVMBuildAlloca(self.builder, self.cached_ptr, "jni.parent.cls.slot");
_ = c.LLVMBuildStore(self.builder, cls, cls_slot);
// Tag the slot pointer onto the phi result via the
// generated metadata: we'll re-extract by re-running
// FindClass — actually simpler: lower nonvirtual on
// the spot below. Drop the implicit `break` here:
const call_fn = self.loadJniFn(ifs, call_method_offset, "jni.callfn.nonvirtual");
const raw_ret = self.toLLVMType(ret_ty_id);
const total_call_params_nv: usize = 4 + msg.args.len;
const call_param_types_nv = self.alloc.alloc(c.LLVMTypeRef, total_call_params_nv) catch unreachable;
defer self.alloc.free(call_param_types_nv);
const call_args_nv = self.alloc.alloc(c.LLVMValueRef, total_call_params_nv) catch unreachable;
defer self.alloc.free(call_args_nv);
call_param_types_nv[0] = self.cached_ptr;
call_param_types_nv[1] = self.cached_ptr;
call_param_types_nv[2] = self.cached_ptr;
call_param_types_nv[3] = self.cached_ptr;
call_args_nv[0] = env;
call_args_nv[1] = target;
call_args_nv[2] = cls;
call_args_nv[3] = mid_val;
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);
call_param_types_nv[i + 4] = coerced_ty;
call_args_nv[i + 4] = self.coerceArg(self.resolveRef(arg_ref), coerced_ty);
}
const call_fn_ty_nv = c.LLVMFunctionType(raw_ret, call_param_types_nv.ptr, @intCast(total_call_params_nv), 0);
const label_nv: [*:0]const u8 = if (ret_ty_id == .void) "" else "jni.nonvirtual.ret";
const result_nv = c.LLVMBuildCall2(self.builder, call_fn_ty_nv, call_fn, call_args_nv.ptr, @intCast(total_call_params_nv), label_nv);
self.mapRef(result_nv);
return;
}
break :blk mid_val;
};
// Call<Type>Method: (JNIEnv*, jobject, jmethodID, args...) -> RetTy
@@ -3550,6 +3631,15 @@ pub const LLVMEmitter = struct {
return c.LLVMBuildInsertValue(self.builder, with_ptr, len_val, 1, "str.len");
}
/// Emit a NUL-terminated C string as a private LLVM global and return
/// the pointer to its first byte. Used for FindClass(env, "<path>") etc.
/// where the runtime expects raw `const char *`, not the sx slice shape.
fn emitCStringGlobal(self: *LLVMEmitter, str: []const u8, name: [*:0]const u8) c.LLVMValueRef {
const z = self.alloc.dupeZ(u8, str) catch unreachable;
defer self.alloc.free(z);
return c.LLVMBuildGlobalStringPtr(self.builder, z.ptr, name);
}
// ── Reflection emission helpers ────────────────────────────────
/// Build (or return cached) a global constant array of {ptr, i64} string values