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

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