Skip to content

Commit 07fc404

Browse files
authored
Initialize classes lazily per JVMS 5.5 (#160)
* 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. * 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. * 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. * 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. * Style * 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. * 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. * 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. * Move test fixture sources to test_data/src * 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.
1 parent ebd9c03 commit 07fc404

52 files changed

Lines changed: 731 additions & 67 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

java_runtime/src/classes/java/lang.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod cloneable;
99
mod comparable;
1010
mod error;
1111
mod exception;
12+
mod exception_in_initializer_error;
1213
mod illegal_argument_exception;
1314
mod incompatible_class_change_error;
1415
mod index_out_of_bounds_exception;
@@ -40,12 +41,12 @@ pub use self::{
4041
abstract_method_error::AbstractMethodError, arithmetic_exception::ArithmeticException,
4142
array_index_out_of_bounds_exception::ArrayIndexOutOfBoundsException, class::Class, class_cast_exception::ClassCastException,
4243
class_loader::ClassLoader, clone_not_supported_exception::CloneNotSupportedException, cloneable::Cloneable, comparable::Comparable, error::Error,
43-
exception::Exception, illegal_argument_exception::IllegalArgumentException, incompatible_class_change_error::IncompatibleClassChangeError,
44-
index_out_of_bounds_exception::IndexOutOfBoundsException, instantiation_error::InstantiationError, integer::Integer,
45-
interrupted_exception::InterruptedException, linkage_error::LinkageError, math::Math, negative_array_size_exception::NegativeArraySizeException,
46-
no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError, no_such_method_error::NoSuchMethodError,
47-
null_pointer_exception::NullPointerException, number_format_exception::NumberFormatException, object::Object, runnable::Runnable,
48-
runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, string::String, string_buffer::StringBuffer,
49-
string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, throwable::Throwable,
50-
unsupported_operation_exception::UnsupportedOperationException,
44+
exception::Exception, exception_in_initializer_error::ExceptionInInitializerError, illegal_argument_exception::IllegalArgumentException,
45+
incompatible_class_change_error::IncompatibleClassChangeError, index_out_of_bounds_exception::IndexOutOfBoundsException,
46+
instantiation_error::InstantiationError, integer::Integer, interrupted_exception::InterruptedException, linkage_error::LinkageError, math::Math,
47+
negative_array_size_exception::NegativeArraySizeException, no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError,
48+
no_such_method_error::NoSuchMethodError, null_pointer_exception::NullPointerException, number_format_exception::NumberFormatException,
49+
object::Object, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, string::String,
50+
string_buffer::StringBuffer, string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread,
51+
throwable::Throwable, unsupported_operation_exception::UnsupportedOperationException,
5152
};

java_runtime/src/classes/java/lang/class.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ impl Class {
6363
tracing::debug!("java.lang.Class::getName({:?})", &this);
6464

6565
let rust_class = JavaLangClass::to_rust_class(jvm, &this).await?;
66-
let result = JavaLangString::from_rust_string(jvm, &rust_class.name()).await?;
66+
let result = JavaLangString::from_rust_string(jvm, &rust_class.name().replace('/', ".")).await?;
6767

6868
Ok(result.into())
6969
}

java_runtime/src/classes/java/lang/exception.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use alloc::vec;
33
use java_class_proto::JavaMethodProto;
44
use jvm::{ClassInstanceRef, Jvm, Result};
55

6-
use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String};
6+
use crate::{
7+
RuntimeClassProto, RuntimeContext,
8+
classes::java::lang::{String, Throwable},
9+
};
710

