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/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/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 new file mode 100644 index 00000000..48bb9e4d --- /dev/null +++ b/java_runtime/src/classes/java/lang/exception_in_initializer_error.rs @@ -0,0 +1,65 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{String, Throwable}, +}; + +// 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()), + 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(), + } + } + + 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(()) + } + + 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/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/java_runtime/tests/classes/java/lang/test_throwable.rs b/java_runtime/tests/classes/java/lang/test_throwable.rs index 59351b10..abe3e4a9 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\ @@ -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/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/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..e9b42e40 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,15 @@ 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?; + + // 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); + } + + Ok(class) } async fn load_class(&self, class_name: &str, class_loader_wrapper: &dyn ClassLoaderWrapper) -> Result { @@ -665,14 +681,73 @@ impl Jvm { self.inner.classes.write().insert(class.definition.name().to_owned(), class.clone()); - let clinit = class.definition.method("", "()V", true); + Ok(()) + } + + #[async_recursion::async_recursion] + async fn ensure_initialized(&self, class: &Class) -> Result<()> { + if class.definition.name().starts_with('[') { + return Ok(()); + } - if let Some(x) = clinit { + 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(super_name) = class.definition.super_class_name() { + // 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) => { + 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 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()); - 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); + + let JavaError::JavaException(exception) = &err; + if self.is_instance(&**exception, "java/lang/Error") { + return Err(err); + } + + let cause = clone_box(&**exception); + let wrapped = self + .new_class("java/lang/ExceptionInInitializerError", "(Ljava/lang/Throwable;)V", (cause,)) + .await?; + + return Err(JavaError::JavaException(wrapped)); + } } + class.set_init_state(InitState::Initialized); + Ok(()) } 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/ClinitFailure$Bad.class b/test_data/ClinitFailure$Bad.class new file mode 100644 index 00000000..ea9989df Binary files /dev/null and b/test_data/ClinitFailure$Bad.class differ diff --git a/test_data/ClinitFailure$BadError.class b/test_data/ClinitFailure$BadError.class new file mode 100644 index 00000000..a7e1f5eb Binary files /dev/null and b/test_data/ClinitFailure$BadError.class differ diff --git a/test_data/ClinitFailure.class b/test_data/ClinitFailure.class new file mode 100644 index 00000000..6fd61b21 Binary files /dev/null and b/test_data/ClinitFailure.class differ diff --git a/test_data/ClinitFailure.txt b/test_data/ClinitFailure.txt new file mode 100644 index 00000000..95ce7127 --- /dev/null +++ b/test_data/ClinitFailure.txt @@ -0,0 +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/Constants.class b/test_data/Constants.class new file mode 100644 index 00000000..dd6b91b1 Binary files /dev/null and b/test_data/Constants.class differ 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 00000000..7cdf771b Binary files /dev/null and b/test_data/ConstantsReader.class differ 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/DoubleInit.class b/test_data/DoubleInit.class new file mode 100644 index 00000000..3f657238 Binary files /dev/null and b/test_data/DoubleInit.class differ 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/LazyClinit$Base.class b/test_data/LazyClinit$Base.class new file mode 100644 index 00000000..4b175704 Binary files /dev/null and b/test_data/LazyClinit$Base.class differ diff --git a/test_data/LazyClinit$Derived.class b/test_data/LazyClinit$Derived.class new file mode 100644 index 00000000..4603518d Binary files /dev/null and b/test_data/LazyClinit$Derived.class differ diff --git a/test_data/LazyClinit$GetstaticTarget.class b/test_data/LazyClinit$GetstaticTarget.class new file mode 100644 index 00000000..0c6d9396 Binary files /dev/null and b/test_data/LazyClinit$GetstaticTarget.class differ diff --git a/test_data/LazyClinit$IFace.class b/test_data/LazyClinit$IFace.class new file mode 100644 index 00000000..5e417254 Binary files /dev/null and b/test_data/LazyClinit$IFace.class differ diff --git a/test_data/LazyClinit$Impl.class b/test_data/LazyClinit$Impl.class new file mode 100644 index 00000000..401232ff Binary files /dev/null and b/test_data/LazyClinit$Impl.class differ diff --git a/test_data/LazyClinit$NewTarget.class b/test_data/LazyClinit$NewTarget.class new file mode 100644 index 00000000..a10a6ee1 Binary files /dev/null and b/test_data/LazyClinit$NewTarget.class differ diff --git a/test_data/LazyClinit$PutstaticTarget.class b/test_data/LazyClinit$PutstaticTarget.class new file mode 100644 index 00000000..0a019476 Binary files /dev/null and b/test_data/LazyClinit$PutstaticTarget.class differ diff --git a/test_data/LazyClinit$SelfRef.class b/test_data/LazyClinit$SelfRef.class new file mode 100644 index 00000000..657f61e2 Binary files /dev/null and b/test_data/LazyClinit$SelfRef.class differ diff --git a/test_data/LazyClinit.class b/test_data/LazyClinit.class new file mode 100644 index 00000000..e1a28430 Binary files /dev/null and b/test_data/LazyClinit.class differ 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/StaticFlag.class b/test_data/StaticFlag.class new file mode 100644 index 00000000..ae212ec2 Binary files /dev/null and b/test_data/StaticFlag.class differ 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/StaticOrder$Helper.class b/test_data/StaticOrder$Helper.class new file mode 100644 index 00000000..080b4ffe Binary files /dev/null and b/test_data/StaticOrder$Helper.class differ diff --git a/test_data/StaticOrder.class b/test_data/StaticOrder.class new file mode 100644 index 00000000..dbacc686 Binary files /dev/null and b/test_data/StaticOrder.class differ 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 diff --git a/test_data/ThrowableCause.class b/test_data/ThrowableCause.class new file mode 100644 index 00000000..7dcc5912 Binary files /dev/null and b/test_data/ThrowableCause.class differ 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 new file mode 100644 index 00000000..47e26a6b --- /dev/null +++ b/test_data/src/ClinitFailure.java @@ -0,0 +1,44 @@ +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()); + System.out.println(t.getCause().getClass().getName()); + System.out.println(t.getCause()); + System.out.println(t); + } + 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/src/Constants.java b/test_data/src/Constants.java new file mode 100644 index 00000000..72c130d8 --- /dev/null +++ b/test_data/src/Constants.java @@ -0,0 +1,21 @@ +public 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"; + + 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/src/ConstantsReader.java b/test_data/src/ConstantsReader.java new file mode 100644 index 00000000..ef4e060c --- /dev/null +++ b/test_data/src/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/src/DoubleInit.java b/test_data/src/DoubleInit.java new file mode 100644 index 00000000..16e4c17f --- /dev/null +++ b/test_data/src/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/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/src/LazyClinit.java b/test_data/src/LazyClinit.java new file mode 100644 index 00000000..6866f31d --- /dev/null +++ b/test_data/src/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/src/StaticFlag.java b/test_data/src/StaticFlag.java new file mode 100644 index 00000000..b495d9dc --- /dev/null +++ b/test_data/src/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/src/StaticOrder.java b/test_data/src/StaticOrder.java new file mode 100644 index 00000000..dddb398f --- /dev/null +++ b/test_data/src/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/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); + } +} diff --git a/test_data/unit/StaticFlag.class b/test_data/unit/StaticFlag.class deleted file mode 100644 index 25ba3846..00000000 Binary files a/test_data/unit/StaticFlag.class and /dev/null differ 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_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(()) -}