classfile: writer honors itf flag for invokestatic on interfaces

This commit is contained in:
Loki 2026-10-11 12:24:50 +02:00
parent 5901c9eb69
commit 4b14eeb40f
Signed by: boba
GPG key ID: 253067914055423B
5 changed files with 123 additions and 10 deletions

View file

@ -96,8 +96,9 @@ pub(crate) fn convert(
owner,
name,
descriptor,
itf,
..
} => invoke(*kind, owner, name, descriptor, pool)?,
} => invoke(*kind, owner, name, descriptor, *itf, pool)?,
Insn::InvokeDynamic {
name,
descriptor,

View file

@ -166,23 +166,28 @@ pub(super) fn convert_op(op: Convert) -> RtInsn {
}
}
/// `invoke*` family: the `itf` flag is authoritative (a Static/Special
/// call may still target an interface method - `List.of`), so the pool
/// entry kind follows it, not the opcode alone.
pub(super) fn invoke(
kind: InvokeKind,
owner: &str,
name: &str,
descriptor: &str,
itf: bool,
pool: &mut Pool,
) -> Result<RtInsn, ClassfileError> {
let itf = itf || matches!(kind, InvokeKind::Interface);
Ok(match kind {
InvokeKind::Virtual => {
RtInsn::Invokevirtual(pool.method_ref(owner, name, descriptor, kind)?)
RtInsn::Invokevirtual(pool.method_ref(owner, name, descriptor, itf)?)
}
InvokeKind::Special => {
RtInsn::Invokespecial(pool.method_ref(owner, name, descriptor, kind)?)
RtInsn::Invokespecial(pool.method_ref(owner, name, descriptor, itf)?)
}
InvokeKind::Static => RtInsn::Invokestatic(pool.method_ref(owner, name, descriptor, kind)?),
InvokeKind::Static => RtInsn::Invokestatic(pool.method_ref(owner, name, descriptor, itf)?),
InvokeKind::Interface => {
let index = pool.method_ref(owner, name, descriptor, kind)?;
let index = pool.method_ref(owner, name, descriptor, itf)?;
RtInsn::Invokeinterface(index, arg_count(descriptor)?)
}
})

View file

@ -9,7 +9,6 @@
use ristretto_classfile::{Constant, ReferenceKind};
use crate::error::ClassfileError;
use crate::insn::InvokeKind;
use super::{Pool, write_err};
@ -42,9 +41,8 @@ impl Pool {
owner: &str,
name: &str,
descriptor: &str,
kind: InvokeKind,
itf: bool,
) -> Result<u16, ClassfileError> {
let itf = matches!(kind, InvokeKind::Interface);
let key = (
owner.to_string(),
name.to_string(),
@ -94,7 +92,7 @@ impl Pool {
ClassfileError::Write(format!("некорректный reference_kind {kind}: {e}"))
})?;
let reference = if itf {
self.method_ref(owner, name, descriptor, InvokeKind::Interface)?
self.method_ref(owner, name, descriptor, true)?
} else if matches!(
reference_kind,
ReferenceKind::GetField
@ -104,7 +102,7 @@ impl Pool {
) {
self.field_ref(owner, name, descriptor)?
} else {
self.method_ref(owner, name, descriptor, InvokeKind::Static)?
self.method_ref(owner, name, descriptor, false)?
};
let index = self
.pool

View file

@ -0,0 +1,108 @@
//! Code-level round trips that the IR must survive.
use noxis_classfile::Insn;
use noxis_classfile::format;
use ristretto_classfile::attributes::{Attribute as RtAttr, Instruction as Rt};
use ristretto_classfile::{
ClassAccessFlags, ClassFile as RtClass, ConstantPool, JAVA_21, Method as RtMethod,
MethodAccessFlags,
};
/// `invokestatic` on an *interface* method (`List.of`) needs a
/// `CONSTANT_InterfaceMethodref`: writing a plain `Methodref` passes the
/// verifier's structural check but HotSpot rejects the linkage with
/// `IncompatibleClassChangeError` at first call.
#[test]
fn invokestatic_interface_method_ref_round_trip() {
let mut pool = ConstantPool::default();
let this = pool.add_class("demo/IfStatic").expect("this");
let object = pool.add_class("java/lang/Object").expect("object");
let name = pool.add_utf8("main").expect("name");
let desc = pool.add_utf8("([Ljava/lang/String;)V").expect("desc");
let code_name = pool.add_utf8("Code").expect("code name");
let list_class = pool.add_class("java/util/List").expect("list");
let of = pool
.add_interface_method_ref(
list_class,
"of",
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/util/List;",
)
.expect("interface ref");
let code = vec![
Rt::Aload_0,
Rt::Aload_0,
Rt::Invokestatic(of),
Rt::Pop,
Rt::Return,
];
let rt = RtClass {
version: JAVA_21,
constant_pool: pool,
access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::SUPER,
this_class: this,
super_class: object,
interfaces: vec![],
fields: vec![],
methods: vec![RtMethod {
access_flags: MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC,
name_index: name,
descriptor_index: desc,
attributes: vec![RtAttr::Code {
name_index: code_name,
max_stack: 2,
max_locals: 1,
code,
exception_table: vec![],
attributes: vec![],
}],
}],
attributes: vec![],
..RtClass::default()
};
let mut original = Vec::new();
rt.to_bytes(&mut original).expect("to_bytes");
let parsed = format::parse(&original).expect("parse");
let method = &parsed.methods[0];
let invoke = method
.code
.as_ref()
.expect("code")
.insns
.iter()
.find_map(|i| match i {
Insn::Invoke {
kind, owner, itf, ..
} => Some((*kind, owner.clone(), *itf)),
_ => None,
})
.expect("invokestatic");
assert!(invoke.2, "parse must keep the interface flag");
let written = format::write(&parsed).expect("write");
let reparsed = format::parse(&written).expect("re-parse");
let method = &reparsed.methods[0];
let invoke = method
.code
.as_ref()
.expect("code")
.insns
.iter()
.find_map(|i| match i {
Insn::Invoke {
kind, owner, itf, ..
} => Some((*kind, owner.clone(), *itf)),
_ => None,
})
.expect("invokestatic");
assert_eq!(
invoke,
(
noxis_classfile::InvokeKind::Static,
"java/util/List".to_string(),
true
),
"InterfaceMethodref must survive write"
);
}

View file

@ -169,3 +169,4 @@ fn round_trip(rt: &RtClass<'_>) -> ClassFile {
/// survive the round trip: HotSpot tolerates it, so we normalize on parse
/// instead of dying in the backend's strict verifier on write.
mod attrs;
mod code;