From 4b14eeb40f2e4dbbf021f598eeffee8e3a31d045 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 11 Oct 2026 12:24:50 +0200 Subject: [PATCH] classfile: writer honors itf flag for invokestatic on interfaces --- .../src/format/write/code/insns.rs | 3 +- .../src/format/write/code/ops.rs | 13 ++- .../src/format/write/pool/refs.rs | 8 +- .../noxis-classfile/tests/roundtrip/code.rs | 108 ++++++++++++++++++ .../noxis-classfile/tests/roundtrip/main.rs | 1 + 5 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 crates/noxis-classfile/tests/roundtrip/code.rs diff --git a/crates/noxis-classfile/src/format/write/code/insns.rs b/crates/noxis-classfile/src/format/write/code/insns.rs index 50748d1..e7781b1 100644 --- a/crates/noxis-classfile/src/format/write/code/insns.rs +++ b/crates/noxis-classfile/src/format/write/code/insns.rs @@ -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, diff --git a/crates/noxis-classfile/src/format/write/code/ops.rs b/crates/noxis-classfile/src/format/write/code/ops.rs index ab302d6..f8566c6 100644 --- a/crates/noxis-classfile/src/format/write/code/ops.rs +++ b/crates/noxis-classfile/src/format/write/code/ops.rs @@ -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 { + 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)?) } }) diff --git a/crates/noxis-classfile/src/format/write/pool/refs.rs b/crates/noxis-classfile/src/format/write/pool/refs.rs index 5cb44f3..2c93c55 100644 --- a/crates/noxis-classfile/src/format/write/pool/refs.rs +++ b/crates/noxis-classfile/src/format/write/pool/refs.rs @@ -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 { - 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 diff --git a/crates/noxis-classfile/tests/roundtrip/code.rs b/crates/noxis-classfile/tests/roundtrip/code.rs new file mode 100644 index 0000000..a87373c --- /dev/null +++ b/crates/noxis-classfile/tests/roundtrip/code.rs @@ -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" + ); +} diff --git a/crates/noxis-classfile/tests/roundtrip/main.rs b/crates/noxis-classfile/tests/roundtrip/main.rs index 0152923..4e09373 100644 --- a/crates/noxis-classfile/tests/roundtrip/main.rs +++ b/crates/noxis-classfile/tests/roundtrip/main.rs @@ -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;