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:
30
examples/ffi-jni-main-03-ctor.sx
Normal file
30
examples/ffi-jni-main-03-ctor.sx
Normal 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; }
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 = ®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("<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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
1
tests/expected/ffi-jni-main-03-ctor.exit
Normal file
1
tests/expected/ffi-jni-main-03-ctor.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
tests/expected/ffi-jni-main-03-ctor.txt
Normal file
1
tests/expected/ffi-jni-main-03-ctor.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user