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

@@ -104,6 +104,8 @@ pub const Lowering = struct {
jni_env_tl_set_fid: ?FuncId = null, // extern `sx_jni_env_tl_set`
needs_jni_env_tl_runtime: bool = false, // set when lowering touches the JNI env TL; signals Compilation to auto-link the runtime .c
foreign_class_map: std.StringHashMap(*const ast.ForeignClassDecl) = std.StringHashMap(*const ast.ForeignClassDecl).init(std.heap.page_allocator), // sx alias → ForeignClassDecl (jni_class / objc_class / swift_class / ... — registered in scan pass)
current_foreign_class: ?*const ast.ForeignClassDecl = null, // set while lowering a `#jni_main` (or any sx-defined `#jni_class`) bodied method — `super.method(args)` dispatch resolves the parent class against this fcd's `#extends`
current_foreign_method: ?ast.ForeignMethodDecl = null, // the specific method whose body is being lowered; `super.<same_name>(...)` reuses its signature
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)
force_block_value: bool = false, // set by lowerBlockValue to extract if-else values
@@ -4081,6 +4083,124 @@ pub const Lowering = struct {
} }, ret_ty);
}
/// Lower `super.method(args)` inside a `#jni_main` / sx-defined
/// `#jni_class` bodied method. Resolves the parent class from the
/// enclosing fcd's `#extends` clause (default `android.app.Activity`)
/// and emits a `JniMsgSend` with `is_nonvirtual=true`, which
/// emit_llvm expands into a `FindClass(parent) + GetMethodID +
/// CallNonvirtual<T>Method` chain.
///
/// Signature derivation: when `method_name` matches the enclosing
/// method's name (the common case — `super.onCreate(b)` from inside
/// `onCreate :: (self, b)` override), the enclosing method's
/// signature is reused. Other method names require the parent class
/// to be declared via `#foreign #jni_class` so the signature can be
/// looked up.
fn lowerSuperCall(
self: *Lowering,
method_name: []const u8,
method_args: []const Ref,
span: ast.Span,
) Ref {
const fcd = self.current_foreign_class orelse {
if (self.diagnostics) |d| d.addFmt(.err, span, "'super' is only valid inside a `#jni_class` method body", .{});
return Ref.none;
};
// Resolve parent foreign_path from the fcd's `#extends`. Default to
// android.app.Activity to match the jni_java_emit default.
var parent_path: []const u8 = "android/app/Activity";
for (fcd.members) |m| switch (m) {
.extends => |alias| {
if (self.foreign_class_map.get(alias)) |parent_fcd| {
parent_path = parent_fcd.foreign_path;
} else {
parent_path = alias;
}
break;
},
else => {},
};
// Resolve method signature. Same-name fast path reuses the
// enclosing method's descriptor; cross-method super calls require
// the parent class to be declared via `#foreign #jni_class`.
var descriptor: []const u8 = "";
var resolved_method: ?ast.ForeignMethodDecl = null;
if (self.current_foreign_method) |em| {
if (std.mem.eql(u8, em.name, method_name)) {
resolved_method = em;
}
}
if (resolved_method == null) {
const parent_fcd = blk: for (fcd.members) |m| switch (m) {
.extends => |alias| if (self.foreign_class_map.get(alias)) |pf| break :blk pf else continue,
else => {},
} else null;
if (parent_fcd) |pf| {
for (pf.members) |pm| switch (pm) {
.method => |pmd| if (std.mem.eql(u8, pmd.name, method_name)) {
resolved_method = pmd;
break;
},
else => {},
};
}
}
const method = resolved_method orelse {
if (self.diagnostics) |d| d.addFmt(.err, span, "no method '{s}' found for `super.{s}(...)` — declare the parent class via `#foreign #jni_class` to make cross-method super calls available", .{ method_name, method_name });
return Ref.none;
};
// Derive descriptor against the parent path (used as enclosing_path
// for `*Self` resolution).
var registry = jni_descriptor.ClassRegistry.init(self.alloc);
defer registry.deinit();
var it = self.foreign_class_map.iterator();
while (it.next()) |entry| {
registry.put(entry.key_ptr.*, entry.value_ptr.*.foreign_path) catch {};
}
descriptor = jni_descriptor.deriveMethod(self.alloc, .{
.enclosing_path = parent_path,
.classes = &registry,
}, method) catch |err| {
if (self.diagnostics) |d| d.addFmt(.err, span, "super-call descriptor derivation failed for '{s}.{s}': {s}", .{ parent_path, method_name, @errorName(err) });
return Ref.none;
};
// env from the lexical stack (pushed by synthesizeJniMainStub).
if (self.jni_env_stack.items.len <= self.jni_env_stack_base) {
if (self.diagnostics) |d| d.addFmt(.err, span, "`super.{s}(...)` requires an enclosing `#jni_main` method scope (env is unavailable)", .{method_name});
return Ref.none;
}
const env_ref = self.jni_env_stack.items[self.jni_env_stack.items.len - 1];
// `self` is the first param of the synthesized `Java_*` fn. Bound
// in scope as `self` by synthesizeJniMainStub.
const self_binding = if (self.scope) |s| s.lookup("self") else null;
const self_ref = if (self_binding) |b| (if (b.is_alloca) self.builder.load(b.ref, b.ty) else b.ref) else Ref.none;
const name_sid = self.module.types.internString(method_name);
const name_ref = self.builder.constString(name_sid);
const sig_sid = self.module.types.internString(descriptor);
const sig_ref = self.builder.constString(sig_sid);
const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else .void;
const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable;
return self.builder.emit(.{ .jni_msg_send = .{
.env = env_ref,
.target = self_ref,
.name = name_ref,
.sig = sig_ref,
.args = args_owned,
.is_static = false,
.is_nonvirtual = true,
.parent_class_path = self.alloc.dupe(u8, parent_path) catch parent_path,
.cache_key = null, // per-call FindClass + GetMethodID; caching is a follow-up
} }, ret_ty);
}
// ── Calls ───────────────────────────────────────────────────────
fn lowerCall(self: *Lowering, c: *const ast.Call) Ref {
@@ -4426,6 +4546,16 @@ pub const Lowering = struct {
return self.emitError(id.name, c.callee.span);
},
.field_access => |fa| {
// `super.method(args)` from inside a `#jni_main` (or any
// sx-defined `#jni_class`) bodied method. Dispatch via
// CallNonvirtual<T>Method against the parent class
// resolved from the enclosing fcd's `#extends` clause.
if (fa.object.data == .identifier and
std.mem.eql(u8, fa.object.data.identifier.name, "super"))
{
return self.lowerSuperCall(fa.field, args.items, c.callee.span);
}
// Pattern-match context.allocator.alloc/dealloc → heap_alloc/heap_free
if (self.matchContextAllocCall(fa, args.items)) |ref| return ref;
@@ -9832,6 +9962,33 @@ pub const Lowering = struct {
scope.put(self.module.types.getString(p.name), .{ .ref = slot, .ty = p.ty, .is_alloca = true });
}
// Push the JNIEnv* arg onto the lexical `#jni_env` stack so the
// method body's `#jni_call(...)` / `super.method(...)` sites pick
// it up without an explicit `#jni_env(env) { ... }` wrapper. The
// JNI runtime guarantees the env passed to a native method is
// valid for the calling thread.
const env_slot = scope.lookup("env").?.ref;
const env_loaded = self.builder.load(env_slot, ptr_void);
const env_stack_base = self.jni_env_stack_base;
self.jni_env_stack_base = self.jni_env_stack.items.len;
self.jni_env_stack.append(self.alloc, env_loaded) catch {};
defer {
_ = self.jni_env_stack.pop();
self.jni_env_stack_base = env_stack_base;
}
// Record method context so `super.method(args)` inside the body
// can find the parent class (via `#extends`) and the method's
// signature.
const saved_fcd = self.current_foreign_class;
const saved_method = self.current_foreign_method;
self.current_foreign_class = fcd;
self.current_foreign_method = md;
defer {
self.current_foreign_class = saved_fcd;
self.current_foreign_method = saved_method;
}
const saved_target = self.target_type;
self.target_type = if (ret_ty != .void) ret_ty else null;
if (ret_ty != .void) {