diff --git a/examples/ffi-jni-main-03-ctor.sx b/examples/ffi-jni-main-03-ctor.sx new file mode 100644 index 0000000..d922971 --- /dev/null +++ b/examples/ffi-jni-main-03-ctor.sx @@ -0,0 +1,30 @@ +// `Alias.new(args)` constructor dispatch on a `#foreign #jni_class` +// (chess-on-Pixel migration, R.6). The sx-side `static new :: (...) -> +// *Self;` member lowers to JNI `FindClass + GetMethodID("", sig) +// + NewObject(env, clazz, mid, args...)`. +// +// This smoke instantiates a `SurfaceView` from inside the Activity's +// `onCreate` body — chess's render surface starts the same way. + +#import "modules/std.sx"; +#import "modules/compiler.sx"; + +Bundle :: #foreign #jni_class("android/os/Bundle") { } +JContext :: #foreign #jni_class("android/content/Context") { } + +SurfaceView :: #foreign #jni_class("android/view/SurfaceView") { + static new :: (ctx: *JContext) -> *Self; +} + +g_held_view : *void = null; + +SxApp :: #jni_main #jni_class("co/swipelab/sxjnictor/SxApp") { + onCreate :: (self: *Self, b: *Bundle) { + super.onCreate(b); + ctx : *JContext = xx self; // Activity IS a JContext (extends JContext). + view := SurfaceView.new(ctx); + g_held_view = xx view; // keep alive so LLVM doesn't DCE the construction. + } +} + +main :: () -> s32 { 0; } diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 928567e..53d203e 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -37,6 +37,7 @@ fn isIdentByte(b: u8) bool { const Jni = struct { const FindClass: u32 = 6; const NewGlobalRef: u32 = 21; + const NewObject: u32 = 28; const GetObjectClass: u32 = 31; const GetMethodID: u32 = 33; // CallMethod (instance, varargs variant). Each numeric type @@ -1264,8 +1265,17 @@ pub const LLVMEmitter = struct { // static: target IS the jclass — skip GetObjectClass // mid = ifs[GetStaticMethodID](env, target, name, sig) // ifs[CallStaticMethod](env, target, mid, args...) + // ctor: cls = ifs[FindClass](env, parent_class_path) + // mid = ifs[GetMethodID](env, cls, "", sig) + // ifs[NewObject](env, cls, mid, args...) → jobject + // nonvirt: handled below via FindClass + GetMethodID + + // CallNonvirtualMethod. // The cached path (msg.cache_key != null) still shares one // (jclass GlobalRef, jmethodID) pair per literal (name, sig). + if (msg.is_constructor) { + self.emitJniConstructor(msg, instruction.ty); + return; + } const ret_ty_id = instruction.ty; const is_pointer_ret = switch (self.ir_mod.types.get(ret_ty_id)) { .pointer, .many_pointer => true, @@ -3640,6 +3650,56 @@ pub const LLVMEmitter = struct { return c.LLVMBuildGlobalStringPtr(self.builder, z.ptr, name); } + /// Expand a JNI constructor dispatch (`Foo.new(args)` in sx). Chain: + /// `FindClass(env, parent_class_path)` → `GetMethodID(env, clazz, + /// "", sig)` → `NewObject(env, clazz, mid, args...)`. Returns + /// the new jobject. Per-call lookups — no caching yet. + fn emitJniConstructor(self: *LLVMEmitter, msg: ir_inst.JniMsgSend, ret_ty_id: TypeId) void { + const env = self.resolveRef(msg.env); + const sig_ptr = self.extractSlicePtr(self.resolveRef(msg.sig)); + const name_ptr = self.extractSlicePtr(self.resolveRef(msg.name)); + + const ifs = c.LLVMBuildLoad2(self.builder, self.cached_ptr, env, "jni.ifs"); + + const path = msg.parent_class_path orelse ""; + const path_global = self.emitCStringGlobal(path, "jni.ctor.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 }; + const cls = c.LLVMBuildCall2(self.builder, fc_ty, find_class, &fc_args, 2, "jni.ctor.cls"); + + const get_mid = self.loadJniFn(ifs, Jni.GetMethodID, "jni.GetMethodID"); + 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 }; + const mid = c.LLVMBuildCall2(self.builder, gmid_ty, get_mid, &gmid_args, 4, "jni.ctor.mid"); + + const new_object = self.loadJniFn(ifs, Jni.NewObject, "jni.NewObject"); + const raw_ret = self.toLLVMType(ret_ty_id); + const total_call_params: usize = 3 + msg.args.len; + const call_param_types = self.alloc.alloc(c.LLVMTypeRef, total_call_params) catch unreachable; + defer self.alloc.free(call_param_types); + const call_args = self.alloc.alloc(c.LLVMValueRef, total_call_params) catch unreachable; + defer self.alloc.free(call_args); + call_param_types[0] = self.cached_ptr; + call_param_types[1] = self.cached_ptr; + call_param_types[2] = self.cached_ptr; + call_args[0] = env; + call_args[1] = cls; + call_args[2] = mid; + 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[i + 3] = coerced_ty; + call_args[i + 3] = self.coerceArg(self.resolveRef(arg_ref), coerced_ty); + } + const call_fn_ty = c.LLVMFunctionType(raw_ret, call_param_types.ptr, @intCast(total_call_params), 0); + const result = c.LLVMBuildCall2(self.builder, call_fn_ty, new_object, call_args.ptr, @intCast(total_call_params), "jni.new.obj"); + self.mapRef(result); + } + // ── 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 7c19f39..ab5afbf 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -332,10 +332,15 @@ pub const JniMsgSend = struct { /// `#jni_main` Activity method body — lowers to `CallNonvirtualMethod` /// against `parent_class_path`. Mutually exclusive with `is_static`. is_nonvirtual: bool = false, + /// `true` when this is a `Foo.new(args)` constructor dispatch — lowers + /// to `FindClass(parent_class_path) + GetMethodID("", sig) + + /// NewObject(env, clazz, mid, args...)`. Returns a fresh jobject. + /// Mutually exclusive with the other dispatch flags. + is_constructor: 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. + /// `is_nonvirtual` is true, OR of the class being constructed when + /// `is_constructor` is true. emit_llvm uses `FindClass` to materialise + /// the jclass at the call site (per-call; caching is follow-up). parent_class_path: ?[]const u8 = null, cache_key: ?CacheKey = null, }; diff --git a/src/ir/lower.zig b/src/ir/lower.zig index bde366a..369d7cd 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -4083,6 +4083,88 @@ pub const Lowering = struct { } }, ret_ty); } + /// Lower `Alias.new(args)` where `Alias` is a foreign-class identifier + /// with `static new :: (...) -> *Self;` — JNI constructor dispatch: + /// `FindClass + GetMethodID("", "(args)V") + NewObject(env, + /// clazz, mid, args...)`. Returns the new jobject. + /// + /// Non-`new` static methods aren't supported via this path yet — the + /// user can use `#jni_static_call(T)(class, "name", sig, args...)` + /// for those. Constructor is the common case for #jni_main bodies + /// that need to instantiate Android classes (SurfaceView, etc.). + fn lowerForeignStaticCall( + self: *Lowering, + fcd: *const ast.ForeignClassDecl, + method: ast.ForeignMethodDecl, + method_args: []const Ref, + span: ast.Span, + ) Ref { + if (fcd.runtime != .jni_class and fcd.runtime != .jni_interface) { + if (self.diagnostics) |d| d.addFmt(.err, span, "static calls on '{s}' runtime not yet supported (Phase 3/4)", .{@tagName(fcd.runtime)}); + return Ref.none; + } + if (!std.mem.eql(u8, method.name, "new")) { + if (self.diagnostics) |d| d.addFmt(.err, span, "static foreign-class call '{s}.{s}' not yet supported via `Alias.method()` syntax \u{2014} only `new` is wired today; use `#jni_static_call` directly for other static methods", .{ fcd.name, method.name }); + return Ref.none; + } + + if (self.jni_env_stack.items.len <= self.jni_env_stack_base) { + if (self.diagnostics) |d| d.addFmt(.err, span, "constructor `{s}.new(...)` requires an enclosing `#jni_env` scope (or `#jni_main` body)", .{fcd.name}); + return Ref.none; + } + const env_ref = self.jni_env_stack.items[self.jni_env_stack.items.len - 1]; + + // Build class registry snapshot for `*Foo` cross-class refs. + 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 {}; + } + + // For `new`, the JNI descriptor's return position is `V` (the + // constructor returns void; the new jobject comes back from + // `NewObject` itself). Patch the AST by overriding return_type + // to null during derivation. + const m_for_desc: ast.ForeignMethodDecl = .{ + .name = method.name, + .params = method.params, + .param_names = method.param_names, + .return_type = null, + .is_static = method.is_static, + .jni_descriptor_override = method.jni_descriptor_override, + .body = method.body, + }; + + const descriptor = jni_descriptor.deriveMethod(self.alloc, .{ + .enclosing_path = fcd.foreign_path, + .classes = ®istry, + }, m_for_desc) catch |err| { + if (self.diagnostics) |d| d.addFmt(.err, span, "JNI descriptor derivation failed for '{s}.new': {s}", .{ fcd.name, @errorName(err) }); + return Ref.none; + }; + + const ret_ty = self.module.types.ptrTo(.void); // jobject + + const name_sid = self.module.types.internString(""); + 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 args_owned = self.alloc.dupe(Ref, method_args) catch unreachable; + return self.builder.emit(.{ .jni_msg_send = .{ + .env = env_ref, + .target = Ref.none, // unused for ctor — class is resolved via parent_class_path + .name = name_ref, + .sig = sig_ref, + .args = args_owned, + .is_static = false, + .is_constructor = true, + .parent_class_path = self.alloc.dupe(u8, fcd.foreign_path) catch fcd.foreign_path, + .cache_key = null, + } }, 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`) @@ -4556,6 +4638,23 @@ pub const Lowering = struct { return self.lowerSuperCall(fa.field, args.items, c.callee.span); } + // `Alias.method(args)` where Alias is a foreign-class + // identifier and `method` is a `static` member — JNI + // dispatch via FindClass + GetStaticMethodID + CallStatic*, + // OR (for `new`) via FindClass + GetMethodID("") + + // NewObject. Falls through to existing paths when no match. + if (fa.object.data == .identifier) { + const alias = fa.object.data.identifier.name; + if (self.foreign_class_map.get(alias)) |fcd| { + for (fcd.members) |m| switch (m) { + .method => |md| if (md.is_static and std.mem.eql(u8, md.name, fa.field)) { + return self.lowerForeignStaticCall(fcd, md, args.items, c.callee.span); + }, + else => {}, + }; + } + } + // Pattern-match context.allocator.alloc/dealloc → heap_alloc/heap_free if (self.matchContextAllocCall(fa, args.items)) |ref| return ref; diff --git a/tests/cross_compile.sh b/tests/cross_compile.sh index 0e74f7d..d0def07 100755 --- a/tests/cross_compile.sh +++ b/tests/cross_compile.sh @@ -40,6 +40,9 @@ TUPLES=( # 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" + # `Alias.new(args)` constructor dispatch: lowers to FindClass + + # GetMethodID("") + NewObject. Compile-only — runtime via chess. + "android|examples/ffi-jni-main-03-ctor.sx" ) PASS=0 diff --git a/tests/expected/ffi-jni-main-03-ctor.exit b/tests/expected/ffi-jni-main-03-ctor.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/ffi-jni-main-03-ctor.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/ffi-jni-main-03-ctor.txt b/tests/expected/ffi-jni-main-03-ctor.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/expected/ffi-jni-main-03-ctor.txt @@ -0,0 +1 @@ +