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

@@ -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;