811
// class java.lang.Exception
912
pub struct Exception;
@@ -17,6 +20,13 @@ impl Exception {
1720
methods: vec![
1821
JavaMethodProto::new("<init>", "()V", Self::init, Default::default()),
1922
JavaMethodProto::new("<init>", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()),
23+
JavaMethodProto::new("<init>", "(Ljava/lang/Throwable;)V", Self::init_with_cause, Default::default()),
24+
JavaMethodProto::new(
25+
"<init>",
26+
"(Ljava/lang/String;Ljava/lang/Throwable;)V",
27+
Self::init_with_message_and_cause,
28+
Default::default(),
29+
),
2030
],
2131
fields: vec![],
2232
access_flags: Default::default(),
@@ -40,4 +50,36 @@ impl Exception {
4050

4151
Ok(())
4252
}
53+
54+
async fn init_with_cause(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>, cause: ClassInstanceRef<Throwable>) -> Result<()> {
55+
tracing::debug!("java.lang.Exception::<init>({:?}, {:?})", &this, &cause);
56+
57+
let _: () = jvm
58+
.invoke_special(&this, "java/lang/Throwable", "<init>", "(Ljava/lang/Throwable;)V", (cause,))
59+
.await?;
60+
61+
Ok(())
62+
}
63+
64+
async fn init_with_message_and_cause(
65+
jvm: &Jvm,
66+
_: &mut RuntimeContext,
67+
this: ClassInstanceRef<Self>,
68+
message: ClassInstanceRef<String>,
69+
cause: ClassInstanceRef<Throwable>,
70+
) -> Result<()> {
71+
tracing::debug!("java.lang.Exception::<init>({:?}, {:?}, {:?})", &this, &message, &cause);
72+
73+
let _: () = jvm
74+
.invoke_special(
75+
&this,
76+
"java/lang/Throwable",
77+
"<init>",
78+
"(Ljava/lang/String;Ljava/lang/Throwable;)V",
79+
(message, cause),
80+
)
81+
.await?;
82+
83+
Ok(())
84+
}
4385
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use alloc::vec;
2+
3+
use java_class_proto::JavaMethodProto;
4+
use jvm::{ClassInstanceRef, Jvm, Result};
5+
6+
use crate::{
7+
RuntimeClassProto, RuntimeContext,
8+
classes::java::lang::{String, Throwable},
9+
};
10+
11+
// class java.lang.ExceptionInInitializerError
12+
pub struct ExceptionInInitializerError;
13+
14+
impl ExceptionInInitializerError {
15+
pub fn as_proto() -> RuntimeClassProto {
16+
RuntimeClassProto {
17+
name: "java/lang/ExceptionInInitializerError",
18+
parent_class: Some("java/lang/LinkageError"),
19+
interfaces: vec![],
20+
methods: vec![
21+
JavaMethodProto::new("<init>", "()V", Self::init, Default::default()),
22+
JavaMethodProto::new("<init>", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()),
23+
JavaMethodProto::new("<init>", "(Ljava/lang/Throwable;)V", Self::init_with_cause, Default::default()),
24+
JavaMethodProto::new("getException", "()Ljava/lang/Throwable;", Self::get_exception, Default::default()),
25+
],
26+
fields: vec![],
27+
access_flags: Default::default(),
28+
}
29+
}
30+
31+
async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>) -> Result<()> {
32+
tracing::debug!("java.lang.ExceptionInInitializerError::<init>({:?})", &this);
33+
34+
let _: () = jvm.invoke_special(&this, "java/lang/LinkageError", "<init>", "()V", ()).await?;
35+
36+
Ok(())
37+
}
38+
39+
async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>, message: ClassInstanceRef<String>) -> Result<()> {
40+
tracing::debug!("java.lang.ExceptionInInitializerError::<init>({:?}, {:?})", &this, &message);
41+
42+
let _: () = jvm
43+
.invoke_special(&this, "java/lang/LinkageError", "<init>", "(Ljava/lang/String;)V", (message,))
44+
.await?;
45+
46+
Ok(())
47+
}
48+
49+
async fn init_with_cause(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef<Self>, cause: ClassInstanceRef<Throwable>) -> Result<()> {
50+
tracing::debug!("java.lang.ExceptionInInitializerError::<init>({:?}, {:?})", &this, &cause);
51+
52+
// unlike Throwable(Throwable), this keeps detailMessage null so toString is just the class name
53+
let _: () = jvm.invoke_special(&this, "java/lang/LinkageError", "<init>", "()V", ()).await?;
54+
55+
jvm.put_field(&mut this, "cause", "Ljava/lang/Throwable;", cause).await?;
56+
57+
Ok(())
58+
}
59+
60+
async fn get_exception(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>) -> Result<ClassInstanceRef<Throwable>> {
61+
tracing::debug!("java.lang.ExceptionInInitializerError::getException({:?})", &this);
62+
63+
jvm.get_field(&this, "cause", "Ljava/lang/Throwable;").await
64+
}
65+
}

