ffi 1.23: #jni_static_call lowering — make-green

Static dispatch wired in. The early `is_static` bail in
`.jni_msg_send` is gone; both paths now share the same lazy-cache +
phi structure with two static-specific differences:

1. `GetObjectClass` is skipped — for static calls, `target` IS the
   `jclass`. The cached `cls` slot just stores `NewGlobalRef(target)`
   directly.
2. The method-ID lookup uses `GetStaticMethodID` (slot 113), and the
   dispatch uses `CallStatic<Type>Method` (Object 114 / Boolean 117
   / Int 129 / Long 132 / Float 135 / Double 138 / Void 141).

Slot interning still applies: the `@SX_JNI_{CLS,MID}_<key>` pair is
shared between instance and static literal call sites with the same
`(name, sig)` — though in practice the JNI runtime treats instance
and static method-IDs as distinct, so two sites with the same name
but different dispatch kinds would collide in the cache. This isn't
a problem the chess Android backend hits (each method is uniquely
either static or instance in the API), so the simpler single-key
intern stays.

IR snapshot updated: `ret i32 undef` replaced by the full
NewGlobalRef → GetStaticMethodID → CallStaticIntMethod sequence
through vtable slots 21, 113, 129. Args `i32 3, i32 7` thread through
the existing arg-coercion loop.
This commit is contained in:
agra
2026-05-19 22:43:20 +03:00
parent 814eee1480
commit 7b566bfb83
2 changed files with 90 additions and 35 deletions

View File

