diff --git a/jvm/src/garbage_collector.rs b/jvm/src/garbage_collector.rs index 90082e11..68f8a568 100644 --- a/jvm/src/garbage_collector.rs +++ b/jvm/src/garbage_collector.rs @@ -44,6 +44,21 @@ pub fn determine_garbage( find_reachable_objects(jvm, object, &mut reachable_objects)?; } + // java.lang.ref: a weak reference is cleared once its referent is no longer strongly reachable + if let Some(reference_class) = classes.get("java/lang/ref/Reference") { + let referent = reference_class.definition.field("referent", "Ljava/lang/Object;", false).unwrap(); + for object in all_class_instances + .iter() + .filter(|object| reachable_objects.contains(&object.identity()) && jvm.is_instance(&***object, "java/lang/ref/Reference")) + { + if let JavaValue::Object(Some(value)) = object.get_field(&*referent)? + && !reachable_objects.contains(&value.identity()) + { + object.clone().put_field(&*referent, JavaValue::Object(None))?; + } + } + } + Ok(all_class_instances .iter() .filter(|object| !reachable_objects.contains(&object.identity())) @@ -112,7 +127,11 @@ fn find_reachable_objects(jvm: &Jvm, object: &Box, reachable_ } fn find_all_fields(jvm: &Jvm, class_definition: &dyn ClassDefinition) -> Vec> { - let result = class_definition.fields(); + let mut result = class_definition.fields(); + if class_definition.name() == "java/lang/ref/Reference" { + // weak: the referent must not be kept reachable through the reference + result.retain(|field| field.name() != "referent"); + } let super_class_name = class_definition.super_class_name(); if let Some(x) = super_class_name { diff --git a/rustjava-runtime/src/classes/java/lang.rs b/rustjava-runtime/src/classes/java/lang.rs index 4482399d..252d7d6f 100644 --- a/rustjava-runtime/src/classes/java/lang.rs +++ b/rustjava-runtime/src/classes/java/lang.rs @@ -71,6 +71,8 @@ mod unsupported_operation_exception; mod verify_error; mod virtual_machine_error; +pub mod r#ref; + pub use self::{ abstract_method_error::AbstractMethodError, abstract_string_builder::AbstractStringBuilder, appendable::Appendable, arithmetic_exception::ArithmeticException, array_index_out_of_bounds_exception::ArrayIndexOutOfBoundsException, diff --git a/rustjava-runtime/src/classes/java/lang/array_index_out_of_bounds_exception.rs b/rustjava-runtime/src/classes/java/lang/array_index_out_of_bounds_exception.rs index ca878b43..22d707f2 100644 --- a/rustjava-runtime/src/classes/java/lang/array_index_out_of_bounds_exception.rs +++ b/rustjava-runtime/src/classes/java/lang/array_index_out_of_bounds_exception.rs @@ -1,6 +1,6 @@ -use alloc::vec; +use alloc::{format, vec}; -use jvm::{ClassInstanceRef, Jvm, Result}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; use jvm_class_proto::JavaMethodProto; use jvm_types::{ClassAccessFlags, MethodAccessFlags}; @@ -18,6 +18,7 @@ impl ArrayIndexOutOfBoundsException { methods: vec![ JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(I)V", Self::init_with_index, MethodAccessFlags::PUBLIC), ], fields: vec![], access_flags: ClassAccessFlags::PUBLIC, @@ -47,4 +48,21 @@ impl ArrayIndexOutOfBoundsException { Ok(()) } + + async fn init_with_index(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, index: i32) -> Result<()> { + tracing::debug!("java.lang.ArrayIndexOutOfBoundsException::({this:?}, {index:?})"); + + let message = JavaLangString::from_rust_string(jvm, &format!("Array index out of range: {index}")).await?; + let _: () = jvm + .invoke_special( + &this, + "java/lang/IndexOutOfBoundsException", + "", + "(Ljava/lang/String;)V", + (message,), + ) + .await?; + + Ok(()) + } } diff --git a/rustjava-runtime/src/classes/java/lang/ref.rs b/rustjava-runtime/src/classes/java/lang/ref.rs new file mode 100644 index 00000000..3131e5c3 --- /dev/null +++ b/rustjava-runtime/src/classes/java/lang/ref.rs @@ -0,0 +1,4 @@ +mod reference; +mod weak_reference; + +pub use {reference::Reference, weak_reference::WeakReference}; diff --git a/rustjava-runtime/src/classes/java/lang/ref/reference.rs b/rustjava-runtime/src/classes/java/lang/ref/reference.rs new file mode 100644 index 00000000..61a5b382 --- /dev/null +++ b/rustjava-runtime/src/classes/java/lang/ref/reference.rs @@ -0,0 +1,57 @@ +use alloc::vec; + +use jvm::{ClassInstanceRef, Jvm, Result}; +use jvm_class_proto::{JavaFieldProto, JavaMethodProto}; +use jvm_types::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; + +// abstract class java.lang.ref.Reference +pub struct Reference; + +impl Reference { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/ref/Reference", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/lang/Object;)V", Self::init, MethodAccessFlags::empty()), + JavaMethodProto::new("get", "()Ljava/lang/Object;", Self::get, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("clear", "()V", Self::clear, MethodAccessFlags::PUBLIC), + ], + // the garbage collector does not trace this field and clears it once the referent is otherwise unreachable + fields: vec![JavaFieldProto::new("referent", "Ljava/lang/Object;", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, referent: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.ref.Reference::({this:?}, {referent:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "java/lang/ref/Reference", "referent", "Ljava/lang/Object;", referent) + .await?; + + Ok(()) + } + + async fn get(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.lang.ref.Reference::get({this:?})"); + + jvm.get_field(&this, "java/lang/ref/Reference", "referent", "Ljava/lang/Object;").await + } + + async fn clear(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.ref.Reference::clear({this:?})"); + + jvm.put_field( + &mut this, + "java/lang/ref/Reference", + "referent", + "Ljava/lang/Object;", + ClassInstanceRef::::new(None), + ) + .await + } +} diff --git a/rustjava-runtime/src/classes/java/lang/ref/weak_reference.rs b/rustjava-runtime/src/classes/java/lang/ref/weak_reference.rs new file mode 100644 index 00000000..3a47f0cd --- /dev/null +++ b/rustjava-runtime/src/classes/java/lang/ref/weak_reference.rs @@ -0,0 +1,35 @@ +use alloc::vec; + +use jvm::{ClassInstanceRef, Jvm, Result}; +use jvm_class_proto::JavaMethodProto; +use jvm_types::{ClassAccessFlags, MethodAccessFlags}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; + +// class java.lang.ref.WeakReference +pub struct WeakReference; + +impl WeakReference { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/ref/WeakReference", + parent_class: Some("java/lang/ref/Reference"), + interfaces: vec![], + methods: vec![JavaMethodProto::new( + "", + "(Ljava/lang/Object;)V", + Self::init, + MethodAccessFlags::PUBLIC, + )], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, referent: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.ref.WeakReference::({this:?}, {referent:?})"); + + jvm.invoke_special(&this, "java/lang/ref/Reference", "", "(Ljava/lang/Object;)V", (referent,)) + .await + } +} diff --git a/rustjava-runtime/src/classes/java/lang/string_index_out_of_bounds_exception.rs b/rustjava-runtime/src/classes/java/lang/string_index_out_of_bounds_exception.rs index 5af5aa65..b3e1b85a 100644 --- a/rustjava-runtime/src/classes/java/lang/string_index_out_of_bounds_exception.rs +++ b/rustjava-runtime/src/classes/java/lang/string_index_out_of_bounds_exception.rs @@ -1,6 +1,6 @@ -use alloc::vec; +use alloc::{format, vec}; -use jvm::{ClassInstanceRef, Jvm, Result}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; use jvm_class_proto::JavaMethodProto; use jvm_types::{ClassAccessFlags, MethodAccessFlags}; @@ -18,6 +18,7 @@ impl StringIndexOutOfBoundsException { methods: vec![ JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(I)V", Self::init_with_index, MethodAccessFlags::PUBLIC), ], fields: vec![], access_flags: ClassAccessFlags::PUBLIC, @@ -49,4 +50,21 @@ impl StringIndexOutOfBoundsException { Ok(()) } + + async fn init_with_index(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, index: i32) -> Result<()> { + tracing::debug!("java.lang.StringIndexOutOfBoundsException::({this:?}, {index:?})"); + + let message = JavaLangString::from_rust_string(jvm, &format!("String index out of range: {index}")).await?; + let _: () = jvm + .invoke_special( + &this, + "java/lang/IndexOutOfBoundsException", + "", + "(Ljava/lang/String;)V", + (message,), + ) + .await?; + + Ok(()) + } } diff --git a/rustjava-runtime/src/classes/java/util/timer.rs b/rustjava-runtime/src/classes/java/util/timer.rs index dcbf3a68..e712972d 100644 --- a/rustjava-runtime/src/classes/java/util/timer.rs +++ b/rustjava-runtime/src/classes/java/util/timer.rs @@ -45,6 +45,12 @@ impl Timer { Self::schedule_at_fixed_rate, MethodAccessFlags::PUBLIC, ), + JavaMethodProto::new( + "scheduleAtFixedRate", + "(Ljava/util/TimerTask;Ljava/util/Date;J)V", + Self::schedule_date_at_fixed_rate, + MethodAccessFlags::PUBLIC, + ), JavaMethodProto::new("cancel", "()V", Self::cancel, MethodAccessFlags::PUBLIC), ], fields: vec![JavaFieldProto::new( @@ -206,6 +212,32 @@ impl Timer { Self::sched(jvm, this, task, time, period).await } + async fn schedule_date_at_fixed_rate( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + task: ClassInstanceRef, + date: ClassInstanceRef, + period: i64, + ) -> Result<()> { + tracing::debug!("java.util.Timer::scheduleAtFixedRate({this:?}, {task:?}, {date:?}, {period:?})"); + + if task.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "task").await); + } + if date.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "time").await); + } + if period <= 0 { + return Err(jvm.exception("java/lang/IllegalArgumentException", "non-positive period").await); + } + let time: i64 = jvm.invoke_virtual(&date, "java/util/Date", "getTime", "()J", ()).await?; + if time < 0 { + return Err(jvm.exception("java/lang/IllegalArgumentException", "illegal execution time").await); + } + Self::sched(jvm, this, task, time, period).await + } + async fn sched(jvm: &Jvm, this: ClassInstanceRef, mut task: ClassInstanceRef, time: i64, period: i64) -> Result<()> { let period = if !(-(i64::MAX >> 1)..=(i64::MAX >> 1)).contains(&period) { period >> 1 diff --git a/rustjava-runtime/src/loader.rs b/rustjava-runtime/src/loader.rs index d94c87f7..fde76135 100644 --- a/rustjava-runtime/src/loader.rs +++ b/rustjava-runtime/src/loader.rs @@ -118,6 +118,8 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::UnsatisfiedLinkError::as_proto(), crate::classes::java::lang::VerifyError::as_proto(), crate::classes::java::lang::VirtualMachineError::as_proto(), + crate::classes::java::lang::r#ref::Reference::as_proto(), + crate::classes::java::lang::r#ref::WeakReference::as_proto(), crate::classes::java::net::JarURLConnection::as_proto(), crate::classes::java::net::MalformedURLException::as_proto(), crate::classes::java::net::UnknownServiceException::as_proto(), diff --git a/rustjava-runtime/tests/classes/java/lang/test_cldc11_exceptions.rs b/rustjava-runtime/tests/classes/java/lang/test_cldc11_exceptions.rs index 8b971a76..73a8d7e8 100644 --- a/rustjava-runtime/tests/classes/java/lang/test_cldc11_exceptions.rs +++ b/rustjava-runtime/tests/classes/java/lang/test_cldc11_exceptions.rs @@ -149,3 +149,22 @@ async fn test_runtime_api_exception_and_error_message_constructors() -> Result<( Ok(()) } + +#[tokio::test] +async fn test_index_exception_index_constructors() -> Result<()> { + let jvm = test_jvm().await?; + + for (class_name, index, expected) in [ + ("java/lang/ArrayIndexOutOfBoundsException", 5, "Array index out of range: 5"), + ("java/lang/StringIndexOutOfBoundsException", -3, "String index out of range: -3"), + ] { + let instance = jvm.new_class(class_name, "(I)V", (index,)).await?; + let message: ClassInstanceRef = jvm + .invoke_virtual(&instance, class_name, "getMessage", "()Ljava/lang/String;", ()) + .await?; + + assert_eq!(JavaLangString::to_rust_string(&jvm, &message).await?, expected); + } + + Ok(()) +} diff --git a/rustjava-runtime/tests/classes/java/util/test_timer.rs b/rustjava-runtime/tests/classes/java/util/test_timer.rs index cade7856..f2f638e7 100644 --- a/rustjava-runtime/tests/classes/java/util/test_timer.rs +++ b/rustjava-runtime/tests/classes/java/util/test_timer.rs @@ -162,6 +162,7 @@ async fn timer_01_to_05_registers_jdk_shaped_api_and_state() -> Result<()> { ("schedule", "(Ljava/util/TimerTask;JJ)V"), ("schedule", "(Ljava/util/TimerTask;Ljava/util/Date;J)V"), ("scheduleAtFixedRate", "(Ljava/util/TimerTask;JJ)V"), + ("scheduleAtFixedRate", "(Ljava/util/TimerTask;Ljava/util/Date;J)V"), ("cancel", "()V"), ] { let method = timer @@ -390,6 +391,22 @@ async fn timer_normalizes_long_max_period_and_rejects_first_recurrence_overflow( (-i64::MAX) >> 1 ); assert_eq!(jvm.get_field::(&fixed_rate, "TestTimerTask", "period", "J").await?, i64::MAX >> 1); + let dated_fixed_rate: ClassInstanceRef = jvm.new_class("TestTimerTask", "()V", ()).await?.into(); + let date = jvm.new_class("java/util/Date", "(J)V", (2_000i64,)).await?; + let _: () = jvm + .invoke_virtual( + &timer, + &timer.class_definition().name(), + "scheduleAtFixedRate", + "(Ljava/util/TimerTask;Ljava/util/Date;J)V", + (dated_fixed_rate.clone(), date, 25i64), + ) + .await?; + assert_eq!( + jvm.get_field::(&dated_fixed_rate, "TestTimerTask", "nextExecutionTime", "J").await?, + 2_000 + ); + assert_eq!(jvm.get_field::(&dated_fixed_rate, "TestTimerTask", "period", "J").await?, 25); let _: () = jvm.invoke_virtual(&timer, &timer.class_definition().name(), "cancel", "()V", ()).await?; await_spawn(next_spawn(&runtime).await, "large-period timer worker").await?; @@ -412,21 +429,23 @@ async fn timer_normalizes_long_max_period_and_rejects_first_recurrence_overflow( assert_eq!(jvm.get_field::(&task, "TestTimerTask", "state", "I").await?, 0); assert_eq!(jvm.get_field::(&task, "TestTimerTask", "period", "J").await?, 0); } - let date = jvm.new_class("java/util/Date", "(J)V", (i64::MAX - 50,)).await?; - let dated: ClassInstanceRef = jvm.new_class("TestTimerTask", "()V", ()).await?.into(); - assert_exception( - jvm.invoke_virtual::<_, ()>( - &timer, - &timer.class_definition().name(), - "schedule", - "(Ljava/util/TimerTask;Ljava/util/Date;J)V", - (dated.clone(), date, 60i64), + for method in ["schedule", "scheduleAtFixedRate"] { + let date = jvm.new_class("java/util/Date", "(J)V", (i64::MAX - 50,)).await?; + let dated: ClassInstanceRef = jvm.new_class("TestTimerTask", "()V", ()).await?.into(); + assert_exception( + jvm.invoke_virtual::<_, ()>( + &timer, + &timer.class_definition().name(), + method, + "(Ljava/util/TimerTask;Ljava/util/Date;J)V", + (dated.clone(), date, 60i64), + ) + .await, + "java/lang/IllegalArgumentException", ) - .await, - "java/lang/IllegalArgumentException", - ) - .await; - assert_eq!(jvm.get_field::(&dated, "TestTimerTask", "state", "I").await?, 0); + .await; + assert_eq!(jvm.get_field::(&dated, "TestTimerTask", "state", "I").await?, 0); + } let _: () = jvm.invoke_virtual(&timer, &timer.class_definition().name(), "cancel", "()V", ()).await?; await_spawn(next_spawn(&runtime).await, "near-boundary validation timer worker").await?; Ok(()) diff --git a/test-data/WeakReferenceSemantics$Node.class b/test-data/WeakReferenceSemantics$Node.class new file mode 100644 index 00000000..4fb2f55a Binary files /dev/null and b/test-data/WeakReferenceSemantics$Node.class differ diff --git a/test-data/WeakReferenceSemantics.class b/test-data/WeakReferenceSemantics.class new file mode 100644 index 00000000..34f34dd9 Binary files /dev/null and b/test-data/WeakReferenceSemantics.class differ diff --git a/test-data/WeakReferenceSemantics.txt b/test-data/WeakReferenceSemantics.txt new file mode 100644 index 00000000..ce331037 --- /dev/null +++ b/test-data/WeakReferenceSemantics.txt @@ -0,0 +1,8 @@ +held: true +held by field: true +collected: true +chain head: true +chain tail: true +empty: true +cleared: true +held after clear: true diff --git a/test-data/src/WeakReferenceSemantics.java b/test-data/src/WeakReferenceSemantics.java new file mode 100644 index 00000000..66b507ca --- /dev/null +++ b/test-data/src/WeakReferenceSemantics.java @@ -0,0 +1,52 @@ +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; + +public class WeakReferenceSemantics { + private static class Node { + Object next; + } + + // referents are created in a separate frame so that nothing but the references can keep them alive + private static WeakReference unreachable() { + return new WeakReference(new Object()); + } + + private static WeakReference[] unreachableChain() { + Node head = new Node(); + head.next = new Object(); + @SuppressWarnings("unchecked") + WeakReference[] result = new WeakReference[] { new WeakReference(head), new WeakReference(head.next) }; + return result; + } + + private static void report(String label, boolean value) { + System.out.print(label); + System.out.print(": "); + System.out.println(value); + } + + public static void main(String[] args) { + Object strong = new Object(); + Reference held = new WeakReference(strong); + Node holder = new Node(); + holder.next = new Object(); + Reference heldByField = new WeakReference(holder.next); + Reference collected = unreachable(); + Reference[] chain = unreachableChain(); + Reference empty = new WeakReference(null); + + System.gc(); + + report("held", held.get() == strong); + report("held by field", heldByField.get() == holder.next); + report("collected", collected.get() == null); + report("chain head", chain[0].get() == null); + report("chain tail", chain[1].get() == null); + report("empty", empty.get() == null); + + held.clear(); + report("cleared", held.get() == null); + System.gc(); + report("held after clear", heldByField.get() == holder.next); + } +}