java_runtime/src/classes/java/lang/runtime_exception.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use alloc::vec;
33
use java_class_proto::JavaMethodProto;
44
use jvm::{ClassInstanceRef, Jvm, Result};
55

6-
use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String};
6+
use crate::{
7+
RuntimeClassProto, RuntimeContext,
8+
classes::java::lang::{String, Throwable},
9+
};
710

811
// class java.lang.RuntimeException
912
pub struct RuntimeException;
@@ -17,6 +20,13 @@ impl RuntimeException {
1720
methods: vec![
1821
JavaMethodProto::new("<init>", "()V", Self::init, Default::default()),
1922
JavaMethodProto::new("<init>", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()),
23+
JavaMethodProto::new("<init>", "(Ljava/lang/Throwable;)V", Self::init_with_cause, Default::default()),
24+
JavaMethodProto::new(
25+
"<init>",
26+
"(Ljava/lang/String;Ljava/lang/Throwable;)V",
27+
Self::init_with_message_and_cause,
28+
Default::default(),
29+
),
2030
],
2131
fields: vec![],
2232
access_flags: Default::default(),
@@ -40,4 +50,36 @@ impl RuntimeException {
4050

4151
Ok(())
4252
}
53+
54+
async fn init_with_cause(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>, cause: ClassInstanceRef<Throwable>) -> Result<()> {
55+
tracing::debug!("java.lang.RuntimeException::<init>({:?}, {:?})", &this, &cause);
56+
57+
let _: () = jvm
58+
.invoke_special(&this, "java/lang/Exception", "<init>", "(Ljava/lang/Throwable;)V", (cause,))
59+
.await?;
60+
61+
Ok(())
62+
}
63+
64+
async fn init_with_message_and_cause(
65+
jvm: &Jvm,
66+
_: &mut RuntimeContext,
67+
this: ClassInstanceRef<Self>,
68+
message: ClassInstanceRef<String>,
69+
cause: ClassInstanceRef<Throwable>,
70+
) -> Result<()> {
71+
tracing::debug!("java.lang.RuntimeException::<init>({:?}, {:?}, {:?})", &this, &message, &cause);
72+
73+
let _: () = jvm
74+
.invoke_special(
75+
&this,
76+
"java/lang/Exception",
77+
"<init>",
78+
"(Ljava/lang/String;Ljava/lang/Throwable;)V",
79+
(message, cause),
80+
)
81+
.await?;
82+
83+
Ok(())
84+
}
4385
}

java_runtime/src/classes/java/lang/throwable.rs

