diff --git a/examples/ffi-jni-main-02-super.sx b/examples/ffi-jni-main-02-super.sx new file mode 100644 index 0000000..5893bf6 --- /dev/null +++ b/examples/ffi-jni-main-02-super.sx @@ -0,0 +1,24 @@ +// `super.method(args)` dispatch inside a `#jni_main` Activity body +// (chess-on-Pixel migration, R.6). The override of a lifecycle method +// like `onCreate` needs to invoke the parent's `onCreate` so the +// Android runtime's setup completes (`SuperNotCalledException` +// otherwise) — the sx-side body calls `super.onCreate(b)` and the +// compiler lowers it to JNI `CallNonvirtualVoidMethod` against the +// parent class declared via `#extends`. +// +// No `#extends` here → defaults to `android.app.Activity`. The smoke +// is compile-only — runtime correctness is verified by APK install +// + on-device launch, which is the chess deploy. + +#import "modules/std.sx"; +#import "modules/compiler.sx"; + +Bundle :: #foreign #jni_class("android/os/Bundle") { } + +SxApp :: #jni_main #jni_class("co/swipelab/sxjnimainsuper/SxApp") { + onCreate :: (self: *Self, b: *Bundle) { + super.onCreate(b); + } +} + +main :: () -> s32 { 0; } diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index e444b99..928567e 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -35,6 +35,7 @@ fn isIdentByte(b: u8) bool { /// ``. 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; + // CallNonvirtualMethod (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; }; // CallMethod: (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, "") 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 diff --git a/src/ir/inst.zig b/src/ir/inst.zig index 00b5918..7c19f39 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -328,6 +328,15 @@ pub const JniMsgSend = struct { sig: Ref, args: []const Ref, is_static: bool, + /// `true` when this is a `super.method(args)` dispatch from inside a + /// `#jni_main` Activity method body — lowers to `CallNonvirtualMethod` + /// against `parent_class_path`. Mutually exclusive with `is_static`. + is_nonvirtual: bool = false, + /// Foreign path of the parent class (e.g. `android/app/Activity`) when + /// `is_nonvirtual` is true. emit_llvm interns a separate + /// `jclass GlobalRef` slot keyed on this path so all nonvirtual calls + /// targeting the same super share one FindClass lookup. + parent_class_path: ?[]const u8 = null, cache_key: ?CacheKey = null, }; diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 9e2daa0..bde366a 100644 --- a/src/ir/lower.zig +++ b/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.(...)` 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 + + /// CallNonvirtualMethod` 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 + // CallNonvirtualMethod 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) { diff --git a/tests/cross_compile.sh b/tests/cross_compile.sh index f69c11e..0e74f7d 100755 --- a/tests/cross_compile.sh +++ b/tests/cross_compile.sh @@ -36,6 +36,10 @@ TUPLES=( # #jni_class(...)` decl must continue to lower + link cleanly for # android even without an APK build (compile-only check). "android|examples/ffi-jni-main-01-emit.sx" + # `super.method(args)` dispatch: lowers to JNI CallNonvirtualVoidMethod + # against the parent class (Activity by default). Compile-only check + # — runtime correctness is verified by on-device chess deploy. + "android|examples/ffi-jni-main-02-super.sx" ) PASS=0 diff --git a/tests/expected/ffi-jni-main-02-super.exit b/tests/expected/ffi-jni-main-02-super.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/ffi-jni-main-02-super.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/ffi-jni-main-02-super.txt b/tests/expected/ffi-jni-main-02-super.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/expected/ffi-jni-main-02-super.txt @@ -0,0 +1 @@ +