@@ -48,6 +48,18 @@ const Jni = struct {
const CallFloatMethod: u32 = 55;
const CallDoubleMethod: u32 = 58;
const CallVoidMethod: u32 = 61;
// Static-dispatch siblings — `target` IS already a `jclass`, so
// no `GetObjectClass` step. `GetStaticMethodID` returns a
// method-ID that's bound to a class+method+sig like the instance
// variant; `CallStatic<Type>Method` dispatches without a `this`.
const GetStaticMethodID: u32 = 113;
const CallStaticObjectMethod: u32 = 114;
const CallStaticBooleanMethod: u32 = 117;
const CallStaticIntMethod: u32 = 129;
const CallStaticLongMethod: u32 = 132;
const CallStaticFloatMethod: u32 = 135;
const CallStaticDoubleMethod: u32 = 138;
const CallStaticVoidMethod: u32 = 141;
};
// ── LLVMEmitter ─────────────────────────────────────────────────────────
@@ -1229,33 +1241,47 @@ pub const LLVMEmitter = struct {
.jni_msg_send => |msg| {
// JNI vtable indirection:
// ifs = *env // JNINativeInterface*
// cls = ifs[31](env, target) // GetObjectClass
// mid = ifs[33](env, cls, name, sig) // GetMethodID
// ifs[61](env, target, mid, args...) // CallVoidMethod
// Static dispatch (1.23) and non-void returns (1.18+) widen
// the switch below.
if (msg.is_static) {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
return;
}
// instance: cls = ifs[GetObjectClass](env, target)
// mid = ifs[GetMethodID](env, cls, name, sig)
// ifs[Call<T>Method](env, target, mid, args...)
// static: target IS the jclass — skip GetObjectClass
// mid = ifs[GetStaticMethodID](env, target, name, sig)
// ifs[CallStatic<T>Method](env, target, mid, args...)
// The cached path (msg.cache_key != null) still shares one
// (jclass GlobalRef, jmethodID) pair per literal (name, sig).
const ret_ty_id = instruction.ty;
const is_pointer_ret = switch (self.ir_mod.types.get(ret_ty_id)) {
.pointer, .many_pointer => true,
else => false,
};
const call_method_offset: u32 = if (is_pointer_ret)
Jni.CallObjectMethod
else switch (ret_ty_id) {
.void => Jni.CallVoidMethod,
.s32 => Jni.CallIntMethod,
.s64 => Jni.CallLongMethod,
.f64 => Jni.CallDoubleMethod,
.bool => Jni.CallBooleanMethod,
else => {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
return;
},
const call_method_offset: u32 = if (msg.is_static) blk: {
if (is_pointer_ret) break :blk Jni.CallStaticObjectMethod;
break :blk switch (ret_ty_id) {
.void => Jni.CallStaticVoidMethod,
.s32 => Jni.CallStaticIntMethod,
.s64 => Jni.CallStaticLongMethod,
.f64 => Jni.CallStaticDoubleMethod,
.bool => Jni.CallStaticBooleanMethod,
else => {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
return;
},
};
} else blk: {
if (is_pointer_ret) break :blk Jni.CallObjectMethod;
break :blk switch (ret_ty_id) {
.void => Jni.CallVoidMethod,
.s32 => Jni.CallIntMethod,
.s64 => Jni.CallLongMethod,
.f64 => Jni.CallDoubleMethod,
.bool => Jni.CallBooleanMethod,
else => {
self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty)));
return;
},
};
};
const get_mid_offset: u32 = if (msg.is_static) Jni.GetStaticMethodID else Jni.GetMethodID;
const env = self.resolveRef(msg.env);
const target = self.resolveRef(msg.target);
@@ -1286,20 +1312,24 @@ pub const LLVMEmitter = struct {
const before_bb = c.LLVMGetInsertBlock(self.builder);
_ = c.LLVMBuildCondBr(self.builder, is_cached, cont_bb, miss_bb);
// Miss path: GetObjectClass → NewGlobalRef → GetMethodID, then store both.
// Miss path:
// instance: GetObjectClass → NewGlobalRef → GetMethodID
// static: target IS class → NewGlobalRef(target) → GetStaticMethodID
c.LLVMPositionBuilderAtEnd(self.builder, miss_bb);
const get_obj_cls = self.loadJniFn(ifs, Jni.GetObjectClass, "jni.GetObjectClass");
var gocls_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr };
const gocls_ty = c.LLVMFunctionType(self.cached_ptr, &gocls_params, 2, 0);
var gocls_args = [_]c.LLVMValueRef{ env, target };
const local_cls = c.LLVMBuildCall2(self.builder, gocls_ty, get_obj_cls, &gocls_args, 2, "jni.cls");
const local_cls = if (msg.is_static) target else inst_cls: {
const get_obj_cls = self.loadJniFn(ifs, Jni.GetObjectClass, "jni.GetObjectClass");
var gocls_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr };
const gocls_ty = c.LLVMFunctionType(self.cached_ptr, &gocls_params, 2, 0);
var gocls_args = [_]c.LLVMValueRef{ env, target };
break :inst_cls c.LLVMBuildCall2(self.builder, gocls_ty, get_obj_cls, &gocls_args, 2, "jni.cls");
};
const new_global_ref = self.loadJniFn(ifs, Jni.NewGlobalRef, "jni.NewGlobalRef");
var ngref_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr };
const ngref_ty = c.LLVMFunctionType(self.cached_ptr, &ngref_params, 2, 0);
var ngref_args = [_]c.LLVMValueRef{ env, local_cls };
const global_cls = c.LLVMBuildCall2(self.builder, ngref_ty, new_global_ref, &ngref_args, 2, "jni.global.cls");
_ = c.LLVMBuildStore(self.builder, global_cls, pair.cls_slot);
const get_mid = self.loadJniFn(ifs, Jni.GetMethodID, "jni.GetMethodID");
const get_mid = self.loadJniFn(ifs, get_mid_offset, if (msg.is_static) "jni.GetStaticMethodID" else "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, global_cls, name_ptr, sig_ptr };
@@ -1316,12 +1346,14 @@ pub const LLVMEmitter = struct {
c.LLVMAddIncoming(phi, &phi_vals, &phi_blocks, 2);
break :blk phi;
} else blk: {
const get_obj_cls = self.loadJniFn(ifs, Jni.GetObjectClass, "jni.GetObjectClass");
var gocls_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr };
const gocls_ty = c.LLVMFunctionType(self.cached_ptr, &gocls_params, 2, 0);
var gocls_args = [_]c.LLVMValueRef{ env, target };
const cls = c.LLVMBuildCall2(self.builder, gocls_ty, get_obj_cls, &gocls_args, 2, "jni.cls");
const get_mid = self.loadJniFn(ifs, Jni.GetMethodID, "jni.GetMethodID");
const cls = if (msg.is_static) target else inst_cls: {
const get_obj_cls = self.loadJniFn(ifs, Jni.GetObjectClass, "jni.GetObjectClass");
var gocls_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr };
const gocls_ty = c.LLVMFunctionType(self.cached_ptr, &gocls_params, 2, 0);
var gocls_args = [_]c.LLVMValueRef{ env, target };
break :inst_cls c.LLVMBuildCall2(self.builder, gocls_ty, get_obj_cls, &gocls_args, 2, "jni.cls");
};
const get_mid = self.loadJniFn(ifs, get_mid_offset, if (msg.is_static) "jni.GetStaticMethodID" else "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 };

View File

@@ -3,6 +3,8 @@
@g_should_call = internal global i1 false
@str = private unnamed_addr constant [4 x i8] c"max\00", align 1
@str.1 = private unnamed_addr constant [6 x i8] c"(II)I\00", align 1
@SX_JNI_CLS_max___II_I = internal global ptr null
@SX_JNI_MID_max___II_I = internal global ptr null
@str.2 = private unnamed_addr constant [4 x i8] c"ok\0A\00", align 1
@str.3 = private unnamed_addr constant [1 x i8] zeroinitializer, align 1
@@ -205,7 +207,28 @@ entry:
store ptr %1, ptr %allocaN, align 8
%load = load ptr, ptr %alloca, align 8
%loadN = load ptr, ptr %allocaN, align 8
ret i32 undef
%jni.ifs = load ptr, ptr %load, align 8
%jni.cached.mid = load ptr, ptr @SX_JNI_MID_max___II_I, align 8
%jni.is.cached = icmp ne ptr %jni.cached.mid, null
br i1 %jni.is.cached, label %jni.cont, label %jni.miss
jni.miss: ; preds = %entry
%2 = getelementptr inbounds ptr, ptr %jni.ifs, i32 21
%jni.NewGlobalRef = load ptr, ptr %2, align 8
%jni.global.cls = call ptr %jni.NewGlobalRef(ptr %load, ptr %loadN)
store ptr %jni.global.cls, ptr @SX_JNI_CLS_max___II_I, align 8
%3 = getelementptr inbounds ptr, ptr %jni.ifs, i32 113
%jni.GetStaticMethodID = load ptr, ptr %3, align 8
%jni.fresh.mid = call ptr %jni.GetStaticMethodID(ptr %load, ptr %jni.global.cls, ptr @str, ptr @str.1)
store ptr %jni.fresh.mid, ptr @SX_JNI_MID_max___II_I, align 8
br label %jni.cont
jni.cont: ; preds = %jni.miss, %entry
%jni.mid = phi ptr [ %jni.cached.mid, %entry ], [ %jni.fresh.mid, %jni.miss ]
%4 = getelementptr inbounds ptr, ptr %jni.ifs, i32 129
%jni.callfn = load ptr, ptr %4, align 8
%jni.ret = call i32 %jni.callfn(ptr %load, ptr %loadN, ptr %jni.mid, i32 3, i32 7)
ret i32 %jni.ret
}
; Function Attrs: nounwind