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:
157
src/ir/lower.zig
157
src/ir/lower.zig
@@ -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 = ®istry,
|
||||
}, 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) {
|
||||
|
||||
Reference in New Issue
Block a user