ffi #jni_main: Alias.new(args) constructor dispatch via JNI NewObject

Adds the constructor-invocation arm of the foreign-class DSL:
`SurfaceView.new(ctx)` (where `SurfaceView` is a `#foreign #jni_class`
with `static new :: (ctx: *Context) -> *Self;`) lowers to
`FindClass(env, "android/view/SurfaceView") + GetMethodID(env, cls,
"<init>", "(args)V") + NewObject(env, cls, mid, args...)`. Returns
the fresh jobject.

  - inst.zig: `JniMsgSend.is_constructor` flag + `parent_class_path`
    re-purposed to carry the class being constructed (alongside its
    existing nonvirtual-super-class use). Mutually exclusive with
    `is_static` / `is_nonvirtual`.
  - lower.zig: `lowerCall.field_access` arm now recognises
    `Alias.method(args)` where `Alias` resolves in `foreign_class_map`
    and the matching member is `static`. `new` routes to a new
    `lowerForeignStaticCall` that derives a `(args)V` JNI descriptor
    and emits a `JniMsgSend` with `is_constructor=true`. Non-`new`
    static calls report a clear "use #jni_static_call" diagnostic
    until that sugar lands.
  - emit_llvm.zig: new `NewObject` vtable slot (28) + `emitJniConstructor`
    helper expanding the FindClass+GetMethodID+NewObject chain. The
    jni_msg_send arm short-circuits to it when `is_constructor` is set.

Smoke `ffi-jni-main-03-ctor.sx` exercises both this slice and the
previous super-dispatch slice in a single `onCreate` body: calls
`super.onCreate(b)` then constructs a `SurfaceView` with the Activity
as Context. IR shows the expected six-stage chain (FindClass+GetMethodID+
CallNonvirtual + FindClass+GetMethodID+NewObject); APK builds clean.

Naming caveat: the Java type `android.content.Context` clashes with
sx stdlib's `Context :: struct {...}` (heap-context). The smoke aliases
it `JContext` — future work could add a path-prefix or `as` rename
form on `#jni_class` to avoid the manual rename.

133 host / 6 cross / zig build test all green.
This commit is contained in:
agra
2026-05-20 17:14:51 +03:00
parent 36f40057f7
commit c02b6b3b1b
7 changed files with 202 additions and 3 deletions

View File

@@ -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("<init>", 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; }

View File

@@ -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;
// Call<Type>Method (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[CallStatic<T>Method](env, target, mid, args...)
// ctor: cls = ifs[FindClass](env, parent_class_path)
// mid = ifs[GetMethodID](env, cls, "<init>", sig)
// ifs[NewObject](env, cls, mid, args...) → jobject
// nonvirt: handled below via FindClass + GetMethodID +
// CallNonvirtual<T>Method.
// 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,
/// "<init>", 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

View File

@@ -332,10 +332,15 @@ pub const JniMsgSend = struct {
/// `#jni_main` Activity method body — lowers to `CallNonvirtual<T>Method`
/// 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("<init>", 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,
};

View File

@@ -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("<init>", "(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 = &registry,
}, 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("<init>");
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("<init>") +
// 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;

View File

@@ -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("<init>") + NewObject. Compile-only — runtime via chess.
"android|examples/ffi-jni-main-03-ctor.sx"
)
PASS=0

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@