From 5f91e617be440035ebea2d2b0fdcf18d637ba117 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 13 Jun 2026 08:18:21 +0900 Subject: [PATCH 01/10] Add class initialization state machine ensure_initialized guards instantiate/getstatic/putstatic/invokestatic behind an InitState machine with InProgress re-entry, runs clinit through execute_method so it gets a java frame, and canonicalizes resolve results to the registry Class so init state is shared. Registration still initializes eagerly; the lazy flip comes separately. --- jvm/src/class_loader.rs | 23 ++++++++++-- jvm/src/jvm.rs | 70 ++++++++++++++++++++++++++++++++--- test_data/unit/Counter.class | Bin 0 -> 331 bytes test_data/unit/Counter.java | 9 +++++ test_data/unit/SelfRef.class | Bin 0 -> 415 bytes test_data/unit/SelfRef.java | 10 +++++ tests/test_lazy_clinit.rs | 34 +++++++++++++++++ 7 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 test_data/unit/Counter.class create mode 100644 test_data/unit/Counter.java create mode 100644 test_data/unit/SelfRef.class create mode 100644 test_data/unit/SelfRef.java create mode 100644 tests/test_lazy_clinit.rs diff --git a/jvm/src/class_loader.rs b/jvm/src/class_loader.rs index a601594e..7e3c096e 100644 --- a/jvm/src/class_loader.rs +++ b/jvm/src/class_loader.rs @@ -7,10 +7,19 @@ use crate::{ runtime::{JavaLangClass, JavaLangClassLoader}, }; +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum InitState { + NotInitialized, + InProgress, + Initialized, + Erroneous, +} + #[derive(Clone)] pub struct Class { pub definition: Box, java_class: Arc>>>, + init_state: Arc>, } impl Class { @@ -18,9 +27,18 @@ impl Class { Self { definition, java_class: Arc::new(RwLock::new(java_class)), + init_state: Arc::new(RwLock::new(InitState::NotInitialized)), } } + pub(crate) fn init_state(&self) -> InitState { + *self.init_state.read() + } + + pub(crate) fn set_init_state(&self, state: InitState) { + *self.init_state.write() = state; + } + pub fn set_java_class(&self, java_class: Box) { *self.java_class.write() = Some(java_class); } @@ -81,10 +99,7 @@ impl ClassLoaderWrapper for JavaClassLoaderWrapper { if let Some(class) = class { let definition = JavaLangClass::to_rust_class(jvm, &class).await?; - Ok(Some(Class { - definition, - java_class: Arc::new(RwLock::new(Some(class))), - })) + Ok(Some(Class::new(definition, Some(class)))) } else { Ok(None) } diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 92afa01d..aa738731 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -20,7 +20,7 @@ use crate::{ array_class_instance::{ArrayClassInstance, ArrayRawBuffer, ArrayRawBufferMut}, class_definition::ClassDefinition, class_instance::ClassInstance, - class_loader::{BootstrapClassLoader, BootstrapClassLoaderWrapper, Class, ClassLoaderWrapper, JavaClassLoaderWrapper}, + class_loader::{BootstrapClassLoader, BootstrapClassLoaderWrapper, Class, ClassLoaderWrapper, InitState, JavaClassLoaderWrapper}, error::JavaError, field::Field, garbage_collector::determine_garbage, @@ -125,6 +125,8 @@ impl Jvm { .await); } + self.ensure_initialized(&class).await?; + let instance = class.definition.instantiate(self).await?; let thread_id = (self.inner.get_current_thread_id)(); @@ -178,6 +180,8 @@ impl Jvm { let field = class.definition.field(name, descriptor, true); if let Some(field) = field { + self.ensure_initialized(&class).await?; + Ok(class.definition.get_static_field(&*field)?.into()) } else { Err(self @@ -197,6 +201,8 @@ impl Jvm { let field = class.definition.field(name, descriptor, true); if let Some(field) = field { + self.ensure_initialized(&class).await?; + class.definition.put_static_field(&*field, value.into()) } else { Err(self @@ -265,6 +271,8 @@ impl Jvm { .await); } + self.ensure_initialized(&class).await?; + Ok(self.execute_method(&class, None, &method, args).await?.into()) } else { tracing::error!("No such method: {}.{}:{}", class_name, name, descriptor); @@ -513,7 +521,14 @@ impl Jvm { &JavaClassLoaderWrapper::new(self.current_class_loader().await?) }; - self.load_class(class_name, class_loader_wrapper).await + let class = self.load_class(class_name, class_loader_wrapper).await?; + + // wrapper가 만든 별도 Class를 그대로 쓰면 init 상태가 공유되지 않으므로 레지스트리에 등록된 canonical 본을 반환 + if let Some(registered) = self.get_class(class_name) { + return Ok(registered); + } + + Ok(class) } async fn load_class(&self, class_name: &str, class_loader_wrapper: &dyn ClassLoaderWrapper) -> Result { @@ -665,14 +680,59 @@ impl Jvm { self.inner.classes.write().insert(class.definition.name().to_owned(), class.clone()); - let clinit = class.definition.method("", "()V", true); + self.ensure_initialized(&class).await?; + + Ok(()) + } + + #[async_recursion::async_recursion] + async fn ensure_initialized(&self, class: &Class) -> Result<()> { + if class.definition.name().starts_with('[') { + return Ok(()); + } + + match class.init_state() { + InitState::Initialized | InitState::InProgress => return Ok(()), + InitState::Erroneous => { + return Err(self + .exception( + "java/lang/NoClassDefFoundError", + &format!("Could not initialize class {}", class.definition.name()), + ) + .await); + } + InitState::NotInitialized => {} + } + + class.set_init_state(InitState::InProgress); - if let Some(x) = clinit { + if let Some(super_name) = class.definition.super_class_name() { + // resolve 실패는 초기화 실패가 아님 — 재시도 가능 상태로 되돌린다 + let super_class = match self.resolve_class(&super_name).await { + Ok(x) => x, + Err(err) => { + class.set_init_state(InitState::NotInitialized); + return Err(err); + } + }; + + if let Err(err) = self.ensure_initialized(&super_class).await { + class.set_init_state(InitState::Erroneous); + return Err(err); + } + } + + if let Some(clinit) = class.definition.method("", "()V", true) { tracing::debug!("Calling for {}", class.definition.name()); - x.run(self, Box::new([])).await?; + if let Err(err) = self.execute_method(class, None, &clinit, Box::new([])).await { + class.set_init_state(InitState::Erroneous); + return Err(err); + } } + class.set_init_state(InitState::Initialized); + Ok(()) } diff --git a/test_data/unit/Counter.class b/test_data/unit/Counter.class new file mode 100644 index 0000000000000000000000000000000000000000..9b313d28d46e24c925bfda7119bf6714ec1458f5 GIT binary patch literal 331 zcmZWlO>4qH6r7htH%a4HJ@p`X5o!4PrrWv#%Q}RU@BN1DyR~gTmQ=+#D20Kyaihw=7j1nN}_y3FrWKN z2R0l9*TVxmf|{g7lIx7%h^V{~XhyIm=}HqC^C;1aVi)M_!w+I+DtTIj8-hIy0<>y>$c^9^Nv{y^Pa<*y{%F2yaz1|H8~N-xO( literal 0 HcmV?d00001 diff --git a/test_data/unit/Counter.java b/test_data/unit/Counter.java new file mode 100644 index 00000000..d437f84b --- /dev/null +++ b/test_data/unit/Counter.java @@ -0,0 +1,9 @@ +class Counter { + static int initCount; + + static { + initCount++; + } + + static void touch() {} +} diff --git a/test_data/unit/SelfRef.class b/test_data/unit/SelfRef.class new file mode 100644 index 0000000000000000000000000000000000000000..7ff2e8a294824e5e314dd2a4a85f945d064309cc GIT binary patch literal 415 zcmZWlO-sW-5Pg%R*)*;;ep^BD*m|&kpa>!e1yQi#*-hMPYD}ag{wgopgW|y-;Exh# zLaCq&v-8;Z=FQB0eSdrcIKp-a2d;vr(ST2g#^%ZNGLzr-&f~F7N`l`{^Rzr9xSj4* z69JkEp~eC=_QR8B7*m5?R{M}kg64pRVg9E)NyfR39ZvKw`bKu zY$ul{&iJKK7FBXb2>MAj>lzM=YLeKKRCqn>cOYY8k8K@Z!{N-{i~<>pv&X^vfUw4B zhcn5`{u}6p0b!kQp#=vrFK3bN)`rZzLV4HEf3!YpH|Dl&X65{|lyx`fvQFIw(C=8B WVmazgp?-&u|CbGXxUAc%ar*~~J2IaD literal 0 HcmV?d00001 diff --git a/test_data/unit/SelfRef.java b/test_data/unit/SelfRef.java new file mode 100644 index 00000000..e5a836de --- /dev/null +++ b/test_data/unit/SelfRef.java @@ -0,0 +1,10 @@ +class SelfRef { + static int a = peek(); + static int b = 41; + + static int peek() { + return b + 1; + } + + static void touch() {} +} diff --git a/tests/test_lazy_clinit.rs b/tests/test_lazy_clinit.rs new file mode 100644 index 00000000..b4ab6c15 --- /dev/null +++ b/tests/test_lazy_clinit.rs @@ -0,0 +1,34 @@ +use jvm::Result; +use jvm_rust::ClassDefinitionImpl; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_clinit_runs_once() -> Result<()> { + let jvm = test_jvm().await?; + + let class = ClassDefinitionImpl::from_classfile(include_bytes!("../test_data/unit/Counter.class"))?; + jvm.register_class(Box::new(class), None).await?; + + let _: () = jvm.invoke_static("Counter", "touch", "()V", ()).await?; + let _: () = jvm.invoke_static("Counter", "touch", "()V", ()).await?; + + assert_eq!(jvm.get_static_field::("Counter", "initCount", "I").await?, 1); + + Ok(()) +} + +#[tokio::test] +async fn test_clinit_reentrancy() -> Result<()> { + let jvm = test_jvm().await?; + + let class = ClassDefinitionImpl::from_classfile(include_bytes!("../test_data/unit/SelfRef.class"))?; + jvm.register_class(Box::new(class), None).await?; + + let _: () = jvm.invoke_static("SelfRef", "touch", "()V", ()).await?; + + assert_eq!(jvm.get_static_field::("SelfRef", "a", "I").await?, 1); + assert_eq!(jvm.get_static_field::("SelfRef", "b", "I").await?, 41); + + Ok(()) +} From 51aa232e6d0e64a6ab328252a1c58a0f8e674031 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 13 Jun 2026 08:19:59 +0900 Subject: [PATCH 02/10] Initialize classes lazily at first active use Registration no longer runs clinit; initialization happens at new, getstatic, putstatic, and invokestatic per JVMS 5.5, superclass first, and interfaces only on access to their own static fields. --- jvm/src/jvm.rs | 2 - test_data/unit/InitBase.class | Bin 0 -> 315 bytes test_data/unit/InitBase.java | 5 ++ test_data/unit/InitDerived.class | Bin 0 -> 363 bytes test_data/unit/InitDerived.java | 7 +++ test_data/unit/InitLog.class | Bin 0 -> 253 bytes test_data/unit/InitLog.java | 5 ++ test_data/unit/Inst.class | Bin 0 -> 275 bytes test_data/unit/Inst.java | 5 ++ test_data/unit/Mark.class | Bin 0 -> 272 bytes test_data/unit/Mark.java | 8 +++ test_data/unit/MarkIFace.class | Bin 0 -> 244 bytes test_data/unit/MarkIFace.java | 3 + test_data/unit/MarkImpl.class | Bin 0 -> 207 bytes test_data/unit/MarkImpl.java | 2 + test_data/unit/Source.class | Bin 0 -> 359 bytes test_data/unit/Source.java | 9 +++ test_data/unit/Target.class | Bin 0 -> 206 bytes test_data/unit/Target.java | 3 + tests/test_lazy_clinit.rs | 101 +++++++++++++++++++++++++++++++ 20 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 test_data/unit/InitBase.class create mode 100644 test_data/unit/InitBase.java create mode 100644 test_data/unit/InitDerived.class create mode 100644 test_data/unit/InitDerived.java create mode 100644 test_data/unit/InitLog.class create mode 100644 test_data/unit/InitLog.java create mode 100644 test_data/unit/Inst.class create mode 100644 test_data/unit/Inst.java create mode 100644 test_data/unit/Mark.class create mode 100644 test_data/unit/Mark.java create mode 100644 test_data/unit/MarkIFace.class create mode 100644 test_data/unit/MarkIFace.java create mode 100644 test_data/unit/MarkImpl.class create mode 100644 test_data/unit/MarkImpl.java create mode 100644 test_data/unit/Source.class create mode 100644 test_data/unit/Source.java create mode 100644 test_data/unit/Target.class create mode 100644 test_data/unit/Target.java diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index aa738731..2611010a 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -680,8 +680,6 @@ impl Jvm { self.inner.classes.write().insert(class.definition.name().to_owned(), class.clone()); - self.ensure_initialized(&class).await?; - Ok(()) } diff --git a/test_data/unit/InitBase.class b/test_data/unit/InitBase.class new file mode 100644 index 0000000000000000000000000000000000000000..64a6ec317422be0d6a7524d375552074d9593ae2 GIT binary patch literal 315 zcmYLF!AiqG5Pj3cZW1@PYHLpgkJf|zfg;#~P$>3b5xjMiE+r)hv=n_@kjCmWkqgytwKvg(%LT@9W> z1PB!(jbmtnn(}H^t{J6eT~yW)Xey!;jg~0F#Jt+MTUn=YN(jU-W{TiX$`y~+EGz6o z{gPO>FiFl#FiP{om(gQcyVTxi!pFbf8wrU^v^l$!bWRV#8KXW=66X37^v-~A&Ry1m i19bkRgTn*y2b8~j-a>5=|6AlXfCmkqja|kKT>Jo3Wi%E5 literal 0 HcmV?d00001 diff --git a/test_data/unit/InitBase.java b/test_data/unit/InitBase.java new file mode 100644 index 00000000..e51e1dc1 --- /dev/null +++ b/test_data/unit/InitBase.java @@ -0,0 +1,5 @@ +class InitBase { + static { + InitLog.baseOrder = ++InitLog.counter; + } +} diff --git a/test_data/unit/InitDerived.class b/test_data/unit/InitDerived.class new file mode 100644 index 0000000000000000000000000000000000000000..da76ce6912c77b3b3764aa318cf6cfe8edcb927d GIT binary patch literal 363 zcmZXQ%Sr<=6o&uPnNFtD)K=?tA-J?I`UFMrf>0>BPy}~QXM#rR1maBJ%XT5S@Bw@% z@f<9z&;v<+{(L!?eE)oY0XV~cf*JyaP@|5B5DyDmo~Lhy5DhHrQ$lduyKf*yLm|;< zLKBo=BlpBAb5&6qPoSYd8yamPbZ)$zn*7GIP}n3i<;~@vO9(Gq&PCcITbS$Wb!_}y zI-amoFI|;Ab0>prvS=W=b(PP|m6iL>zcl@qbeiG_9Uej=;KgHru*K?-PZH< x(B)mOMaS4)q6h3qtYHyjJ4>v$sMDH3g^v$&s5x4z%<`Agght5ucUjl4_X{0pI8*=t literal 0 HcmV?d00001 diff --git a/test_data/unit/InitDerived.java b/test_data/unit/InitDerived.java new file mode 100644 index 00000000..10fc161f --- /dev/null +++ b/test_data/unit/InitDerived.java @@ -0,0 +1,7 @@ +class InitDerived extends InitBase { + static { + InitLog.derivedOrder = ++InitLog.counter; + } + + static void touch() {} +} diff --git a/test_data/unit/InitLog.class b/test_data/unit/InitLog.class new file mode 100644 index 0000000000000000000000000000000000000000..5d4000f289ff50e336932ae06a7f3118bb3c9705 GIT binary patch literal 253 zcmXYr%?g505QWcVdClza0fLs*B3~ea2m(QJQMAA6C2o|0sqfV)Xwd`oP|@6QGv}Od zVCH2#Qd0Q{MicPs_mkA0V#+e2x1o%_menCB#n?JzE|l&aM1_wp-Rs! zqO-Vj`0km*nal6_2f!4A01a3!><|u`3ejF4b)4&B7r!NYlU54NNmgXltAh3TwDIA= zcM*hWA>?kY%blUgIqJMN3Tok^tza*TPf~k!*b;V@Sz%W7M`FsgPI3}&lIGXc;QdgS zshMZOeR=mH9WcTJtI3hca$peEkhc=@e1iI-U};WSi}ujHS;zm3yD0sJd-uwp0gItF J9X;v>`hQTVDj5I( literal 0 HcmV?d00001 diff --git a/test_data/unit/Inst.java b/test_data/unit/Inst.java new file mode 100644 index 00000000..f8135265 --- /dev/null +++ b/test_data/unit/Inst.java @@ -0,0 +1,5 @@ +class Inst { + static { + Target.value = 99; + } +} diff --git a/test_data/unit/Mark.class b/test_data/unit/Mark.class new file mode 100644 index 0000000000000000000000000000000000000000..e423593cadc4e33860f6a015888bb8b912de865f GIT binary patch literal 272 zcmX|6O>4qX5S&e(rit;R^(+)m+C%*VQYb>9pzWch_ZRz+Xp9hzzsgJTAb9Wx_@hc^ zEqHl5J2N}G@BDi_0eD6`f(JixwdCahdQq#Z9KQzz=MiZTxe$Pa;_!B^$~p(a|yYVBS)YG#kX?56(UW5@}NY tALwaM6MVnKuM%4NgRHYqfya|7vo=>9;t6^{+_@#x7|l#D80*XoG=3h|DD(gT literal 0 HcmV?d00001 diff --git a/test_data/unit/MarkIFace.java b/test_data/unit/MarkIFace.java new file mode 100644 index 00000000..fcbc1bb3 --- /dev/null +++ b/test_data/unit/MarkIFace.java @@ -0,0 +1,3 @@ +interface MarkIFace { + int X = Mark.set(); +} diff --git a/test_data/unit/MarkImpl.class b/test_data/unit/MarkImpl.class new file mode 100644 index 0000000000000000000000000000000000000000..0676fc447fe7f90a44e3456b7d7f062cf2216e53 GIT binary patch literal 207 zcmX^0Z`VEs1_nn4el7+k24;2!79Ivx1~x_pfvm)`ME#t^ymWp4q^#8B5=I6#o6Nk- z5<5l)W)00Sb_Nbc1`glEqHNFHf*f`RE=C4UFwZS9IhB!t#W_C(Nb&n*=B4_T<|d^U zg(N2B07bZh^Gl18Q{6H_9A21yJ&S7@SR-ZvNUvYyCM09%}{r0!0u(5TqU~y>H@DQ(^)ksm~=n2p)U@A4;4} zq#j(@{l1-4-p_F_gK_rqRADnxq!Iy(8oX`3bEJl3stT(O`MI&YU(;S#S_cmKF#&Keu;It zP;o}Ysfs#z7I0@t_WPdSn%|~|58BJ6$%iwhu$Z56lY^bj%?}XSXLrgf3^I9#cw-|l wb%7 literal 0 HcmV?d00001 diff --git a/test_data/unit/Source.java b/test_data/unit/Source.java new file mode 100644 index 00000000..e22bc38a --- /dev/null +++ b/test_data/unit/Source.java @@ -0,0 +1,9 @@ +class Source { + static int own = 7; + + static { + Target.value = 42; + } + + static void touch() {} +} diff --git a/test_data/unit/Target.class b/test_data/unit/Target.class new file mode 100644 index 0000000000000000000000000000000000000000..0d0495f76ed6281e8a8f36ea1a60ee6bc039f2ab GIT binary patch literal 206 zcmX9&OA3Ne6g}5}YS{*YhSeZ95J3b%5H(1eKkXq;%s|wAH3}NEfEE?qXOnw=9M1iG zUvB^#L_Q3dF02qX9KyH|orp_OKRPo@;X59Fo54P=s(yASWOs-(Ox`3 Qk3J6h>R6WuLq-FmAFgpDh5!Hn literal 0 HcmV?d00001 diff --git a/test_data/unit/Target.java b/test_data/unit/Target.java new file mode 100644 index 00000000..99ee082e --- /dev/null +++ b/test_data/unit/Target.java @@ -0,0 +1,3 @@ +class Target { + static int value; +} diff --git a/tests/test_lazy_clinit.rs b/tests/test_lazy_clinit.rs index b4ab6c15..c514acb3 100644 --- a/tests/test_lazy_clinit.rs +++ b/tests/test_lazy_clinit.rs @@ -32,3 +32,104 @@ async fn test_clinit_reentrancy() -> Result<()> { Ok(()) } + +async fn register(jvm: &jvm::Jvm, class_data: &[u8]) -> Result<()> { + let class = ClassDefinitionImpl::from_classfile(class_data)?; + jvm.register_class(Box::new(class), None).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_clinit_not_run_at_registration() -> Result<()> { + let jvm = test_jvm().await?; + + register(&jvm, include_bytes!("../test_data/unit/Target.class")).await?; + register(&jvm, include_bytes!("../test_data/unit/Source.class")).await?; + + assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 0); + + let _: () = jvm.invoke_static("Source", "touch", "()V", ()).await?; + assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 42); + + Ok(()) +} + +#[tokio::test] +async fn test_getstatic_triggers_clinit() -> Result<()> { + let jvm = test_jvm().await?; + + register(&jvm, include_bytes!("../test_data/unit/Target.class")).await?; + register(&jvm, include_bytes!("../test_data/unit/Source.class")).await?; + + assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 0); + assert_eq!(jvm.get_static_field::("Source", "own", "I").await?, 7); + assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 42); + + Ok(()) +} + +#[tokio::test] +async fn test_putstatic_triggers_clinit() -> Result<()> { + let jvm = test_jvm().await?; + + register(&jvm, include_bytes!("../test_data/unit/Target.class")).await?; + register(&jvm, include_bytes!("../test_data/unit/Source.class")).await?; + + assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 0); + + jvm.put_static_field("Source", "own", "I", 100).await?; + assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 42); + assert_eq!(jvm.get_static_field::("Source", "own", "I").await?, 100); + + Ok(()) +} + +#[tokio::test] +async fn test_new_triggers_clinit() -> Result<()> { + let jvm = test_jvm().await?; + + register(&jvm, include_bytes!("../test_data/unit/Target.class")).await?; + register(&jvm, include_bytes!("../test_data/unit/Inst.class")).await?; + + assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 0); + + let _ = jvm.instantiate_class("Inst").await?; + assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 99); + + Ok(()) +} + +#[tokio::test] +async fn test_superclass_initialized_first() -> Result<()> { + let jvm = test_jvm().await?; + + register(&jvm, include_bytes!("../test_data/unit/InitLog.class")).await?; + register(&jvm, include_bytes!("../test_data/unit/InitBase.class")).await?; + register(&jvm, include_bytes!("../test_data/unit/InitDerived.class")).await?; + + assert_eq!(jvm.get_static_field::("InitLog", "counter", "I").await?, 0); + + let _: () = jvm.invoke_static("InitDerived", "touch", "()V", ()).await?; + assert_eq!(jvm.get_static_field::("InitLog", "baseOrder", "I").await?, 1); + assert_eq!(jvm.get_static_field::("InitLog", "derivedOrder", "I").await?, 2); + + Ok(()) +} + +#[tokio::test] +async fn test_interface_not_initialized_by_implementor() -> Result<()> { + let jvm = test_jvm().await?; + + register(&jvm, include_bytes!("../test_data/unit/Mark.class")).await?; + register(&jvm, include_bytes!("../test_data/unit/MarkIFace.class")).await?; + register(&jvm, include_bytes!("../test_data/unit/MarkImpl.class")).await?; + + let _ = jvm.instantiate_class("MarkImpl").await?; + assert_eq!(jvm.get_static_field::("Mark", "value", "I").await?, 0); + + assert_eq!(jvm.get_static_field::("MarkIFace", "X", "I").await?, 1); + assert_eq!(jvm.get_static_field::("Mark", "value", "I").await?, 1); + + Ok(()) +} From 6155e4a72062fe4a52f7f6bf5f4273af981653a9 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 13 Jun 2026 08:22:24 +0900 Subject: [PATCH 03/10] Wrap clinit failures in ExceptionInInitializerError Non-Error exceptions from clinit are wrapped per JVMS 5.5; Errors propagate as-is. The failed class becomes erroneous and later uses throw NoClassDefFoundError. --- java_runtime/src/classes/java/lang.rs | 17 +++---- .../lang/exception_in_initializer_error.rs | 43 ++++++++++++++++++ java_runtime/src/loader.rs | 1 + jvm/src/jvm.rs | 12 ++++- test_data/unit/ClinitThrows.class | Bin 0 -> 455 bytes test_data/unit/ClinitThrows.java | 12 +++++ test_data/unit/ClinitThrowsError.class | Bin 0 -> 461 bytes test_data/unit/ClinitThrowsError.java | 12 +++++ tests/test_lazy_clinit.rs | 37 +++++++++++++++ 9 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 java_runtime/src/classes/java/lang/exception_in_initializer_error.rs create mode 100644 test_data/unit/ClinitThrows.class create mode 100644 test_data/unit/ClinitThrows.java create mode 100644 test_data/unit/ClinitThrowsError.class create mode 100644 test_data/unit/ClinitThrowsError.java diff --git a/java_runtime/src/classes/java/lang.rs b/java_runtime/src/classes/java/lang.rs index 837d83b0..961c875a 100644 --- a/java_runtime/src/classes/java/lang.rs +++ b/java_runtime/src/classes/java/lang.rs @@ -9,6 +9,7 @@ mod cloneable; mod comparable; mod error; mod exception; +mod exception_in_initializer_error; mod illegal_argument_exception; mod incompatible_class_change_error; mod index_out_of_bounds_exception; @@ -40,12 +41,12 @@ pub use self::{ abstract_method_error::AbstractMethodError, arithmetic_exception::ArithmeticException, array_index_out_of_bounds_exception::ArrayIndexOutOfBoundsException, class::Class, class_cast_exception::ClassCastException, class_loader::ClassLoader, clone_not_supported_exception::CloneNotSupportedException, cloneable::Cloneable, comparable::Comparable, error::Error, - exception::Exception, illegal_argument_exception::IllegalArgumentException, incompatible_class_change_error::IncompatibleClassChangeError, - index_out_of_bounds_exception::IndexOutOfBoundsException, instantiation_error::InstantiationError, integer::Integer, - interrupted_exception::InterruptedException, linkage_error::LinkageError, math::Math, negative_array_size_exception::NegativeArraySizeException, - no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError, no_such_method_error::NoSuchMethodError, - null_pointer_exception::NullPointerException, number_format_exception::NumberFormatException, object::Object, runnable::Runnable, - runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, string::String, string_buffer::StringBuffer, - string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, throwable::Throwable, - unsupported_operation_exception::UnsupportedOperationException, + exception::Exception, exception_in_initializer_error::ExceptionInInitializerError, illegal_argument_exception::IllegalArgumentException, + incompatible_class_change_error::IncompatibleClassChangeError, index_out_of_bounds_exception::IndexOutOfBoundsException, + instantiation_error::InstantiationError, integer::Integer, interrupted_exception::InterruptedException, linkage_error::LinkageError, math::Math, + negative_array_size_exception::NegativeArraySizeException, no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError, + no_such_method_error::NoSuchMethodError, null_pointer_exception::NullPointerException, number_format_exception::NumberFormatException, + object::Object, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, string::String, + string_buffer::StringBuffer, string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, + throwable::Throwable, unsupported_operation_exception::UnsupportedOperationException, }; diff --git a/java_runtime/src/classes/java/lang/exception_in_initializer_error.rs b/java_runtime/src/classes/java/lang/exception_in_initializer_error.rs new file mode 100644 index 00000000..27a1aa34 --- /dev/null +++ b/java_runtime/src/classes/java/lang/exception_in_initializer_error.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.ExceptionInInitializerError +pub struct ExceptionInInitializerError; + +impl ExceptionInInitializerError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/ExceptionInInitializerError", + parent_class: Some("java/lang/LinkageError"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.ExceptionInInitializerError::({:?})", &this); + + let _: () = jvm.invoke_special(&this, "java/lang/LinkageError", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.ExceptionInInitializerError::({:?}, {:?})", &this, &message); + + let _: () = jvm + .invoke_special(&this, "java/lang/LinkageError", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index 473f6a45..a83ccecb 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -43,6 +43,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::Comparable::as_proto(), crate::classes::java::lang::Error::as_proto(), crate::classes::java::lang::Exception::as_proto(), + crate::classes::java::lang::ExceptionInInitializerError::as_proto(), crate::classes::java::lang::IllegalArgumentException::as_proto(), crate::classes::java::lang::InstantiationError::as_proto(), crate::classes::java::lang::IncompatibleClassChangeError::as_proto(), diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 2611010a..0fbaf803 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -725,7 +725,17 @@ impl Jvm { if let Err(err) = self.execute_method(class, None, &clinit, Box::new([])).await { class.set_init_state(InitState::Erroneous); - return Err(err); + + let JavaError::JavaException(exception) = &err; + if self.is_instance(&**exception, "java/lang/Error") { + return Err(err); + } + + // Throwable has no cause field, so carry the original exception as the message + let message: Box = self.invoke_virtual(exception, "toString", "()Ljava/lang/String;", ()).await?; + let message = JavaLangString::to_rust_string(self, &message).await?; + + return Err(self.exception("java/lang/ExceptionInInitializerError", &message).await); } } diff --git a/test_data/unit/ClinitThrows.class b/test_data/unit/ClinitThrows.class new file mode 100644 index 0000000000000000000000000000000000000000..6e2e1269ffed614cc3b979e4d2d8770a4a065cfa GIT binary patch literal 455 zcmZWlT}uK%6g{)My1MS>M`>jQJ!%j61Ck)5Ah3cIp~u~A&@p#Mw%PLM^cSiJp`hOT zQPCX>4H}p`mvhhDbMF28dVL3Qj2#CCOdFPq46+QRv48KkCVo6@T?S(wDu!%Z#6lf0 zn2qKjj~wzg92W(+3~pzli~Ug|AMO~~jP99@HHP}C(N!8N5%H5*$frukIENC06-XI5 z@KDjp>kQRKclD*G5)ls%n*$G0#LJFU_ZmQ9^t*gyRH=HD1T@PVlvD(I;<&W2LGV`HI#E0pgD~afP$pQulv4*T zs#8Us~Id~n6Y;ktLFKOgx0eaztz4EQgf1kW1 zb(ax+)@TLpy}hKhQw6}Y-eAANww~dBz`d=O7N~q_2_u!L!s0x`cb1*mjODSzhzY8( Jx>hr=_X|s$Ps#uQ literal 0 HcmV?d00001 diff --git a/test_data/unit/ClinitThrowsError.java b/test_data/unit/ClinitThrowsError.java new file mode 100644 index 00000000..e015c82d --- /dev/null +++ b/test_data/unit/ClinitThrowsError.java @@ -0,0 +1,12 @@ +class ClinitThrowsError { + static int x; + + static { + x = 1; + if (x == 1) { + throw new LinkageError("boom"); + } + } + + static void touch() {} +} diff --git a/tests/test_lazy_clinit.rs b/tests/test_lazy_clinit.rs index c514acb3..2252bdb5 100644 --- a/tests/test_lazy_clinit.rs +++ b/tests/test_lazy_clinit.rs @@ -133,3 +133,40 @@ async fn test_interface_not_initialized_by_implementor() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_clinit_failure_wrapped_and_class_erroneous() -> Result<()> { + let jvm = test_jvm().await?; + + register(&jvm, include_bytes!("../test_data/unit/ClinitThrows.class")).await?; + + let result: Result<()> = jvm.invoke_static("ClinitThrows", "touch", "()V", ()).await; + let Err(jvm::JavaError::JavaException(exception)) = result else { + panic!("Expected JavaException, got {:?}", result); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ExceptionInInitializerError")); + + let result: Result<()> = jvm.invoke_static("ClinitThrows", "touch", "()V", ()).await; + let Err(jvm::JavaError::JavaException(exception)) = result else { + panic!("Expected JavaException, got {:?}", result); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NoClassDefFoundError")); + + Ok(()) +} + +#[tokio::test] +async fn test_clinit_failure_error_propagates_unwrapped() -> Result<()> { + let jvm = test_jvm().await?; + + register(&jvm, include_bytes!("../test_data/unit/ClinitThrowsError.class")).await?; + + let result: Result<()> = jvm.invoke_static("ClinitThrowsError", "touch", "()V", ()).await; + let Err(jvm::JavaError::JavaException(exception)) = result else { + panic!("Expected JavaException, got {:?}", result); + }; + assert!(jvm.is_instance(&*exception, "java/lang/LinkageError")); + assert!(!jvm.is_instance(&*exception, "java/lang/ExceptionInInitializerError")); + + Ok(()) +} From 3930d1a772eca1f1866b01a6e4ba514e28c5376d Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 13 Jun 2026 08:23:55 +0900 Subject: [PATCH 04/10] Add E2E fixtures for lazy class initialization StaticOrder checks that an ldc class literal resolves without initializing, and DoubleInit guards against double clinit through the on-demand class loading path. --- test_data/DoubleInit.class | Bin 0 -> 539 bytes test_data/DoubleInit.java | 12 ++++++++++++ test_data/DoubleInit.txt | 1 + test_data/StaticOrder$Helper.class | Bin 0 -> 536 bytes test_data/StaticOrder.class | Bin 0 -> 542 bytes test_data/StaticOrder.java | 15 +++++++++++++++ test_data/StaticOrder.txt | 2 ++ 7 files changed, 30 insertions(+) create mode 100644 test_data/DoubleInit.class create mode 100644 test_data/DoubleInit.java create mode 100644 test_data/DoubleInit.txt create mode 100644 test_data/StaticOrder$Helper.class create mode 100644 test_data/StaticOrder.class create mode 100644 test_data/StaticOrder.java create mode 100644 test_data/StaticOrder.txt diff --git a/test_data/DoubleInit.class b/test_data/DoubleInit.class new file mode 100644 index 0000000000000000000000000000000000000000..3f6572388c44d420561cef67662c8f9a7d37ac7a GIT binary patch literal 539 zcmZvZ%SyvQ6o&ulC6{SzYiq0Z-i6q@Fb~ioh$09@g-SQB(m1qJa*>z_K9(+2T=)P! zlz1iOnbCUW0Z_b?g{CfWYaEvxb4tX5~3q@!Q)tU3)*q#$i?91^?xG{s)mw}9r z81kL&h{J%XgIg#mIX?}Pu_w++z)+0C#GRU0LRm+}LKVw@fWyc8Sop*V6H+$@=~gnd zuOb=5!#EO--!o8WD5woN8ffZRwXlX3Lw$joLBCU8FCbj!hnJR4Fd0mG-4U@)!dt;m z8OT6fB>q@L*A7)o3%(=C*X-OZHX41`n;)DH!z6OWnN+??cH~1<6WY|095oXtfSLe} z=5_koqR1!~4_?8ZC}G&3SuLyFP#vVGq%9ii`6tt0VlyQv*}js{(BINGW0sMp-zLqR hm?dIw|FZYy#hK5rYPA=vWHxp8Ad`_+zC(EqyWen2UYY;^ literal 0 HcmV?d00001 diff --git a/test_data/DoubleInit.java b/test_data/DoubleInit.java new file mode 100644 index 00000000..16e4c17f --- /dev/null +++ b/test_data/DoubleInit.java @@ -0,0 +1,12 @@ +public class DoubleInit { + static { + System.out.println("init"); + } + + static void touch() {} + + public static void main(String[] args) { + touch(); + touch(); + } +} diff --git a/test_data/DoubleInit.txt b/test_data/DoubleInit.txt new file mode 100644 index 00000000..b1b71610 --- /dev/null +++ b/test_data/DoubleInit.txt @@ -0,0 +1 @@ +init diff --git a/test_data/StaticOrder$Helper.class b/test_data/StaticOrder$Helper.class new file mode 100644 index 0000000000000000000000000000000000000000..080b4ffe8b09d3c7b5180ab832cc4af7eaee686d GIT binary patch literal 536 zcmZvZ?Mebc6o%jNYu0s3)2!^fDA1t103`?^3`Bzs|F7$yE9f zpE5}mPTe7cQFr~t>r6YGN>|)^`aZX!cBM@b+7^*&hLPepqDQO%4Axx` z2-)zRC=wA(Y~CNoDAIKGiqod~f?=DY13H<`T)crjQ^2r8UwsWKFi={eCCcda2jhh8 rF0u1PpY#iOe(e?6A65N>fdt`6vav^T4Etn3pNC`-FsW~m;u9F(ezRx0 literal 0 HcmV?d00001 diff --git a/test_data/StaticOrder.class b/test_data/StaticOrder.class new file mode 100644 index 0000000000000000000000000000000000000000..dbacc6867b1bc4ab400e8ed5b4e3e5485d5319a9 GIT binary patch literal 542 zcmZvZ%Sr<=6o&t_(@8rWZJk>0_X|`PcHvSHq#z2l6)J9A&9t#b<|3I?@Ue8E;=%{; zp~RDMVFeS&xqs&*`TTnS0C0?L2RY<9EEfgX4AndT!Ec6sJZyG@JJHt+_MwcWK4Qo> zcCI*z3|>$BTK2nYAk@~W2=9d|!9khBbuod-KbYQQszpTA$%tU}cGf^9%?l-C-P20= z(LoVa>Jx~YLX>1u{gcnY?@e_W6R`0^%tj`hjE4t1R}MJlT+Ev*6?8J{ z-*PN6l*a~Vuv*DLFjU$y7M)QP2zBWPA$hFGmxP{aT>r;raC*r|^~H%a$<;CDo*9FMC&nS777fiij_P5A`Md32?aOtFOk2Pf Fir)y@Zae@0 literal 0 HcmV?d00001 diff --git a/test_data/StaticOrder.java b/test_data/StaticOrder.java new file mode 100644 index 00000000..dddb398f --- /dev/null +++ b/test_data/StaticOrder.java @@ -0,0 +1,15 @@ +public class StaticOrder { + static class Helper { + static { + System.out.println("helper-init"); + } + + static void touch() {} + } + + public static void main(String[] args) { + Class c = Helper.class; + System.out.println("before"); + Helper.touch(); + } +} diff --git a/test_data/StaticOrder.txt b/test_data/StaticOrder.txt new file mode 100644 index 00000000..8a87ebd8 --- /dev/null +++ b/test_data/StaticOrder.txt @@ -0,0 +1,2 @@ +before +helper-init From 3067771b817103ea5e0f873e04fe10dcea428a58 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 13 Jun 2026 08:24:44 +0900 Subject: [PATCH 05/10] Style --- jvm/src/jvm.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 0fbaf803..a6991b41 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -523,7 +523,8 @@ impl Jvm { let class = self.load_class(class_name, class_loader_wrapper).await?; - // wrapper가 만든 별도 Class를 그대로 쓰면 init 상태가 공유되지 않으므로 레지스트리에 등록된 canonical 본을 반환 + // loader wrappers may build a fresh Class around an already-registered definition, + // so return the registry copy to keep init state shared if let Some(registered) = self.get_class(class_name) { return Ok(registered); } @@ -705,7 +706,7 @@ impl Jvm { class.set_init_state(InitState::InProgress); if let Some(super_name) = class.definition.super_class_name() { - // resolve 실패는 초기화 실패가 아님 — 재시도 가능 상태로 되돌린다 + // resolution failure is not an initialization failure, so initialization may be retried let super_class = match self.resolve_class(&super_name).await { Ok(x) => x, Err(err) => { From 235058dc23f974fb379cad0d0f98ced88437d3d0 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 13 Jun 2026 08:32:22 +0900 Subject: [PATCH 06/10] Set ConstantValue static fields during class preparation javac emits compile-time constants as ConstantValue attributes with no clinit code, so they previously read as zero. ClassDefinition::prepare materializes them into static storage before initialization. --- jvm/src/array_class_definition.rs | 4 ++ jvm/src/class_definition.rs | 1 + jvm/src/jvm.rs | 5 +++ jvm_rust/src/class_definition.rs | 66 ++++++++++++++++++++++++++++-- test_data/unit/Constants.class | Bin 0 -> 481 bytes test_data/unit/Constants.java | 11 +++++ tests/test_constant_value.rs | 26 ++++++++++++ 7 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 test_data/unit/Constants.class create mode 100644 test_data/unit/Constants.java create mode 100644 tests/test_constant_value.rs diff --git a/jvm/src/array_class_definition.rs b/jvm/src/array_class_definition.rs index 7d377805..69d60d0d 100644 --- a/jvm/src/array_class_definition.rs +++ b/jvm/src/array_class_definition.rs @@ -39,6 +39,10 @@ impl ClassDefinition for T { panic!("Cannot instantiate array class") } + async fn prepare(&self, _: &Jvm) -> Result<()> { + Ok(()) + } + fn method(&self, _name: &str, _descriptor: &str, _is_static: bool) -> Option> { None } diff --git a/jvm/src/class_definition.rs b/jvm/src/class_definition.rs index 536e4f56..7612b5ec 100644 --- a/jvm/src/class_definition.rs +++ b/jvm/src/class_definition.rs @@ -14,6 +14,7 @@ pub trait ClassDefinition: Sync + Send + AsAny + Debug + DynClone { fn interface_names(&self) -> Vec; fn access_flags(&self) -> ClassAccessFlags; async fn instantiate(&self, jvm: &Jvm) -> Result>; + async fn prepare(&self, jvm: &Jvm) -> Result<()>; fn method(&self, name: &str, descriptor: &str, is_static: bool) -> Option>; fn field(&self, name: &str, descriptor: &str, is_static: bool) -> Option>; fn fields(&self) -> Vec>; diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index a6991b41..b4f9ba8e 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -721,6 +721,11 @@ impl Jvm { } } + if let Err(err) = class.definition.prepare(self).await { + class.set_init_state(InitState::Erroneous); + return Err(err); + } + if let Some(clinit) = class.definition.method("", "()V", true) { tracing::debug!("Calling for {}", class.definition.name()); diff --git a/jvm_rust/src/class_definition.rs b/jvm_rust/src/class_definition.rs index 1e07a0a6..76ad8fad 100644 --- a/jvm_rust/src/class_definition.rs +++ b/jvm_rust/src/class_definition.rs @@ -12,10 +12,10 @@ use core::{ use parking_lot::RwLock; -use classfile::ClassInfo; +use classfile::{AttributeInfo, ClassInfo, ConstantPoolReference}; use java_class_proto::JavaClassProto; use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; -use jvm::{ClassDefinition, ClassInstance, Field, JavaType, JavaValue, Jvm, Method, Result}; +use jvm::{ClassDefinition, ClassInstance, Field, JavaType, JavaValue, Jvm, Method, Result, runtime::JavaLangString}; use crate::{class_instance::ClassInstanceImpl, field::FieldImpl, method::MethodImpl}; @@ -26,6 +26,7 @@ struct ClassDefinitionInner { access_flags: ClassAccessFlags, methods: Vec, fields: Vec, + constant_values: Vec<(FieldImpl, ConstantPoolReference)>, storage: RwLock>, // TODO we should use field offset or something } @@ -42,6 +43,19 @@ impl ClassDefinitionImpl { access_flags: ClassAccessFlags, methods: Vec, fields: Vec, + ) -> Self { + Self::with_constant_values(name, super_class_name, interfaces, access_flags, methods, fields, Vec::new()) + } + + #[allow(clippy::too_many_arguments)] + fn with_constant_values( + name: &str, + super_class_name: Option, + interfaces: Vec, + access_flags: ClassAccessFlags, + methods: Vec, + fields: Vec, + constant_values: Vec<(FieldImpl, ConstantPoolReference)>, ) -> Self { Self { inner: Arc::new(ClassDefinitionInner { @@ -51,6 +65,7 @@ impl ClassDefinitionImpl { access_flags, methods, fields, + constant_values, storage: RwLock::new(BTreeMap::new()), }), } @@ -85,19 +100,39 @@ impl ClassDefinitionImpl { let class = ClassInfo::parse(data).unwrap(); // TODO ClassFormatError assert_eq!(class.magic, 0xCAFEBABE); - let fields = class.fields.into_iter().map(FieldImpl::from_field_info).collect::>(); + let mut constant_values = Vec::new(); + let fields = class + .fields + .into_iter() + .map(|field_info| { + let constant = field_info.attributes.iter().find_map(|x| match x { + AttributeInfo::ConstantValue(value) => Some(value.clone()), + _ => None, + }); + + let field = FieldImpl::from_field_info(field_info); + if let Some(x) = constant + && field.access_flags().contains(FieldAccessFlags::STATIC) + { + constant_values.push((field.clone(), x)); + } + + field + }) + .collect::>(); let methods = class.methods.into_iter().map(MethodImpl::from_method_info).collect::>(); let interfaces = class.interfaces.into_iter().map(|x| x.to_string()).collect(); - Ok(Self::new( + Ok(Self::with_constant_values( &class.this_class, class.super_class.map(|x| x.to_string()), interfaces, class.access_flags, methods, fields, + constant_values, )) } @@ -128,6 +163,29 @@ impl ClassDefinition for ClassDefinitionImpl { Ok(Box::new(ClassInstanceImpl::new(self))) } + async fn prepare(&self, jvm: &Jvm) -> Result<()> { + for (field, constant) in &self.inner.constant_values { + let value = match constant { + ConstantPoolReference::Integer(x) => match field.descriptor().as_str() { + "Z" => JavaValue::Boolean(*x != 0), + "B" => JavaValue::Byte(*x as i8), + "C" => JavaValue::Char(*x as u16), + "S" => JavaValue::Short(*x as i16), + _ => JavaValue::Int(*x), + }, + ConstantPoolReference::Long(x) => JavaValue::Long(*x), + ConstantPoolReference::Float(x) => JavaValue::Float(*x), + ConstantPoolReference::Double(x) => JavaValue::Double(*x), + ConstantPoolReference::String(x) => JavaValue::Object(Some(JavaLangString::from_rust_string(jvm, x).await?)), + _ => continue, + }; + + self.inner.storage.write().insert(field.clone(), value); + } + + Ok(()) + } + fn method(&self, name: &str, descriptor: &str, is_static: bool) -> Option> { self.inner .methods diff --git a/test_data/unit/Constants.class b/test_data/unit/Constants.class new file mode 100644 index 0000000000000000000000000000000000000000..e5ee9f51bd42a08daf77c3477c3dc31126e492b0 GIT binary patch literal 481 zcmYL`OHaZ;6ot>Fg#z*t-%s!vWg$O6qap^QX*98fg}Z_cmee*v@qf88abaBRPcku{ zF&UhbnR~u_Co?xQpFeN!02jvI_UgXaqiXUZ5fO=VmfM;VLeQkfc!&3TpN z{>sz0`9)}%Cm%$}dFs^UCJx^3v~LAv`@cme9{OSbFpUjC^1%y&hz&KPdrwfb{m{D} zKX$!g+wBI-WSwX{?0GF;K|%LAI#9=<3X2Z`WU$DY1ajm&rMW;ZQkqNTX-adM zJVR-oCC_1g!VBajO4pakE0pF{@;WLLuF^q$4dW)=8slH4_6m7nLa@bEmH*xg+l&e9 F`~r10KL!8* literal 0 HcmV?d00001 diff --git a/test_data/unit/Constants.java b/test_data/unit/Constants.java new file mode 100644 index 00000000..a45fe8fc --- /dev/null +++ b/test_data/unit/Constants.java @@ -0,0 +1,11 @@ +class Constants { + static final boolean FLAG = true; + static final byte B = 3; + static final char C = 'a'; + static final short S = 7; + static final int I = 42; + static final long L = 1234567890123L; + static final float F = 1.5f; + static final double D = 2.5; + static final String STR = "hello"; +} diff --git a/tests/test_constant_value.rs b/tests/test_constant_value.rs new file mode 100644 index 00000000..73a96143 --- /dev/null +++ b/tests/test_constant_value.rs @@ -0,0 +1,26 @@ +use jvm::{ClassInstance, JavaChar, Result, runtime::JavaLangString}; +use jvm_rust::ClassDefinitionImpl; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_constant_value_fields() -> Result<()> { + let jvm = test_jvm().await?; + + let class = ClassDefinitionImpl::from_classfile(include_bytes!("../test_data/unit/Constants.class"))?; + jvm.register_class(Box::new(class), None).await?; + + assert!(jvm.get_static_field::("Constants", "FLAG", "Z").await?); + assert_eq!(jvm.get_static_field::("Constants", "B", "B").await?, 3); + assert_eq!(jvm.get_static_field::("Constants", "C", "C").await?, 'a' as JavaChar); + assert_eq!(jvm.get_static_field::("Constants", "S", "S").await?, 7); + assert_eq!(jvm.get_static_field::("Constants", "I", "I").await?, 42); + assert_eq!(jvm.get_static_field::("Constants", "L", "J").await?, 1234567890123); + assert_eq!(jvm.get_static_field::("Constants", "F", "F").await?, 1.5); + assert_eq!(jvm.get_static_field::("Constants", "D", "D").await?, 2.5); + + let string: Box = jvm.get_static_field("Constants", "STR", "Ljava/lang/String;").await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &string).await?, "hello"); + + Ok(()) +} From 99eff870408059a1c5b914135959b32d8d22d96e Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 13 Jun 2026 08:34:44 +0900 Subject: [PATCH 07/10] Replace lazy-clinit unit tests with java-verified E2E fixtures LazyClinit and ClinitFailure cover the trigger, ordering, interface, re-entrancy, and failure scenarios; expected outputs are generated by running the fixtures on a real JVM. This caught Class.getName returning the internal slash form instead of the dotted binary name, now fixed. Constants and StaticFlag stay as Rust-side tests: javac inlines compile-time constants so ConstantValue is unobservable from bytecode, and StaticFlag verifies typed reads from native code. --- java_runtime/src/classes/java/lang/class.rs | 2 +- .../tests/classes/java/lang/test_throwable.rs | 4 +- test_data/ClinitFailure$Bad.class | Bin 0 -> 522 bytes test_data/ClinitFailure$BadError.class | Bin 0 -> 528 bytes test_data/ClinitFailure.class | Bin 0 -> 944 bytes test_data/ClinitFailure.java | 41 +++++ test_data/ClinitFailure.txt | 3 + test_data/LazyClinit$Base.class | Bin 0 -> 480 bytes test_data/LazyClinit$Derived.class | Bin 0 -> 550 bytes test_data/LazyClinit$GetstaticTarget.class | Bin 0 -> 541 bytes test_data/LazyClinit$IFace.class | Bin 0 -> 317 bytes test_data/LazyClinit$Impl.class | Bin 0 -> 312 bytes test_data/LazyClinit$NewTarget.class | Bin 0 -> 489 bytes test_data/LazyClinit$PutstaticTarget.class | Bin 0 -> 523 bytes test_data/LazyClinit$SelfRef.class | Bin 0 -> 458 bytes test_data/LazyClinit.class | Bin 0 -> 1162 bytes test_data/LazyClinit.java | 72 ++++++++ test_data/LazyClinit.txt | 13 ++ test_data/unit/ClinitThrows.class | Bin 455 -> 0 bytes test_data/unit/ClinitThrows.java | 12 -- test_data/unit/ClinitThrowsError.class | Bin 461 -> 0 bytes test_data/unit/ClinitThrowsError.java | 12 -- test_data/unit/Counter.class | Bin 331 -> 0 bytes test_data/unit/Counter.java | 9 - test_data/unit/InitBase.class | Bin 315 -> 0 bytes test_data/unit/InitBase.java | 5 - test_data/unit/InitDerived.class | Bin 363 -> 0 bytes test_data/unit/InitDerived.java | 7 - test_data/unit/InitLog.class | Bin 253 -> 0 bytes test_data/unit/InitLog.java | 5 - test_data/unit/Inst.class | Bin 275 -> 0 bytes test_data/unit/Inst.java | 5 - test_data/unit/Mark.class | Bin 272 -> 0 bytes test_data/unit/Mark.java | 8 - test_data/unit/MarkIFace.class | Bin 244 -> 0 bytes test_data/unit/MarkIFace.java | 3 - test_data/unit/MarkImpl.class | Bin 207 -> 0 bytes test_data/unit/MarkImpl.java | 2 - test_data/unit/SelfRef.class | Bin 415 -> 0 bytes test_data/unit/SelfRef.java | 10 - test_data/unit/Source.class | Bin 359 -> 0 bytes test_data/unit/Source.java | 9 - test_data/unit/Target.class | Bin 206 -> 0 bytes test_data/unit/Target.java | 3 - tests/test_lazy_clinit.rs | 172 ------------------ 45 files changed, 132 insertions(+), 265 deletions(-) create mode 100644 test_data/ClinitFailure$Bad.class create mode 100644 test_data/ClinitFailure$BadError.class create mode 100644 test_data/ClinitFailure.class create mode 100644 test_data/ClinitFailure.java create mode 100644 test_data/ClinitFailure.txt create mode 100644 test_data/LazyClinit$Base.class create mode 100644 test_data/LazyClinit$Derived.class create mode 100644 test_data/LazyClinit$GetstaticTarget.class create mode 100644 test_data/LazyClinit$IFace.class create mode 100644 test_data/LazyClinit$Impl.class create mode 100644 test_data/LazyClinit$NewTarget.class create mode 100644 test_data/LazyClinit$PutstaticTarget.class create mode 100644 test_data/LazyClinit$SelfRef.class create mode 100644 test_data/LazyClinit.class create mode 100644 test_data/LazyClinit.java create mode 100644 test_data/LazyClinit.txt delete mode 100644 test_data/unit/ClinitThrows.class delete mode 100644 test_data/unit/ClinitThrows.java delete mode 100644 test_data/unit/ClinitThrowsError.class delete mode 100644 test_data/unit/ClinitThrowsError.java delete mode 100644 test_data/unit/Counter.class delete mode 100644 test_data/unit/Counter.java delete mode 100644 test_data/unit/InitBase.class delete mode 100644 test_data/unit/InitBase.java delete mode 100644 test_data/unit/InitDerived.class delete mode 100644 test_data/unit/InitDerived.java delete mode 100644 test_data/unit/InitLog.class delete mode 100644 test_data/unit/InitLog.java delete mode 100644 test_data/unit/Inst.class delete mode 100644 test_data/unit/Inst.java delete mode 100644 test_data/unit/Mark.class delete mode 100644 test_data/unit/Mark.java delete mode 100644 test_data/unit/MarkIFace.class delete mode 100644 test_data/unit/MarkIFace.java delete mode 100644 test_data/unit/MarkImpl.class delete mode 100644 test_data/unit/MarkImpl.java delete mode 100644 test_data/unit/SelfRef.class delete mode 100644 test_data/unit/SelfRef.java delete mode 100644 test_data/unit/Source.class delete mode 100644 test_data/unit/Source.java delete mode 100644 test_data/unit/Target.class delete mode 100644 test_data/unit/Target.java delete mode 100644 tests/test_lazy_clinit.rs diff --git a/java_runtime/src/classes/java/lang/class.rs b/java_runtime/src/classes/java/lang/class.rs index 59911f3d..77bb98ea 100644 --- a/java_runtime/src/classes/java/lang/class.rs +++ b/java_runtime/src/classes/java/lang/class.rs @@ -63,7 +63,7 @@ impl Class { tracing::debug!("java.lang.Class::getName({:?})", &this); let rust_class = JavaLangClass::to_rust_class(jvm, &this).await?; - let result = JavaLangString::from_rust_string(jvm, &rust_class.name()).await?; + let result = JavaLangString::from_rust_string(jvm, &rust_class.name().replace('/', ".")).await?; Ok(result.into()) } diff --git a/java_runtime/tests/classes/java/lang/test_throwable.rs b/java_runtime/tests/classes/java/lang/test_throwable.rs index 59351b10..7d59e1ea 100644 --- a/java_runtime/tests/classes/java/lang/test_throwable.rs +++ b/java_runtime/tests/classes/java/lang/test_throwable.rs @@ -16,7 +16,7 @@ async fn test_to_string() -> Result<()> { let result = JavaLangString::to_rust_string(&jvm, &to_string).await?; - assert_eq!(result, "java/lang/Throwable: test message"); + assert_eq!(result, "java.lang.Throwable: test message"); Ok(()) } @@ -46,7 +46,7 @@ async fn test_stacktrace() -> Result<()> { assert_eq!( result, "\ - java/net/MalformedURLException: unknown protocol: invalid\n\ + java.net.MalformedURLException: unknown protocol: invalid\n\ \tat java/net/URL.(Ljava/net/URL;Ljava/lang/String;Ljava/net/URLStreamHandler;)V\n\ \tat java/net/URL.(Ljava/net/URL;Ljava/lang/String;)V\n\ \tat java/net/URL.(Ljava/lang/String;)V\n\ diff --git a/test_data/ClinitFailure$Bad.class b/test_data/ClinitFailure$Bad.class new file mode 100644 index 0000000000000000000000000000000000000000..ea9989dffd5545fca2fec93da76b45eb2c24addb GIT binary patch literal 522 zcmZXQT}uK%6o%jNYuDA))wI&Syy!x^$orC@Bq=b1459AEb+WZ}SN0?NEB%bR5en+Q z9~GTh!jJ}L=5Wq)&iit{zTZ9o9HXWohPZ-67D=QSj0u0_O`AL8=H+lA%zz=)vK%Wo zVu;rpgEUm66=+#xkY&)@wk&qIWrv<%NZe7xz-|@f7^;86u0kiUrsDL;6tlo`9Tf)A zL)V>Z&{34?D-7j&_phZNc$PChYz!2X8462EyC-}^{cU$dqWP}nh+a4y3h$Z^ZOSAA zH#F}V)Rr0fBlCJc;O4^xpZy|Q-wi!ebS%jimxk|457gWfesJdcw1Vup;|Q;9bKe&} zErQfhLy1-=Pn<3-4-BgmZPCwUW^WGmLIJ}Xy|N4>XnpGov_ctq{$UxyDhuo&QUD6C zP@bW`L%%8J=O}#2>9J^fE<#e&qQF2JRjS0n40<*qXp?+NSj9H|8ks3kyaV+Ijk#Wo literal 0 HcmV?d00001 diff --git a/test_data/ClinitFailure$BadError.class b/test_data/ClinitFailure$BadError.class new file mode 100644 index 0000000000000000000000000000000000000000..a7e1f5ebda62ebebb366e2c1094265e86ad37971 GIT binary patch literal 528 zcmZXQO;5r=5Qg8CkCxg(`BL$dg9q>+cO@oDB$^-|FeG~0mK6)7HEr>~_%rlEqKV%9 zQO4OuA;e8~cQW(LJ8$Oe`|Sh3DXJP)kWi4+kwTim9PuaKaJe^ZTw5byM-1tv<2lg@ zL!wsiXP_dZK+~}boxy0ive@R1I}HRw@}43FcBdfAu(lBEINq2K#d#3;feMpI%l9W5 z3>4(#B15^>S>U}WaJ=Dhy|185I!jab&-ftLA(56pps6`h7v1T^62T3(T*{;(e`-H4 zs7*UINbZeZ#O?7FfBZ$Xo<9w2(RL(XSekGsjZkw}gwcf`(hl@X&l5q*jn`P1Xi+rvc zG(PwP{87epSG2C}%gpS(XXc!lx%=bix9;%qe)TWvk1!Y1tD`R=3T4fw=EZ1_v540t+YA_6|Jv zo!Pf#3R%o+$mz&qAw<}FKlbH_Jnn?Fi=80Za+}XR%kg`@C(Y4AC3jIr5ha1-Q2H&~ z9FIw4)PKVR<-sAL7f{x)q~jtg0@)KSvm}lAU2`M_3ZZmTSr>pC`YO&}9O3B+D=v;yKK!Q@1Y6@u(>-_M4P@I=b@CzZ zhMJeaTvv|$U8(3eXZ@+;NH3V5s;@rj*g(=QYap~-{>M7hY`IUwQbQDlR znckqX27QDU*hGf5OiYESC1`vs1unkCrI)eH6V+3dIcvn%C~y^9Jh{oN91?8}dS>UZ tVcWMt%@T!p>=V}hZW literal 0 HcmV?d00001 diff --git a/test_data/ClinitFailure.java b/test_data/ClinitFailure.java new file mode 100644 index 00000000..983fb4de --- /dev/null +++ b/test_data/ClinitFailure.java @@ -0,0 +1,41 @@ +public class ClinitFailure { + static boolean fail = true; + + static class Bad { + static { + if (fail) { + throw new RuntimeException("boom"); + } + } + + static void touch() {} + } + + static class BadError { + static { + if (fail) { + throw new LinkageError("boom"); + } + } + + static void touch() {} + } + + public static void main(String[] args) { + try { + Bad.touch(); + } catch (Throwable t) { + System.out.println(t.getClass().getName()); + } + try { + Bad.touch(); + } catch (Throwable t) { + System.out.println(t.getClass().getName()); + } + try { + BadError.touch(); + } catch (Throwable t) { + System.out.println(t.getClass().getName()); + } + } +} diff --git a/test_data/ClinitFailure.txt b/test_data/ClinitFailure.txt new file mode 100644 index 00000000..b61fb741 --- /dev/null +++ b/test_data/ClinitFailure.txt @@ -0,0 +1,3 @@ +java.lang.ExceptionInInitializerError +java.lang.NoClassDefFoundError +java.lang.LinkageError diff --git a/test_data/LazyClinit$Base.class b/test_data/LazyClinit$Base.class new file mode 100644 index 0000000000000000000000000000000000000000..4b175704b71c69eeab47b01b19d25318da28e38a GIT binary patch literal 480 zcmZvZ+e$(~7=_oQwGZvmG&`APP@qBi07_6o6o>_e_p@z~k?ri*GY0lnby3ho570wJ z|Lh_Qnu}RK|E&4e9Ns@(-vFFq&&M1x4zdB}vA~cY@q6BgxEeOD!jTvlhJ}_?(ws14 zYW2Pc7oGz@z#@V__TE$*F(y|$p-rVu3X*Z-I+4osOd|NW>0+6|3%M3Ymgysp6$ga? zMXWLu|JlOe+*v@Rh*|rBrl%vKRKsSy?_iA~*WnM-c4VRZXM|_Sw&PopTt_O=os2_~ z-0(1>j@uffKDvq`DJ(rBH?!N10=CFrMuya; Sl(C&^1-qm^0ZWuth5HE^eqg-- literal 0 HcmV?d00001 diff --git a/test_data/LazyClinit$Derived.class b/test_data/LazyClinit$Derived.class new file mode 100644 index 0000000000000000000000000000000000000000..4603518ddedd07b37a2d0f676698fe287f4ad4b2 GIT binary patch literal 550 zcmZuuZA-#X6n?I6cdnH-v$7Y?d-=|(|F}K+61p8O@p`Z`_fPPeT zt`imN!gkJ{^Xxp&IrsbL^9#Tw$~t0*b0iETkz&ZStd~jCv0YmpUs-`*NHx^Q1w*`2 z?WUn2&7m7ufx(b{vc^`;vD{&;GYO;^5z7lDL%ubw*k0}4w_VwhzOY7h4QmX>K=}4p z49-+r9a*e%mHx%!UGqN}=QkQEIjvaw5f40~ij#UkN=Ntwz^#K}`Ug7ypf;c2Y^ literal 0 HcmV?d00001 diff --git a/test_data/LazyClinit$GetstaticTarget.class b/test_data/LazyClinit$GetstaticTarget.class new file mode 100644 index 0000000000000000000000000000000000000000..0c6d93969177f79dae8e4016f8396c54de6f3128 GIT binary patch literal 541 zcmZvZ?Mebc6o%h%&0V+kBh5bSy9_iiFF*-Gs06V<@&9OptgN%KJ4SX_6%_QN3+SSv zGbtXH8EFN|!=~iYs^6SY=2PZjP?%N;cN8Zeqj1CW;LCe|!w)ou+t_z~wnP zYKpL~9Mvm*hIrGz6%3iSD@A8I8Hk|A2Oedt#xUxLTDH6XG#H8tSLZW}W>0lZQg%eB zuKZA$I3)4noWW^HDT1cQ!%&3e{WBfx(;#E?1O(Beq)7kK8&GD8q8-|q&YZk~JyF20 zO<#Sir4|W(q}9j@q^uX1&q(F+uP7`mX&7WN5GR8JlcL=Su}62%r~@3)ZWGv|xCHAP D4GL~m literal 0 HcmV?d00001 diff --git a/test_data/LazyClinit$IFace.class b/test_data/LazyClinit$IFace.class new file mode 100644 index 0000000000000000000000000000000000000000..5e4172541df9f295f43db58ef6948ffa029d486a GIT binary patch literal 317 zcmYL^%}T>S6ot=C+RSu}rndgB+_+GX_600-3g;ZzXQTg%99E zi8r(9z|8$QcfL9E`}h3=;1(we0YZUJj4mRAn(Ftrv?^`6B6OCzc_oAwmst-IJpmPC z2Qi_)9Xrn^x-bNK;++ZXvAWP3J+8EU8PD^DDHtWM3lF{VxJPEE^)uf}rlmEr^)ffj zgU&1NsQY@|6lPMkMY4To)Iu3FGj4TPJ7&h2wWdic?VNFht~U~ao$m7A2&b)ApGhR> rb7!BcGmh;NU!YHj4^9aKJ|iz&A;gfY00%hqF~AW=#j51)aU}l%C80T_ literal 0 HcmV?d00001 diff --git a/test_data/LazyClinit$Impl.class b/test_data/LazyClinit$Impl.class new file mode 100644 index 0000000000000000000000000000000000000000..401232ff2b844856c2253100bf20c697aa95e768 GIT binary patch literal 312 zcmYk1u};G<7=-U{+Bk$jLV>Xo0|PLS7eJLzq>4yYI}~+yf>ot*9TcZX@K&9W7z9z215gaLd)vX&n*)zYled9hYyOYom6Q?+A)cXz)O z2ng|1etl-z$+!9DT?>T#^H`k7QW5%D{li+%dtBIb=m3!6gE=OmO7~e{pv_e*gdg literal 0 HcmV?d00001 diff --git a/test_data/LazyClinit$NewTarget.class b/test_data/LazyClinit$NewTarget.class new file mode 100644 index 0000000000000000000000000000000000000000..a10a6ee186640748220dc81f3e5698d15017643a GIT binary patch literal 489 zcmZvZZ%YC}5XPTXbMLMzO}+kG1_c_FA3zC0hyt-d@%?IxteksscP93&>P0~>`T%{X zX!Z&vXkW|>v(NlycJ}k@{R6-yjvSmJ?D1Q!4V>2}lL?yHM%48;4R%8Wz?WEG=he2I(M=4UU3~ zB1#O!e<~RGgQ@p5A*(;kWHl1$XQQ>BS}MKTEV?!$hDQ0 zo#|vC!=4!Ul(Cw_L`;uzy1_IY$}45^xh3b^Ohd|!jN{Z?u+S%NJ23 zBL;iX7!`Q5RdbA`y@FwjqB41AGN*4~&lE6hlQq{sfi}Cdke>Vk>=*FD+ADHPx%n}m aL~;*l>{6V<9`+Mo#vyr!fF5 z$x3yQfr*R(%f>S7U-I5ORANdhKO$4H9fMrI)(u>#dMXfnS~rnp$j%}aDz4nonFd?P zVbwt1MgeOKg@1eu#!OQ@N#M#CIWCc~t{m5^0|V;}+im_ZZ+aSf-2J|z&x5g`g4jKc z7DKA(-wFn&?Ml&!rb7|*`Ou?`*%-xEdZ^X&qhKViT%C6o)N@@MDLW!m*M6uBlu5ie zXRupRilFK7Fccwq|CE6;iqvm_AlSerMS4e_r_2^bd$cm0Ieh_pq<~&UvpxnaGVT0G pPhu;Od4iE&d4{u)>pumuBzKU+F2xBV@5gHi2Qi8|i4qQBegZw5ZEFAk literal 0 HcmV?d00001 diff --git a/test_data/LazyClinit$SelfRef.class b/test_data/LazyClinit$SelfRef.class new file mode 100644 index 0000000000000000000000000000000000000000..657f61e2044dbda5702e9feb0371d8a67933800b GIT binary patch literal 458 zcmZ8eO-sW-6r7jVCXK7D^}BxH!9(l8f(l-WAO*2dDp>JsQn!*CQ)yB`f0GyOLGj=Z z@JETWK`eOKo#DNCv$MNj-yfd3B>1Wx>U|86i^go@3p>74VuQFq%d!$ z#bc{6xYmL~D~NP^bm!~gh3AK?IQ4Go%Y)s{U^MLNlfcvqlOuOcSSs5(PEH4LVq=Zs zNnfCvQKW~B(2HXo3+&&F*kTHc4`2})h0HM*B4)Vw5)dqqZ4kv&cHST_B!WfmrVZ?I zh}R=G(#_P6y;s=TtLJ~(Lu!})Y%A$Jpx!Yv#$2&FhCOjNUjpn?eZV-&R?`=XZJi`7Rf4=DQZerjO$}n_j)!qo zz_2&1yc|@f2BN6f5Hrz$MuCP)Gy>X=aws@7jUT&K(xavAY>oG=h0!cf>-m=J>uAAs z4Xq|_;HE%p+WNegEU2Bj=cP}IZ|7GncS|xu@rnPG%HWpTzFi;~PpJcTDvR3})yw4z zY7N~2jaBX$`SIA-KoWfH<++rD(By8xGNB;`kpH86buX? z5Jp14v>6WOSvy~D=orR`hWjQSsKZ68RdcK;kMl;yC?08eY~qRHRPUUXh0PVYNtUHp z<0d8)MdUQFx5l{}3y~~s&@Pg;9hoT?bJAV4as{pgiu%)g%} z7-3Fh%qodlq%nurm}enosi|Q!PmEDJ%3C>1%>r+DC*Ro%u1|Blg|07&s)%V~7jco; ZOUw`lh)cv_;&Mp&OyU*e1>y?Ae*v2P^Vt9Z literal 0 HcmV?d00001 diff --git a/test_data/LazyClinit.java b/test_data/LazyClinit.java new file mode 100644 index 00000000..6866f31d --- /dev/null +++ b/test_data/LazyClinit.java @@ -0,0 +1,72 @@ +public class LazyClinit { + static class GetstaticTarget { + static int x = 5; + + static { + System.out.println("getstatic-init"); + } + } + + static class PutstaticTarget { + static int x; + + static { + System.out.println("putstatic-init"); + } + } + + static class NewTarget { + static { + System.out.println("new-init"); + } + } + + static class Base { + static { + System.out.println("base-init"); + } + } + + static class Derived extends Base { + static { + System.out.println("derived-init"); + } + + static void touch() {} + } + + static int mark() { + System.out.println("iface-init"); + return 1; + } + + interface IFace { + int X = mark(); + } + + static class Impl implements IFace { + } + + static class SelfRef { + static int a = peek(); + static int b = 41; + + static int peek() { + return b + 1; + } + } + + public static void main(String[] args) { + System.out.println("start"); + System.out.println(GetstaticTarget.x); + PutstaticTarget.x = 9; + System.out.println(PutstaticTarget.x); + new NewTarget(); + Derived.touch(); + new Impl(); + System.out.println("impl-created"); + System.out.println(IFace.X); + System.out.println(SelfRef.a); + System.out.println(SelfRef.b); + } +} diff --git a/test_data/LazyClinit.txt b/test_data/LazyClinit.txt new file mode 100644 index 00000000..369f4d54 --- /dev/null +++ b/test_data/LazyClinit.txt @@ -0,0 +1,13 @@ +start +getstatic-init +5 +putstatic-init +9 +new-init +base-init +derived-init +impl-created +iface-init +1 +1 +41 diff --git a/test_data/unit/ClinitThrows.class b/test_data/unit/ClinitThrows.class deleted file mode 100644 index 6e2e1269ffed614cc3b979e4d2d8770a4a065cfa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 455 zcmZWlT}uK%6g{)My1MS>M`>jQJ!%j61Ck)5Ah3cIp~u~A&@p#Mw%PLM^cSiJp`hOT zQPCX>4H}p`mvhhDbMF28dVL3Qj2#CCOdFPq46+QRv48KkCVo6@T?S(wDu!%Z#6lf0 zn2qKjj~wzg92W(+3~pzli~Ug|AMO~~jP99@HHP}C(N!8N5%H5*$frukIENC06-XI5 z@KDjp>kQRKclD*G5)ls%n*$G0#LJFU_ZmQ9^t*gyRH=HD1T@PVlvD(I;<&W2LGV`HI#E0pgD~afP$pQulv4*T zs#8Us~Id~n6Y;ktLFKOgx0eaztz4EQgf1kW1 zb(ax+)@TLpy}hKhQw6}Y-eAANww~dBz`d=O7N~q_2_u!L!s0x`cb1*mjODSzhzY8( Jx>hr=_X|s$Ps#uQ diff --git a/test_data/unit/ClinitThrowsError.java b/test_data/unit/ClinitThrowsError.java deleted file mode 100644 index e015c82d..00000000 --- a/test_data/unit/ClinitThrowsError.java +++ /dev/null @@ -1,12 +0,0 @@ -class ClinitThrowsError { - static int x; - - static { - x = 1; - if (x == 1) { - throw new LinkageError("boom"); - } - } - - static void touch() {} -} diff --git a/test_data/unit/Counter.class b/test_data/unit/Counter.class deleted file mode 100644 index 9b313d28d46e24c925bfda7119bf6714ec1458f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 331 zcmZWlO>4qH6r7htH%a4HJ@p`X5o!4PrrWv#%Q}RU@BN1DyR~gTmQ=+#D20Kyaihw=7j1nN}_y3FrWKN z2R0l9*TVxmf|{g7lIx7%h^V{~XhyIm=}HqC^C;1aVi)M_!w+I+DtTIj8-hIy0<>y>$c^9^Nv{y^Pa<*y{%F2yaz1|H8~N-xO( diff --git a/test_data/unit/Counter.java b/test_data/unit/Counter.java deleted file mode 100644 index d437f84b..00000000 --- a/test_data/unit/Counter.java +++ /dev/null @@ -1,9 +0,0 @@ -class Counter { - static int initCount; - - static { - initCount++; - } - - static void touch() {} -} diff --git a/test_data/unit/InitBase.class b/test_data/unit/InitBase.class deleted file mode 100644 index 64a6ec317422be0d6a7524d375552074d9593ae2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 315 zcmYLF!AiqG5Pj3cZW1@PYHLpgkJf|zfg;#~P$>3b5xjMiE+r)hv=n_@kjCmWkqgytwKvg(%LT@9W> z1PB!(jbmtnn(}H^t{J6eT~yW)Xey!;jg~0F#Jt+MTUn=YN(jU-W{TiX$`y~+EGz6o z{gPO>FiFl#FiP{om(gQcyVTxi!pFbf8wrU^v^l$!bWRV#8KXW=66X37^v-~A&Ry1m i19bkRgTn*y2b8~j-a>5=|6AlXfCmkqja|kKT>Jo3Wi%E5 diff --git a/test_data/unit/InitBase.java b/test_data/unit/InitBase.java deleted file mode 100644 index e51e1dc1..00000000 --- a/test_data/unit/InitBase.java +++ /dev/null @@ -1,5 +0,0 @@ -class InitBase { - static { - InitLog.baseOrder = ++InitLog.counter; - } -} diff --git a/test_data/unit/InitDerived.class b/test_data/unit/InitDerived.class deleted file mode 100644 index da76ce6912c77b3b3764aa318cf6cfe8edcb927d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 363 zcmZXQ%Sr<=6o&uPnNFtD)K=?tA-J?I`UFMrf>0>BPy}~QXM#rR1maBJ%XT5S@Bw@% z@f<9z&;v<+{(L!?eE)oY0XV~cf*JyaP@|5B5DyDmo~Lhy5DhHrQ$lduyKf*yLm|;< zLKBo=BlpBAb5&6qPoSYd8yamPbZ)$zn*7GIP}n3i<;~@vO9(Gq&PCcITbS$Wb!_}y zI-amoFI|;Ab0>prvS=W=b(PP|m6iL>zcl@qbeiG_9Uej=;KgHru*K?-PZH< x(B)mOMaS4)q6h3qtYHyjJ4>v$sMDH3g^v$&s5x4z%<`Agght5ucUjl4_X{0pI8*=t diff --git a/test_data/unit/InitDerived.java b/test_data/unit/InitDerived.java deleted file mode 100644 index 10fc161f..00000000 --- a/test_data/unit/InitDerived.java +++ /dev/null @@ -1,7 +0,0 @@ -class InitDerived extends InitBase { - static { - InitLog.derivedOrder = ++InitLog.counter; - } - - static void touch() {} -} diff --git a/test_data/unit/InitLog.class b/test_data/unit/InitLog.class deleted file mode 100644 index 5d4000f289ff50e336932ae06a7f3118bb3c9705..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 253 zcmXYr%?g505QWcVdClza0fLs*B3~ea2m(QJQMAA6C2o|0sqfV)Xwd`oP|@6QGv}Od zVCH2#Qd0Q{MicPs_mkA0V#+e2x1o%_menCB#n?JzE|l&aM1_wp-Rs! zqO-Vj`0km*nal6_2f!4A01a3!><|u`3ejF4b)4&B7r!NYlU54NNmgXltAh3TwDIA= zcM*hWA>?kY%blUgIqJMN3Tok^tza*TPf~k!*b;V@Sz%W7M`FsgPI3}&lIGXc;QdgS zshMZOeR=mH9WcTJtI3hca$peEkhc=@e1iI-U};WSi}ujHS;zm3yD0sJd-uwp0gItF J9X;v>`hQTVDj5I( diff --git a/test_data/unit/Inst.java b/test_data/unit/Inst.java deleted file mode 100644 index f8135265..00000000 --- a/test_data/unit/Inst.java +++ /dev/null @@ -1,5 +0,0 @@ -class Inst { - static { - Target.value = 99; - } -} diff --git a/test_data/unit/Mark.class b/test_data/unit/Mark.class deleted file mode 100644 index e423593cadc4e33860f6a015888bb8b912de865f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 272 zcmX|6O>4qX5S&e(rit;R^(+)m+C%*VQYb>9pzWch_ZRz+Xp9hzzsgJTAb9Wx_@hc^ zEqHl5J2N}G@BDi_0eD6`f(JixwdCahdQq#Z9KQzz=MiZTxe$Pa;_!B^$~p(a|yYVBS)YG#kX?56(UW5@}NY tALwaM6MVnKuM%4NgRHYqfya|7vo=>9;t6^{+_@#x7|l#D80*XoG=3h|DD(gT diff --git a/test_data/unit/MarkIFace.java b/test_data/unit/MarkIFace.java deleted file mode 100644 index fcbc1bb3..00000000 --- a/test_data/unit/MarkIFace.java +++ /dev/null @@ -1,3 +0,0 @@ -interface MarkIFace { - int X = Mark.set(); -} diff --git a/test_data/unit/MarkImpl.class b/test_data/unit/MarkImpl.class deleted file mode 100644 index 0676fc447fe7f90a44e3456b7d7f062cf2216e53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 207 zcmX^0Z`VEs1_nn4el7+k24;2!79Ivx1~x_pfvm)`ME#t^ymWp4q^#8B5=I6#o6Nk- z5<5l)W)00Sb_Nbc1`glEqHNFHf*f`RE=C4UFwZS9IhB!t#W_C(Nb&n*=B4_T<|d^U zg(N2B07bZh^Gl18Q{6H_9A21yJ&!e1yQi#*-hMPYD}ag{wgopgW|y-;Exh# zLaCq&v-8;Z=FQB0eSdrcIKp-a2d;vr(ST2g#^%ZNGLzr-&f~F7N`l`{^Rzr9xSj4* z69JkEp~eC=_QR8B7*m5?R{M}kg64pRVg9E)NyfR39ZvKw`bKu zY$ul{&iJKK7FBXb2>MAj>lzM=YLeKKRCqn>cOYY8k8K@Z!{N-{i~<>pv&X^vfUw4B zhcn5`{u}6p0b!kQp#=vrFK3bN)`rZzLV4HEf3!YpH|Dl&X65{|lyx`fvQFIw(C=8B WVmazgp?-&u|CbGXxUAc%ar*~~J2IaD diff --git a/test_data/unit/SelfRef.java b/test_data/unit/SelfRef.java deleted file mode 100644 index e5a836de..00000000 --- a/test_data/unit/SelfRef.java +++ /dev/null @@ -1,10 +0,0 @@ -class SelfRef { - static int a = peek(); - static int b = 41; - - static int peek() { - return b + 1; - } - - static void touch() {} -} diff --git a/test_data/unit/Source.class b/test_data/unit/Source.class deleted file mode 100644 index 63f14faaa7ad9880a575d722d32f80e905228453..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 359 zcmZWk%}T>S7@SR-ZvNUvYyCM09%}{r0!0u(5TqU~y>H@DQ(^)ksm~=n2p)U@A4;4} zq#j(@{l1-4-p_F_gK_rqRADnxq!Iy(8oX`3bEJl3stT(O`MI&YU(;S#S_cmKF#&Keu;It zP;o}Ysfs#z7I0@t_WPdSn%|~|58BJ6$%iwhu$Z56lY^bj%?}XSXLrgf3^I9#cw-|l wb%7 diff --git a/test_data/unit/Source.java b/test_data/unit/Source.java deleted file mode 100644 index e22bc38a..00000000 --- a/test_data/unit/Source.java +++ /dev/null @@ -1,9 +0,0 @@ -class Source { - static int own = 7; - - static { - Target.value = 42; - } - - static void touch() {} -} diff --git a/test_data/unit/Target.class b/test_data/unit/Target.class deleted file mode 100644 index 0d0495f76ed6281e8a8f36ea1a60ee6bc039f2ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 206 zcmX9&OA3Ne6g}5}YS{*YhSeZ95J3b%5H(1eKkXq;%s|wAH3}NEfEE?qXOnw=9M1iG zUvB^#L_Q3dF02qX9KyH|orp_OKRPo@;X59Fo54P=s(yASWOs-(Ox`3 Qk3J6h>R6WuLq-FmAFgpDh5!Hn diff --git a/test_data/unit/Target.java b/test_data/unit/Target.java deleted file mode 100644 index 99ee082e..00000000 --- a/test_data/unit/Target.java +++ /dev/null @@ -1,3 +0,0 @@ -class Target { - static int value; -} diff --git a/tests/test_lazy_clinit.rs b/tests/test_lazy_clinit.rs deleted file mode 100644 index 2252bdb5..00000000 --- a/tests/test_lazy_clinit.rs +++ /dev/null @@ -1,172 +0,0 @@ -use jvm::Result; -use jvm_rust::ClassDefinitionImpl; - -use test_utils::test_jvm; - -#[tokio::test] -async fn test_clinit_runs_once() -> Result<()> { - let jvm = test_jvm().await?; - - let class = ClassDefinitionImpl::from_classfile(include_bytes!("../test_data/unit/Counter.class"))?; - jvm.register_class(Box::new(class), None).await?; - - let _: () = jvm.invoke_static("Counter", "touch", "()V", ()).await?; - let _: () = jvm.invoke_static("Counter", "touch", "()V", ()).await?; - - assert_eq!(jvm.get_static_field::("Counter", "initCount", "I").await?, 1); - - Ok(()) -} - -#[tokio::test] -async fn test_clinit_reentrancy() -> Result<()> { - let jvm = test_jvm().await?; - - let class = ClassDefinitionImpl::from_classfile(include_bytes!("../test_data/unit/SelfRef.class"))?; - jvm.register_class(Box::new(class), None).await?; - - let _: () = jvm.invoke_static("SelfRef", "touch", "()V", ()).await?; - - assert_eq!(jvm.get_static_field::("SelfRef", "a", "I").await?, 1); - assert_eq!(jvm.get_static_field::("SelfRef", "b", "I").await?, 41); - - Ok(()) -} - -async fn register(jvm: &jvm::Jvm, class_data: &[u8]) -> Result<()> { - let class = ClassDefinitionImpl::from_classfile(class_data)?; - jvm.register_class(Box::new(class), None).await?; - - Ok(()) -} - -#[tokio::test] -async fn test_clinit_not_run_at_registration() -> Result<()> { - let jvm = test_jvm().await?; - - register(&jvm, include_bytes!("../test_data/unit/Target.class")).await?; - register(&jvm, include_bytes!("../test_data/unit/Source.class")).await?; - - assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 0); - - let _: () = jvm.invoke_static("Source", "touch", "()V", ()).await?; - assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 42); - - Ok(()) -} - -#[tokio::test] -async fn test_getstatic_triggers_clinit() -> Result<()> { - let jvm = test_jvm().await?; - - register(&jvm, include_bytes!("../test_data/unit/Target.class")).await?; - register(&jvm, include_bytes!("../test_data/unit/Source.class")).await?; - - assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 0); - assert_eq!(jvm.get_static_field::("Source", "own", "I").await?, 7); - assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 42); - - Ok(()) -} - -#[tokio::test] -async fn test_putstatic_triggers_clinit() -> Result<()> { - let jvm = test_jvm().await?; - - register(&jvm, include_bytes!("../test_data/unit/Target.class")).await?; - register(&jvm, include_bytes!("../test_data/unit/Source.class")).await?; - - assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 0); - - jvm.put_static_field("Source", "own", "I", 100).await?; - assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 42); - assert_eq!(jvm.get_static_field::("Source", "own", "I").await?, 100); - - Ok(()) -} - -#[tokio::test] -async fn test_new_triggers_clinit() -> Result<()> { - let jvm = test_jvm().await?; - - register(&jvm, include_bytes!("../test_data/unit/Target.class")).await?; - register(&jvm, include_bytes!("../test_data/unit/Inst.class")).await?; - - assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 0); - - let _ = jvm.instantiate_class("Inst").await?; - assert_eq!(jvm.get_static_field::("Target", "value", "I").await?, 99); - - Ok(()) -} - -#[tokio::test] -async fn test_superclass_initialized_first() -> Result<()> { - let jvm = test_jvm().await?; - - register(&jvm, include_bytes!("../test_data/unit/InitLog.class")).await?; - register(&jvm, include_bytes!("../test_data/unit/InitBase.class")).await?; - register(&jvm, include_bytes!("../test_data/unit/InitDerived.class")).await?; - - assert_eq!(jvm.get_static_field::("InitLog", "counter", "I").await?, 0); - - let _: () = jvm.invoke_static("InitDerived", "touch", "()V", ()).await?; - assert_eq!(jvm.get_static_field::("InitLog", "baseOrder", "I").await?, 1); - assert_eq!(jvm.get_static_field::("InitLog", "derivedOrder", "I").await?, 2); - - Ok(()) -} - -#[tokio::test] -async fn test_interface_not_initialized_by_implementor() -> Result<()> { - let jvm = test_jvm().await?; - - register(&jvm, include_bytes!("../test_data/unit/Mark.class")).await?; - register(&jvm, include_bytes!("../test_data/unit/MarkIFace.class")).await?; - register(&jvm, include_bytes!("../test_data/unit/MarkImpl.class")).await?; - - let _ = jvm.instantiate_class("MarkImpl").await?; - assert_eq!(jvm.get_static_field::("Mark", "value", "I").await?, 0); - - assert_eq!(jvm.get_static_field::("MarkIFace", "X", "I").await?, 1); - assert_eq!(jvm.get_static_field::("Mark", "value", "I").await?, 1); - - Ok(()) -} - -#[tokio::test] -async fn test_clinit_failure_wrapped_and_class_erroneous() -> Result<()> { - let jvm = test_jvm().await?; - - register(&jvm, include_bytes!("../test_data/unit/ClinitThrows.class")).await?; - - let result: Result<()> = jvm.invoke_static("ClinitThrows", "touch", "()V", ()).await; - let Err(jvm::JavaError::JavaException(exception)) = result else { - panic!("Expected JavaException, got {:?}", result); - }; - assert!(jvm.is_instance(&*exception, "java/lang/ExceptionInInitializerError")); - - let result: Result<()> = jvm.invoke_static("ClinitThrows", "touch", "()V", ()).await; - let Err(jvm::JavaError::JavaException(exception)) = result else { - panic!("Expected JavaException, got {:?}", result); - }; - assert!(jvm.is_instance(&*exception, "java/lang/NoClassDefFoundError")); - - Ok(()) -} - -#[tokio::test] -async fn test_clinit_failure_error_propagates_unwrapped() -> Result<()> { - let jvm = test_jvm().await?; - - register(&jvm, include_bytes!("../test_data/unit/ClinitThrowsError.class")).await?; - - let result: Result<()> = jvm.invoke_static("ClinitThrowsError", "touch", "()V", ()).await; - let Err(jvm::JavaError::JavaException(exception)) = result else { - panic!("Expected JavaException, got {:?}", result); - }; - assert!(jvm.is_instance(&*exception, "java/lang/LinkageError")); - assert!(!jvm.is_instance(&*exception, "java/lang/ExceptionInInitializerError")); - - Ok(()) -} From d749a1f6c14a4200daa8d06d89e939e3c841830d Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 13 Jun 2026 08:41:06 +0900 Subject: [PATCH 08/10] Replace test_data/unit fixtures with txt-verified E2E tests Constants and StaticFlag move to test_data with mains and expected output generated by a real JVM. ConstantsReader is compiled against a non-final Constants so javac emits getstatic instead of inlining, keeping ConstantValue preparation observable from bytecode. The Rust-side putstatic typed-read assertion is dropped with test_data/unit; the narrowing fix itself is still exercised by StaticFlag's clinit. --- test_data/Constants.class | Bin 0 -> 834 bytes test_data/{unit => }/Constants.java | 12 +++++++++++- test_data/Constants.txt | 7 +++++++ test_data/ConstantsReader.class | Bin 0 -> 712 bytes test_data/ConstantsReader.java | 13 +++++++++++++ test_data/ConstantsReader.txt | 7 +++++++ test_data/StaticFlag.class | Bin 0 -> 665 bytes test_data/StaticFlag.java | 13 +++++++++++++ test_data/StaticFlag.txt | 4 ++++ test_data/unit/Constants.class | Bin 481 -> 0 bytes test_data/unit/StaticFlag.class | Bin 394 -> 0 bytes test_data/unit/StaticFlag.java | 6 ------ tests/test_constant_value.rs | 26 -------------------------- tests/test_putstatic.rs | 19 ------------------- 14 files changed, 55 insertions(+), 52 deletions(-) create mode 100644 test_data/Constants.class rename test_data/{unit => }/Constants.java (50%) create mode 100644 test_data/Constants.txt create mode 100644 test_data/ConstantsReader.class create mode 100644 test_data/ConstantsReader.java create mode 100644 test_data/ConstantsReader.txt create mode 100644 test_data/StaticFlag.class create mode 100644 test_data/StaticFlag.java create mode 100644 test_data/StaticFlag.txt delete mode 100644 test_data/unit/Constants.class delete mode 100644 test_data/unit/StaticFlag.class delete mode 100644 test_data/unit/StaticFlag.java delete mode 100644 tests/test_constant_value.rs delete mode 100644 tests/test_putstatic.rs diff --git a/test_data/Constants.class b/test_data/Constants.class new file mode 100644 index 0000000000000000000000000000000000000000..dd6b91b19949ff6902ad678b999bde573ce48db6 GIT binary patch literal 834 zcmZuv%Wl(96r3A7b`#PjP0~=PN*}!901a>2rt}pQ)G7i*Dlf{)DOPdi*pN6#d=@N> z!~(H~PeMY>NpKM=KC;iudE7fb_kRENzWz5&`hF<6%aXf7t<|yE(O*-k7aTV%n?0@e&Qk?-s4$sm zwjF=yY8;(nVpo7h&=lYhbOe|Ldje_%ZNB?_Tl&^Fc0oNcwsq$Ca8{jzmkPzR+jH|~ z_}W#-x4po9HtcoX{(;l=8PR)=$6-{eugTD+*_K)0_C&g2V?!k%-7fYDrc>@{D z;wr9D!gaC~s^k_e;WhFbw1nRzzeP*<3VEHD@Z02faQBqYk>8^w`TOJ#XbFEvzKXR| zzDNx`8W{YpQ_(!VqEFX8LwzJrXz(j(!5M7eF};-f&_>bKPcZcuc{(G;3|+yAF)QBu w!)s$-#$1efCW?zOmjAB4%)1Q^h_{rMM@8-rra7h}f3<{1D08Px)}*lY8*nRx5&!@I literal 0 HcmV?d00001 diff --git a/test_data/unit/Constants.java b/test_data/Constants.java similarity index 50% rename from test_data/unit/Constants.java rename to test_data/Constants.java index a45fe8fc..72c130d8 100644 --- a/test_data/unit/Constants.java +++ b/test_data/Constants.java @@ -1,4 +1,4 @@ -class Constants { +public class Constants { static final boolean FLAG = true; static final byte B = 3; static final char C = 'a'; @@ -8,4 +8,14 @@ class Constants { static final float F = 1.5f; static final double D = 2.5; static final String STR = "hello"; + + public static void main(String[] args) { + System.out.println(FLAG); + System.out.println(B); + System.out.println(C); + System.out.println(S); + System.out.println(I); + System.out.println(L); + System.out.println(STR); + } } diff --git a/test_data/Constants.txt b/test_data/Constants.txt new file mode 100644 index 00000000..c48c5353 --- /dev/null +++ b/test_data/Constants.txt @@ -0,0 +1,7 @@ +true +3 +a +7 +42 +1234567890123 +hello diff --git a/test_data/ConstantsReader.class b/test_data/ConstantsReader.class new file mode 100644 index 0000000000000000000000000000000000000000..7cdf771bc19990fe533b7a16a91413363236be38 GIT binary patch literal 712 zcmZuvYflqF6g|_HF1wC^<>3QSL4<7;Dn9U01Zoouq*22XKES6fOvsXU*KD^Y{4Dvf zCVb!r@S}+5Zi7Z;lbN~qo_o&SduJ{#&&~nVQFo9--ajN#o5-D2S?G;~-4DBsPAhW@8A$ z7DhB~p`>6pqOhCzVbWD7Y&7ei6x5!BF^pT7(725|3ZvKPC|KV_wH=b@?#W*b;hx4M zCaLgI6sCm!K;X?j{77RO(}WuWKNfga;6@*w)0oFRVNbNm8WmLNZHo6yV*!iwnp|5# ze=hVTp>Or+FEw7_HJkT#wiQbK=CdiX^B_E`3G0op-f}kX|9E-V!h40`-xJw3{-KFk zq7fY$0?i;apL?B_iFf=~n~_4t57_^NyMLXUg5yQK_`qxgl05d0?xIYU19`{WmoEn; z_s_jXZ;GelIZ*it^@E1O3U`5lJZyZxDs4$k=u33{6xJzjrWnnPyD9Ex#={gdnepT* u;$9g~Q&cnPMT+G=Rc83YSejLeD8b^T4q}$x0vs&!)vZCZ;u>>uSpNkAj)Ktu literal 0 HcmV?d00001 diff --git a/test_data/ConstantsReader.java b/test_data/ConstantsReader.java new file mode 100644 index 00000000..ef4e060c --- /dev/null +++ b/test_data/ConstantsReader.java @@ -0,0 +1,13 @@ +// compiled against a non-final Constants so javac emits getstatic instead of +// inlining the constants; run against the final Constants.class with ConstantValue attributes +public class ConstantsReader { + public static void main(String[] args) { + System.out.println(Constants.FLAG); + System.out.println(Constants.B); + System.out.println(Constants.C); + System.out.println(Constants.S); + System.out.println(Constants.I); + System.out.println(Constants.L); + System.out.println(Constants.STR); + } +} diff --git a/test_data/ConstantsReader.txt b/test_data/ConstantsReader.txt new file mode 100644 index 00000000..c48c5353 --- /dev/null +++ b/test_data/ConstantsReader.txt @@ -0,0 +1,7 @@ +true +3 +a +7 +42 +1234567890123 +hello diff --git a/test_data/StaticFlag.class b/test_data/StaticFlag.class new file mode 100644 index 0000000000000000000000000000000000000000..ae212ec21976775eb86214a256a8f6e5d937f2ac GIT binary patch literal 665 zcmZuu(M}UV6g@+?-R?4_wUsI=0s_(kQt`zHh)HdzFMbdrYYC{lwX4@)Sz zC`sHwS-=ldmFiwQR(%1d9W>tvh)W-LvFxHMaS!(eD*qu8a6cK^IN@CF(#TrE1Br)t zB#;eHn?XRP*M{k_p;v=#1m}NR&dhkoV+KlWpLXD_tfB8Gh9ugj$P8V&Oe-W z2n%`H8h(rfih)j|v+vrpV*+01^yX_d*+T<<&7(wDcfesV$OitkYn8AX_Z+pXLr9td}AT7&uFeq z?co^?m<{xl518)u4cr?Prl{CrWr|f>tWU9Jiye06R8_ua)Oh^L@TqDMTafHD{;GIs miGhkUK_NH6N=Z$yUUDbcnG=<14%Fb#LY}ceVu9zjclaB=qHC-G literal 0 HcmV?d00001 diff --git a/test_data/StaticFlag.java b/test_data/StaticFlag.java new file mode 100644 index 00000000..b495d9dc --- /dev/null +++ b/test_data/StaticFlag.java @@ -0,0 +1,13 @@ +public class StaticFlag { + static boolean FLAG = true; + static byte SMALL = 3; + static char LETTER = 'a'; + static short COUNT = 7; + + public static void main(String[] args) { + System.out.println(FLAG); + System.out.println(SMALL); + System.out.println(LETTER); + System.out.println(COUNT); + } +} diff --git a/test_data/StaticFlag.txt b/test_data/StaticFlag.txt new file mode 100644 index 00000000..749a5a48 --- /dev/null +++ b/test_data/StaticFlag.txt @@ -0,0 +1,4 @@ +true +3 +a +7 diff --git a/test_data/unit/Constants.class b/test_data/unit/Constants.class deleted file mode 100644 index e5ee9f51bd42a08daf77c3477c3dc31126e492b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 481 zcmYL`OHaZ;6ot>Fg#z*t-%s!vWg$O6qap^QX*98fg}Z_cmee*v@qf88abaBRPcku{ zF&UhbnR~u_Co?xQpFeN!02jvI_UgXaqiXUZ5fO=VmfM;VLeQkfc!&3TpN z{>sz0`9)}%Cm%$}dFs^UCJx^3v~LAv`@cme9{OSbFpUjC^1%y&hz&KPdrwfb{m{D} zKX$!g+wBI-WSwX{?0GF;K|%LAI#9=<3X2Z`WU$DY1ajm&rMW;ZQkqNTX-adM zJVR-oCC_1g!VBajO4pakE0pF{@;WLLuF^q$4dW)=8slH4_6m7nLa@bEmH*xg+l&e9 F`~r10KL!8* diff --git a/test_data/unit/StaticFlag.class b/test_data/unit/StaticFlag.class deleted file mode 100644 index 25ba38467bb2409b4aee0aaeca84d061768362db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 394 zcmYL^%}#?*5QWc_TcAhLiL^Xi76onJh4 zV4`Edvato5U`3Uz@-!&qoFIbGyCcxEGHu&%;1b%=y%&Z|Zx!v?=wp{>hLbp+JTUeZ z-nVgpLpJ-f$7#$o;%WZ!B_lXtUS`wvYm%*EnG{USaa#N(vZCdBm1aS%xUaE(pi`s&7@BIQh3;Hw@^D8irn@ d>&hBCu2Exe)7aw#YKMTv%>rHCML2EP;1~J-HK_mq diff --git a/test_data/unit/StaticFlag.java b/test_data/unit/StaticFlag.java deleted file mode 100644 index a9dc70bc..00000000 --- a/test_data/unit/StaticFlag.java +++ /dev/null @@ -1,6 +0,0 @@ -class StaticFlag { - static boolean FLAG = true; - static byte SMALL = 3; - static char LETTER = 'a'; - static short COUNT = 7; -} diff --git a/tests/test_constant_value.rs b/tests/test_constant_value.rs deleted file mode 100644 index 73a96143..00000000 --- a/tests/test_constant_value.rs +++ /dev/null @@ -1,26 +0,0 @@ -use jvm::{ClassInstance, JavaChar, Result, runtime::JavaLangString}; -use jvm_rust::ClassDefinitionImpl; - -use test_utils::test_jvm; - -#[tokio::test] -async fn test_constant_value_fields() -> Result<()> { - let jvm = test_jvm().await?; - - let class = ClassDefinitionImpl::from_classfile(include_bytes!("../test_data/unit/Constants.class"))?; - jvm.register_class(Box::new(class), None).await?; - - assert!(jvm.get_static_field::("Constants", "FLAG", "Z").await?); - assert_eq!(jvm.get_static_field::("Constants", "B", "B").await?, 3); - assert_eq!(jvm.get_static_field::("Constants", "C", "C").await?, 'a' as JavaChar); - assert_eq!(jvm.get_static_field::("Constants", "S", "S").await?, 7); - assert_eq!(jvm.get_static_field::("Constants", "I", "I").await?, 42); - assert_eq!(jvm.get_static_field::("Constants", "L", "J").await?, 1234567890123); - assert_eq!(jvm.get_static_field::("Constants", "F", "F").await?, 1.5); - assert_eq!(jvm.get_static_field::("Constants", "D", "D").await?, 2.5); - - let string: Box = jvm.get_static_field("Constants", "STR", "Ljava/lang/String;").await?; - assert_eq!(JavaLangString::to_rust_string(&jvm, &string).await?, "hello"); - - Ok(()) -} diff --git a/tests/test_putstatic.rs b/tests/test_putstatic.rs deleted file mode 100644 index 9e67a5c9..00000000 --- a/tests/test_putstatic.rs +++ /dev/null @@ -1,19 +0,0 @@ -use jvm::Result; -use jvm_rust::ClassDefinitionImpl; - -use test_utils::test_jvm; - -#[tokio::test] -async fn test_putstatic_narrows_to_field_type() -> Result<()> { - let jvm = test_jvm().await?; - - let class = ClassDefinitionImpl::from_classfile(include_bytes!("../test_data/unit/StaticFlag.class"))?; - jvm.register_class(Box::new(class), None).await?; - - assert!(jvm.get_static_field::("StaticFlag", "FLAG", "Z").await?); - assert_eq!(jvm.get_static_field::("StaticFlag", "SMALL", "B").await?, 3); - assert_eq!(jvm.get_static_field::("StaticFlag", "LETTER", "C").await?, 'a' as u16); - assert_eq!(jvm.get_static_field::("StaticFlag", "COUNT", "S").await?, 7); - - Ok(()) -} From 647fe76dae071f746fb492b6d22908550f29ac9e Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 13 Jun 2026 08:43:40 +0900 Subject: [PATCH 09/10] Move test fixture sources to test_data/src --- test_data/{ => src}/ClinitFailure.java | 0 test_data/{ => src}/Constants.java | 0 test_data/{ => src}/ConstantsReader.java | 0 test_data/{ => src}/DoubleInit.java | 0 test_data/{ => src}/InterfaceCast.java | 0 test_data/{ => src}/LazyClinit.java | 0 test_data/{ => src}/StaticFlag.java | 0 test_data/{ => src}/StaticOrder.java | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename test_data/{ => src}/ClinitFailure.java (100%) rename test_data/{ => src}/Constants.java (100%) rename test_data/{ => src}/ConstantsReader.java (100%) rename test_data/{ => src}/DoubleInit.java (100%) rename test_data/{ => src}/InterfaceCast.java (100%) rename test_data/{ => src}/LazyClinit.java (100%) rename test_data/{ => src}/StaticFlag.java (100%) rename test_data/{ => src}/StaticOrder.java (100%) diff --git a/test_data/ClinitFailure.java b/test_data/src/ClinitFailure.java similarity index 100% rename from test_data/ClinitFailure.java rename to test_data/src/ClinitFailure.java diff --git a/test_data/Constants.java b/test_data/src/Constants.java similarity index 100% rename from test_data/Constants.java rename to test_data/src/Constants.java diff --git a/test_data/ConstantsReader.java b/test_data/src/ConstantsReader.java similarity index 100% rename from test_data/ConstantsReader.java rename to test_data/src/ConstantsReader.java diff --git a/test_data/DoubleInit.java b/test_data/src/DoubleInit.java similarity index 100% rename from test_data/DoubleInit.java rename to test_data/src/DoubleInit.java diff --git a/test_data/InterfaceCast.java b/test_data/src/InterfaceCast.java similarity index 100% rename from test_data/InterfaceCast.java rename to test_data/src/InterfaceCast.java diff --git a/test_data/LazyClinit.java b/test_data/src/LazyClinit.java similarity index 100% rename from test_data/LazyClinit.java rename to test_data/src/LazyClinit.java diff --git a/test_data/StaticFlag.java b/test_data/src/StaticFlag.java similarity index 100% rename from test_data/StaticFlag.java rename to test_data/src/StaticFlag.java diff --git a/test_data/StaticOrder.java b/test_data/src/StaticOrder.java similarity index 100% rename from test_data/StaticOrder.java rename to test_data/src/StaticOrder.java From 870a1855d2e6f1383804bce949a86b57ca535fde Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 13 Jun 2026 16:34:55 +0900 Subject: [PATCH 10/10] Add Throwable cause and chained-exception constructors Throwable gets a cause field, getCause/initCause, and the (Throwable) and (String, Throwable) constructors, mirrored on Exception and RuntimeException. printStackTrace now walks the cause chain printing "Caused by:". ExceptionInInitializerError stores the original exception as its cause (no detail message, matching the JDK) and clinit wrapping wires it through that constructor instead of flattening the cause into the message string. --- .../src/classes/java/lang/exception.rs | 44 ++++++- .../lang/exception_in_initializer_error.rs | 24 +++- .../classes/java/lang/runtime_exception.rs | 44 ++++++- .../src/classes/java/lang/throwable.rs | 121 +++++++++++++++--- .../tests/classes/java/lang/test_throwable.rs | 38 ++++++ jvm/src/jvm.rs | 9 +- test_data/ClinitFailure.class | Bin 944 -> 1070 bytes test_data/ClinitFailure.txt | 3 + test_data/ThrowableCause.class | Bin 0 -> 1229 bytes test_data/ThrowableCause.txt | 7 + test_data/src/ClinitFailure.java | 3 + test_data/src/ThrowableCause.java | 21 +++ 12 files changed, 291 insertions(+), 23 deletions(-) create mode 100644 test_data/ThrowableCause.class create mode 100644 test_data/ThrowableCause.txt create mode 100644 test_data/src/ThrowableCause.java diff --git a/java_runtime/src/classes/java/lang/exception.rs b/java_runtime/src/classes/java/lang/exception.rs index cb9534d3..b949a13e 100644 --- a/java_runtime/src/classes/java/lang/exception.rs +++ b/java_runtime/src/classes/java/lang/exception.rs @@ -3,7 +3,10 @@ use alloc::vec; use java_class_proto::JavaMethodProto; use jvm::{ClassInstanceRef, Jvm, Result}; -use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{String, Throwable}, +}; // class java.lang.Exception pub struct Exception; @@ -17,6 +20,13 @@ impl Exception { methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/Throwable;)V", Self::init_with_cause, Default::default()), + JavaMethodProto::new( + "", + "(Ljava/lang/String;Ljava/lang/Throwable;)V", + Self::init_with_message_and_cause, + Default::default(), + ), ], fields: vec![], access_flags: Default::default(), @@ -40,4 +50,36 @@ impl Exception { Ok(()) } + + async fn init_with_cause(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, cause: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.Exception::({:?}, {:?})", &this, &cause); + + let _: () = jvm + .invoke_special(&this, "java/lang/Throwable", "", "(Ljava/lang/Throwable;)V", (cause,)) + .await?; + + Ok(()) + } + + async fn init_with_message_and_cause( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + message: ClassInstanceRef, + cause: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.lang.Exception::({:?}, {:?}, {:?})", &this, &message, &cause); + + let _: () = jvm + .invoke_special( + &this, + "java/lang/Throwable", + "", + "(Ljava/lang/String;Ljava/lang/Throwable;)V", + (message, cause), + ) + .await?; + + Ok(()) + } } diff --git a/java_runtime/src/classes/java/lang/exception_in_initializer_error.rs b/java_runtime/src/classes/java/lang/exception_in_initializer_error.rs index 27a1aa34..48bb9e4d 100644 --- a/java_runtime/src/classes/java/lang/exception_in_initializer_error.rs +++ b/java_runtime/src/classes/java/lang/exception_in_initializer_error.rs @@ -3,7 +3,10 @@ use alloc::vec; use java_class_proto::JavaMethodProto; use jvm::{ClassInstanceRef, Jvm, Result}; -use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{String, Throwable}, +}; // class java.lang.ExceptionInInitializerError pub struct ExceptionInInitializerError; @@ -17,6 +20,8 @@ impl ExceptionInInitializerError { methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/Throwable;)V", Self::init_with_cause, Default::default()), + JavaMethodProto::new("getException", "()Ljava/lang/Throwable;", Self::get_exception, Default::default()), ], fields: vec![], access_flags: Default::default(), @@ -40,4 +45,21 @@ impl ExceptionInInitializerError { Ok(()) } + + async fn init_with_cause(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, cause: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.ExceptionInInitializerError::({:?}, {:?})", &this, &cause); + + // unlike Throwable(Throwable), this keeps detailMessage null so toString is just the class name + let _: () = jvm.invoke_special(&this, "java/lang/LinkageError", "", "()V", ()).await?; + + jvm.put_field(&mut this, "cause", "Ljava/lang/Throwable;", cause).await?; + + Ok(()) + } + + async fn get_exception(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.lang.ExceptionInInitializerError::getException({:?})", &this); + + jvm.get_field(&this, "cause", "Ljava/lang/Throwable;").await + } } diff --git a/java_runtime/src/classes/java/lang/runtime_exception.rs b/java_runtime/src/classes/java/lang/runtime_exception.rs index c7362855..0fd27490 100644 --- a/java_runtime/src/classes/java/lang/runtime_exception.rs +++ b/java_runtime/src/classes/java/lang/runtime_exception.rs @@ -3,7 +3,10 @@ use alloc::vec; use java_class_proto::JavaMethodProto; use jvm::{ClassInstanceRef, Jvm, Result}; -use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{String, Throwable}, +}; // class java.lang.RuntimeException pub struct RuntimeException; @@ -17,6 +20,13 @@ impl RuntimeException { methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/Throwable;)V", Self::init_with_cause, Default::default()), + JavaMethodProto::new( + "", + "(Ljava/lang/String;Ljava/lang/Throwable;)V", + Self::init_with_message_and_cause, + Default::default(), + ), ], fields: vec![], access_flags: Default::default(), @@ -40,4 +50,36 @@ impl RuntimeException { Ok(()) } + + async fn init_with_cause(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, cause: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.RuntimeException::({:?}, {:?})", &this, &cause); + + let _: () = jvm + .invoke_special(&this, "java/lang/Exception", "", "(Ljava/lang/Throwable;)V", (cause,)) + .await?; + + Ok(()) + } + + async fn init_with_message_and_cause( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + message: ClassInstanceRef, + cause: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.lang.RuntimeException::({:?}, {:?}, {:?})", &this, &message, &cause); + + let _: () = jvm + .invoke_special( + &this, + "java/lang/Exception", + "", + "(Ljava/lang/String;Ljava/lang/Throwable;)V", + (message, cause), + ) + .await?; + + Ok(()) + } } diff --git a/java_runtime/src/classes/java/lang/throwable.rs b/java_runtime/src/classes/java/lang/throwable.rs index d00b0e74..7334a729 100644 --- a/java_runtime/src/classes/java/lang/throwable.rs +++ b/java_runtime/src/classes/java/lang/throwable.rs @@ -23,6 +23,20 @@ impl Throwable { methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/Throwable;)V", Self::init_with_cause, Default::default()), + JavaMethodProto::new( + "", + "(Ljava/lang/String;Ljava/lang/Throwable;)V", + Self::init_with_message_and_cause, + Default::default(), + ), + JavaMethodProto::new("getCause", "()Ljava/lang/Throwable;", Self::get_cause, Default::default()), + JavaMethodProto::new( + "initCause", + "(Ljava/lang/Throwable;)Ljava/lang/Throwable;", + Self::init_cause, + Default::default(), + ), JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), JavaMethodProto::new( "fillInStackTrace", @@ -46,6 +60,7 @@ impl Throwable { ], fields: vec![ JavaFieldProto::new("detailMessage", "Ljava/lang/String;", Default::default()), + JavaFieldProto::new("cause", "Ljava/lang/Throwable;", Default::default()), JavaFieldProto::new("stackTrace", "[Ljava/lang/String;", Default::default()), ], access_flags: Default::default(), @@ -74,6 +89,62 @@ impl Throwable { Ok(()) } + async fn init_with_cause(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, cause: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.Throwable::({:?}, {:?})", &this, &cause); + + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + + let message: ClassInstanceRef = if cause.is_null() { + None.into() + } else { + jvm.invoke_virtual(&cause, "toString", "()Ljava/lang/String;", ()).await? + }; + jvm.put_field(&mut this, "detailMessage", "Ljava/lang/String;", message).await?; + jvm.put_field(&mut this, "cause", "Ljava/lang/Throwable;", cause).await?; + + let _: ClassInstanceRef = jvm.invoke_virtual(&this, "fillInStackTrace", "()Ljava/lang/Throwable;", ()).await?; + + Ok(()) + } + + async fn init_with_message_and_cause( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + message: ClassInstanceRef, + cause: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.lang.Throwable::({:?}, {:?}, {:?})", &this, &message, &cause); + + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + + jvm.put_field(&mut this, "detailMessage", "Ljava/lang/String;", message).await?; + jvm.put_field(&mut this, "cause", "Ljava/lang/Throwable;", cause).await?; + + let _: ClassInstanceRef = jvm.invoke_virtual(&this, "fillInStackTrace", "()Ljava/lang/Throwable;", ()).await?; + + Ok(()) + } + + async fn get_cause(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.lang.Throwable::getCause({:?})", &this); + + jvm.get_field(&this, "cause", "Ljava/lang/Throwable;").await + } + + async fn init_cause( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + cause: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.lang.Throwable::initCause({:?}, {:?})", &this, &cause); + + jvm.put_field(&mut this, "cause", "Ljava/lang/Throwable;", cause).await?; + + Ok(this) + } + async fn fill_in_stack_trace(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result> { tracing::debug!("java.lang.Throwable::fillInStackTrace({:?})", &this); @@ -150,23 +221,41 @@ impl Throwable { } async fn do_print_stack_trace(jvm: &Jvm, this: ClassInstanceRef, stream_or_writer: Box) -> Result<()> { - let stack_trace: ClassInstanceRef>> = jvm.get_field(&this, "stackTrace", "[Ljava/lang/String;").await?; - - // TODO we can call println(Ljava/lang/Object;)V - let string: ClassInstanceRef = jvm.invoke_virtual(&this, "toString", "()Ljava/lang/String;", ()).await?; - let _: () = jvm - .invoke_virtual(&stream_or_writer, "println", "(Ljava/lang/String;)V", (string,)) - .await?; - - if !stack_trace.is_null() { - let length = jvm.array_length(&stack_trace).await?; - let lines: Vec> = jvm.load_array(&stack_trace, 0, length).await?; - for line_ref in lines { - let line = JavaLangString::to_rust_string(jvm, &line_ref).await?; - let line = format!("\tat {line}"); - let line = JavaLangString::from_rust_string(jvm, &line).await?; - let _: () = jvm.invoke_virtual(&stream_or_writer, "println", "(Ljava/lang/String;)V", (line,)).await?; + let mut current: ClassInstanceRef = this; + let mut header: Option<&str> = None; + + // a malformed initCause could create a cycle, so cap the depth + for _ in 0..32 { + let string: ClassInstanceRef = jvm.invoke_virtual(¤t, "toString", "()Ljava/lang/String;", ()).await?; + let prefix: ClassInstanceRef = match header { + Some(x) => { + let string = JavaLangString::to_rust_string(jvm, &string).await?; + JavaLangString::from_rust_string(jvm, &format!("{x}{string}")).await?.into() + } + None => string, + }; + let _: () = jvm + .invoke_virtual(&stream_or_writer, "println", "(Ljava/lang/String;)V", (prefix,)) + .await?; + + let stack_trace: ClassInstanceRef>> = jvm.get_field(¤t, "stackTrace", "[Ljava/lang/String;").await?; + if !stack_trace.is_null() { + let length = jvm.array_length(&stack_trace).await?; + let lines: Vec> = jvm.load_array(&stack_trace, 0, length).await?; + for line_ref in lines { + let line = JavaLangString::to_rust_string(jvm, &line_ref).await?; + let line = format!("\tat {line}"); + let line = JavaLangString::from_rust_string(jvm, &line).await?; + let _: () = jvm.invoke_virtual(&stream_or_writer, "println", "(Ljava/lang/String;)V", (line,)).await?; + } + } + + let cause: ClassInstanceRef = jvm.invoke_virtual(¤t, "getCause", "()Ljava/lang/Throwable;", ()).await?; + if cause.is_null() { + break; } + current = cause; + header = Some("Caused by: "); } Ok(()) diff --git a/java_runtime/tests/classes/java/lang/test_throwable.rs b/java_runtime/tests/classes/java/lang/test_throwable.rs index 7d59e1ea..abe3e4a9 100644 --- a/java_runtime/tests/classes/java/lang/test_throwable.rs +++ b/java_runtime/tests/classes/java/lang/test_throwable.rs @@ -55,3 +55,41 @@ async fn test_stacktrace() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_print_stack_trace_with_cause() -> Result<()> { + let jvm = test_jvm().await?; + + let cause_message = JavaLangString::from_rust_string(&jvm, "root").await?; + let cause = jvm + .new_class("java/lang/RuntimeException", "(Ljava/lang/String;)V", (cause_message,)) + .await?; + + let message = JavaLangString::from_rust_string(&jvm, "wrapper").await?; + let throwable = jvm + .new_class( + "java/lang/RuntimeException", + "(Ljava/lang/String;Ljava/lang/Throwable;)V", + (message, cause), + ) + .await?; + + let string_writer = jvm.new_class("java/io/StringWriter", "()V", ()).await?; + let print_writer = jvm + .new_class("java/io/PrintWriter", "(Ljava/io/Writer;)V", (string_writer.clone(),)) + .await?; + + let _: () = jvm + .invoke_virtual(&throwable, "printStackTrace", "(Ljava/io/PrintWriter;)V", (print_writer,)) + .await?; + + let result: ClassInstanceRef = jvm.invoke_virtual(&string_writer, "toString", "()Ljava/lang/String;", ()).await?; + let result = JavaLangString::to_rust_string(&jvm, &result).await?; + + assert_eq!( + result, + "java.lang.RuntimeException: wrapper\nCaused by: java.lang.RuntimeException: root\n" + ); + + Ok(()) +} diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index b4f9ba8e..e9b42e40 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -737,11 +737,12 @@ impl Jvm { return Err(err); } - // Throwable has no cause field, so carry the original exception as the message - let message: Box = self.invoke_virtual(exception, "toString", "()Ljava/lang/String;", ()).await?; - let message = JavaLangString::to_rust_string(self, &message).await?; + let cause = clone_box(&**exception); + let wrapped = self + .new_class("java/lang/ExceptionInInitializerError", "(Ljava/lang/Throwable;)V", (cause,)) + .await?; - return Err(self.exception("java/lang/ExceptionInInitializerError", &message).await); + return Err(JavaError::JavaException(wrapped)); } } diff --git a/test_data/ClinitFailure.class b/test_data/ClinitFailure.class index 26a59d6770d46777a7c2227b9809c1f36d669d54..6fd61b21a883c9fb82eb3aaca5a2f397e5aa3071 100644 GIT binary patch delta 501 zcmZWl%Sr+P6g@NL%s5SvVGpt_N^?fdDa*37HwA)Vi>Ot>7=t2GR8~tr!bP9pF0#O) zMM2B{q92LQ=q77(&V8J7@8v$o_lo-Y{rU!A3!4qq5{@(kHOwF+Kq=Uj9qana7BH+E z*^+f*nPuy&U>+1N&u^`K*-ojLfT3XuVF82sFG((6vQH~1<4{EuZW*(+3&Gc8Vot`q zhBy{#cfv>2gvia@h2fKOTJ ncm-u}bw45Y_Z1@7ud*p}O`0q*iqgKuc!0426YIn%c>~H1Rs~0% delta 356 zcmY+8%}T>i6ols{Hc3mmP(p=j@Q0?YZLQX7RZu~t#kFq`&_$pq1YP?Oc@Hrt2rgW> z^htaQ@k<54&CJ}nXU?2Q@5!xve!sm-I@MubiA^t3*;Ui1c0NsRlDkQqOz+d{##wwd zPSe}8thP2>b$oTz%f8IVWm^_;Bd>5!Ho7DS+~gI)1}=48W%AizA1uC$P+42rhKtb` z78R?Gvw>a9hIU{Y*@YcG1X@&GKE+-2VGEQ-s)BXuLbff*)v^MuFuRVsinB(dL8irg zL{C#kI0I7iawe|)>i7@1e&^rKOsO>YtX6)4GPBCxqb(|jc$Rjx2Ro>U{0O~(_9)p0 IBltkx4?(IV=>Px# diff --git a/test_data/ClinitFailure.txt b/test_data/ClinitFailure.txt index b61fb741..95ce7127 100644 --- a/test_data/ClinitFailure.txt +++ b/test_data/ClinitFailure.txt @@ -1,3 +1,6 @@ java.lang.ExceptionInInitializerError +java.lang.RuntimeException +java.lang.RuntimeException: boom +java.lang.ExceptionInInitializerError java.lang.NoClassDefFoundError java.lang.LinkageError diff --git a/test_data/ThrowableCause.class b/test_data/ThrowableCause.class new file mode 100644 index 0000000000000000000000000000000000000000..7dcc5912997e0d5b3756d38cf8ef90da5a1c8b85 GIT binary patch literal 1229 zcmZ`(T~8BH5Ixgwd)s9x(DE&)Ma4pa`h`kC5Qruu1&t*d)R%3!DU0ovEL*=4-+cE~ z|A0R015G6H2l%7Jxm#?vq&CgY?aY}oXXeiK_n)7?0IXtNM*u+q%|HlYLhru$+RT(q zyOeoU*q23@5MHuu%Uvb}Q&Ud`B7}(+Xro+~C9}Nhlxh`eyAR$L<$-He?Fe*2$g*we z=nyazeK%nsm22hZUB|LZi;A>|Fxm<{soAbokv_6MCacz5MHWLsk@XX%JCOP4xA&at z8?#WBTp@}<3<(Sy7{O>;m-%;xuB>o#Owaln`BpXan6bWA0A+1I-Lm2jtq{-`zc&@FcZ?@N%r(il#$1IEpZgMNdJ=3z~u97@&;5P0MqG}0F zrOo`ON=Ll{3xrrx%PEDnUfty;dvaW7t5zvUXIotrOQ~>a!eHu!kFB{Wgzmg+7GG_e z2OeJFKDX5YO3?Gwnp2byEybE>uRo`55J}wSGLDoy{F(T!hY}>@=h@?k#AttYi{i?FBuG_ literal 0 HcmV?d00001 diff --git a/test_data/ThrowableCause.txt b/test_data/ThrowableCause.txt new file mode 100644 index 00000000..7adb56de --- /dev/null +++ b/test_data/ThrowableCause.txt @@ -0,0 +1,7 @@ +java.lang.RuntimeException: outer +java.lang.IllegalArgumentException: inner +true +java.lang.RuntimeException: java.lang.IllegalArgumentException: inner +true +null +true diff --git a/test_data/src/ClinitFailure.java b/test_data/src/ClinitFailure.java index 983fb4de..47e26a6b 100644 --- a/test_data/src/ClinitFailure.java +++ b/test_data/src/ClinitFailure.java @@ -26,6 +26,9 @@ public static void main(String[] args) { Bad.touch(); } catch (Throwable t) { System.out.println(t.getClass().getName()); + System.out.println(t.getCause().getClass().getName()); + System.out.println(t.getCause()); + System.out.println(t); } try { Bad.touch(); diff --git a/test_data/src/ThrowableCause.java b/test_data/src/ThrowableCause.java new file mode 100644 index 00000000..829b334c --- /dev/null +++ b/test_data/src/ThrowableCause.java @@ -0,0 +1,21 @@ +public class ThrowableCause { + public static void main(String[] args) { + Throwable inner = new IllegalArgumentException("inner"); + + Throwable withMsg = new RuntimeException("outer", inner); + System.out.println(withMsg); + System.out.println(withMsg.getCause()); + System.out.println(withMsg.getCause() == inner); + + Throwable causeOnly = new RuntimeException(inner); + System.out.println(causeOnly); + System.out.println(causeOnly.getCause() == inner); + + Throwable bare = new java.lang.Exception("bare"); + System.out.println(bare.getCause()); + + Throwable chained = new java.lang.Exception("chained"); + chained.initCause(inner); + System.out.println(chained.getCause() == inner); + } +}