Lines changed: 105 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ impl Throwable {
2323
methods: vec![
2424
JavaMethodProto::new("<init>", "()V", Self::init, Default::default()),
2525
JavaMethodProto::new("<init>", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()),
26+
JavaMethodProto::new("<init>", "(Ljava/lang/Throwable;)V", Self::init_with_cause, Default::default()),
27+
JavaMethodProto::new(
28+
"<init>",
29+
"(Ljava/lang/String;Ljava/lang/Throwable;)V",
30+
Self::init_with_message_and_cause,
31+
Default::default(),
32+
),
33+
JavaMethodProto::new("getCause", "()Ljava/lang/Throwable;", Self::get_cause, Default::default()),
34+
JavaMethodProto::new(
35+
"initCause",
36+
"(Ljava/lang/Throwable;)Ljava/lang/Throwable;",
37+
Self::init_cause,
38+
Default::default(),
39+
),
2640
JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()),
2741
JavaMethodProto::new(
2842
"fillInStackTrace",
@@ -46,6 +60,7 @@ impl Throwable {
4660
],
4761
fields: vec![
4862
JavaFieldProto::new("detailMessage", "Ljava/lang/String;", Default::default()),
63+
JavaFieldProto::new("cause", "Ljava/lang/Throwable;", Default::default()),
4964
JavaFieldProto::new("stackTrace", "[Ljava/lang/String;", Default::default()),
5065
],
5166
access_flags: Default::default(),
@@ -74,6 +89,62 @@ impl Throwable {
7489
Ok(())
7590
}
7691

92+
async fn init_with_cause(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef<Self>, cause: ClassInstanceRef<Self>) -> Result<()> {
93+
tracing::debug!("java.lang.Throwable::<init>({:?}, {:?})", &this, &cause);
94+
95+
let _: () = jvm.invoke_special(&this, "java/lang/Object", "<init>", "()V", ()).await?;
96+
97+
let message: ClassInstanceRef<String> = if cause.is_null() {
98+
None.into()
99+
} else {
100+
jvm.invoke_virtual(&cause, "toString", "()Ljava/lang/String;", ()).await?
101+
};
102+
jvm.put_field(&mut this, "detailMessage", "Ljava/lang/String;", message).await?;
103+
jvm.put_field(&mut this, "cause", "Ljava/lang/Throwable;", cause).await?;
104+
105+
let _: ClassInstanceRef<Self> = jvm.invoke_virtual(&this, "fillInStackTrace", "()Ljava/lang/Throwable;", ()).await?;
106+
107+
Ok(())
108+
}
109+
110+
async fn init_with_message_and_cause(
111+
jvm: &Jvm,
112+
_: &mut RuntimeContext,
113+
mut this: ClassInstanceRef<Self>,
114+
message: ClassInstanceRef<String>,
115+
cause: ClassInstanceRef<Self>,
116+
) -> Result<()> {
117+
tracing::debug!("java.lang.Throwable::<init>({:?}, {:?}, {:?})", &this, &message, &cause);
118+
119+
let _: () = jvm.invoke_special(&this, "java/lang/Object", "<init>", "()V", ()).await?;
120+
121+
jvm.put_field(&mut this, "detailMessage", "Ljava/lang/String;", message).await?;
122+
jvm.put_field(&mut this, "cause", "Ljava/lang/Throwable;", cause).await?;
123+
124+
let _: ClassInstanceRef<Self> = jvm.invoke_virtual(&this, "fillInStackTrace", "()Ljava/lang/Throwable;", ()).await?;
125+
126+
Ok(())
127+
}
128+
129+
async fn get_cause(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>) -> Result<ClassInstanceRef<Self>> {
130+
tracing::debug!("java.lang.Throwable::getCause({:?})", &this);
131+
132+
jvm.get_field(&this, "cause", "Ljava/lang/Throwable;").await
133+
}
134+
135+
async fn init_cause(
136+
jvm: &Jvm,
137+
_: &mut RuntimeContext,
138+
mut this: ClassInstanceRef<Self>,
139+
cause: ClassInstanceRef<Self>,
140+
) -> Result<ClassInstanceRef<Self>> {
141+
tracing::debug!("java.lang.Throwable::initCause({:?}, {:?})", &this, &cause);
142+
143+
jvm.put_field(&mut this, "cause", "Ljava/lang/Throwable;", cause).await?;
144+
145+
Ok(this)
146+
}
147+
77148
async fn fill_in_stack_trace(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef<Self>) -> Result<ClassInstanceRef<Self>> {
78149
tracing::debug!("java.lang.Throwable::fillInStackTrace({:?})", &this);
79150

@@ -150,23 +221,41 @@ impl Throwable {
150221
}
151222

152223
async fn do_print_stack_trace(jvm: &Jvm, this: ClassInstanceRef<Self>, stream_or_writer: Box<dyn ClassInstance>) -> Result<()> {
153-
let stack_trace: ClassInstanceRef<Array<ClassInstanceRef<String>>> = jvm.get_field(&this, "stackTrace", "[Ljava/lang/String;").await?;
154-
155-
// TODO we can call println(Ljava/lang/Object;)V
156-
let string: ClassInstanceRef<String> = jvm.invoke_virtual(&this, "toString", "()Ljava/lang/String;", ()).await?;
157-
let _: () = jvm
158-
.invoke_virtual(&stream_or_writer, "println", "(Ljava/lang/String;)V", (string,))
159-
.await?;
160-
161-
if !stack_trace.is_null() {
162-
let length = jvm.array_length(&stack_trace).await?;
163-
let lines: Vec<ClassInstanceRef<String>> = jvm.load_array(&stack_trace, 0, length).await?;
164-
for line_ref in lines {
165-
let line = JavaLangString::to_rust_string(jvm, &line_ref).await?;
166-
let line = format!("\tat {line}");
167-
let line = JavaLangString::from_rust_string(jvm, &line).await?;
168-
let _: () = jvm.invoke_virtual(&stream_or_writer, "println", "(Ljava/lang/String;)V", (line,)).await?;
224+
let mut current: ClassInstanceRef<Self> = this;
225+
let mut header: Option<&str> = None;
226+
227+
// a malformed initCause could create a cycle, so cap the depth
228+
for _ in 0..32 {
229+
let string: ClassInstanceRef<String> = jvm.invoke_virtual(&current, "toString", "()Ljava/lang/String;", ()).await?;
230+
let prefix: ClassInstanceRef<String> = match header {
231+
Some(x) => {
232+
let string = JavaLangString::to_rust_string(jvm, &string).await?;
233+
JavaLangString::from_rust_string(jvm, &format!("{x}{string}")).await?.into()
234+
}
235+
None => string,
236+
};
237+
let _: () = jvm
238+
.invoke_virtual(&stream_or_writer, "println", "(Ljava/lang/String;)V", (prefix,))
239+
.await?;
240+
241+
let stack_trace: ClassInstanceRef<Array<ClassInstanceRef<String>>> = jvm.get_field(&current, "stackTrace", "[Ljava/lang/String;").await?;
242+
if !stack_trace.is_null() {
243+
let length = jvm.array_length(&stack_trace).await?;
244+
let lines: Vec<ClassInstanceRef<String>> = jvm.load_array(&stack_trace, 0, length).await?;
245+
for line_ref in lines {
246+
let line = JavaLangString::to_rust_string(jvm, &line_ref).await?;
247+
let line = format!("\tat {line}");
248+
let line = JavaLangString::from_rust_string(jvm, &line).await?;
249+
let _: () = jvm.invoke_virtual(&stream_or_writer, "println", "(Ljava/lang/String;)V", (line,)).await?;
250+
}
251+
}
252+
253+
let cause: ClassInstanceRef<Self> = jvm.invoke_virtual(&current, "getCause", "()Ljava/lang/Throwable;", ()).await?;
254+
if cause.is_null() {
255+
break;
169256
}
257+
current = cause;
258+
header = Some("Caused by: ");
170259
}
171260

172261
Ok(())

java_runtime/src/loader.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option<RuntimeClassProto> {
4343
crate::classes::java::lang::Comparable::as_proto(),
4444
crate::classes::java::lang::Error::as_proto(),
4545
crate::classes::java::lang::Exception::as_proto(),
46+
crate::classes::java::lang::ExceptionInInitializerError::as_proto(),
4647
crate::classes::java::lang::IllegalArgumentException::as_proto(),
4748
crate::classes::java::lang::InstantiationError::as_proto(),
4849
crate::classes::java::lang::IncompatibleClassChangeError::as_proto(),

0 commit comments

Comments
 (0)