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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user