diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index 724639f29..387985644 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -166,13 +166,172 @@ mod impls { impl_ffi_variant!(ref PackedStringArray, packed_string_array_to_variant, packed_string_array_from_variant); impl_ffi_variant!(ref PackedVector2Array, packed_vector2_array_to_variant, packed_vector2_array_from_variant); impl_ffi_variant!(ref PackedVector3Array, packed_vector3_array_to_variant, packed_vector3_array_from_variant); - #[cfg(since_api = "4.3")] - impl_ffi_variant!(ref PackedVector4Array, packed_vector4_array_to_variant, packed_vector4_array_from_variant); impl_ffi_variant!(ref PackedColorArray, packed_color_array_to_variant, packed_color_array_from_variant); impl_ffi_variant!(ref Signal, signal_to_variant, signal_from_variant); impl_ffi_variant!(ref Callable, callable_to_variant, callable_from_variant); + + #[cfg(since_api = "4.2")] + mod api_4_2 { + use crate::task::impl_dynamic_send; + + impl_dynamic_send!( + Send; + bool, u8, u16, u32, u64, i8, i16, i32, i64, f32, f64 + ); + + impl_dynamic_send!( + Send; + builtin::{ + StringName, Transform2D, Transform3D, Vector2, Vector2i, Vector2Axis, + Vector3, Vector3i, Vector3Axis, Vector4, Vector4i, Rect2, Rect2i, Plane, Quaternion, Aabb, Basis, Projection, Color, Rid + } + ); + + impl_dynamic_send!( + !Send; + Variant, GString, Dictionary, VariantArray, Callable, NodePath, PackedByteArray, PackedInt32Array, PackedInt64Array, PackedFloat32Array, + PackedFloat64Array, PackedStringArray, PackedVector2Array, PackedVector3Array, PackedColorArray, Signal + ); + + // This should be kept in sync with crate::registry::signal::variadic. + impl_dynamic_send!(tuple; ); + impl_dynamic_send!(tuple; arg1: A1); + impl_dynamic_send!(tuple; arg1: A1, arg2: A2); + impl_dynamic_send!(tuple; arg1: A1, arg2: A2, arg3: A3); + impl_dynamic_send!(tuple; arg1: A1, arg2: A2, arg3: A3, arg4: A4); + impl_dynamic_send!(tuple; arg1: A1, arg2: A2, arg3: A3, arg4: A4, arg5: A5); + impl_dynamic_send!(tuple; arg1: A1, arg2: A2, arg3: A3, arg4: A4, arg5: A5, arg6: A6); + impl_dynamic_send!(tuple; arg1: A1, arg2: A2, arg3: A3, arg4: A4, arg5: A5, arg6: A6, arg7: A7); + impl_dynamic_send!(tuple; arg1: A1, arg2: A2, arg3: A3, arg4: A4, arg5: A5, arg6: A6, arg7: A7, arg8: A8); + impl_dynamic_send!(tuple; arg1: A1, arg2: A2, arg3: A3, arg4: A4, arg5: A5, arg6: A6, arg7: A7, arg8: A8, arg9: A9); + } + + #[cfg(since_api = "4.3")] + mod api_4_3 { + use crate::task::impl_dynamic_send; + + use super::*; + + impl_ffi_variant!(ref PackedVector4Array, packed_vector4_array_to_variant, packed_vector4_array_from_variant); + + impl_dynamic_send!(!Send; PackedVector4Array); + } } +// Compile time check that we cover all the Variant types with trait implementations for: +// - IntoDynamicSend +// - DynamicSend +// - GodotType +// - ArrayElement +const _: () = { + use crate::classes::Object; + use crate::obj::{Gd, IndexEnum}; + + #[cfg(before_api = "4.2")] + const fn variant_type() -> VariantType { + ::VARIANT_TYPE + } + + #[cfg(since_api = "4.2")] + const fn variant_type( + ) -> VariantType { + ::VARIANT_TYPE + } + + const NIL: VariantType = variant_type::(); + const BOOL: VariantType = variant_type::(); + const I64: VariantType = variant_type::(); + const F64: VariantType = variant_type::(); + const GSTRING: VariantType = variant_type::(); + + const VECTOR2: VariantType = variant_type::(); + const VECTOR2I: VariantType = variant_type::(); + const RECT2: VariantType = variant_type::(); + const RECT2I: VariantType = variant_type::(); + const VECTOR3: VariantType = variant_type::(); + const VECTOR3I: VariantType = variant_type::(); + const TRANSFORM2D: VariantType = variant_type::(); + const TRANSFORM3D: VariantType = variant_type::(); + const VECTOR4: VariantType = variant_type::(); + const VECTOR4I: VariantType = variant_type::(); + const PLANE: VariantType = variant_type::(); + const QUATERNION: VariantType = variant_type::(); + const AABB: VariantType = variant_type::(); + const BASIS: VariantType = variant_type::(); + const PROJECTION: VariantType = variant_type::(); + const COLOR: VariantType = variant_type::(); + const STRING_NAME: VariantType = variant_type::(); + const NODE_PATH: VariantType = variant_type::(); + const RID: VariantType = variant_type::(); + const OBJECT: VariantType = variant_type::>(); + const CALLABLE: VariantType = variant_type::(); + const SIGNAL: VariantType = variant_type::(); + const DICTIONARY: VariantType = variant_type::(); + const ARRAY: VariantType = variant_type::(); + const PACKED_BYTE_ARRAY: VariantType = variant_type::(); + const PACKED_INT32_ARRAY: VariantType = variant_type::(); + const PACKED_INT64_ARRAY: VariantType = variant_type::(); + const PACKED_FLOAT32_ARRAY: VariantType = variant_type::(); + const PACKED_FLOAT64_ARRAY: VariantType = variant_type::(); + const PACKED_STRING_ARRAY: VariantType = variant_type::(); + const PACKED_VECTOR2_ARRAY: VariantType = variant_type::(); + const PACKED_VECTOR3_ARRAY: VariantType = variant_type::(); + const PACKED_COLOR_ARRAY: VariantType = variant_type::(); + + #[cfg(since_api = "4.3")] + const PACKED_VECTOR4_ARRAY: VariantType = variant_type::(); + + const MAX: i32 = VariantType::ENUMERATOR_COUNT as i32; + + // The matched value is not relevant, we just want to ensure that the full list from 0 to MAX is covered. + #[deny(unreachable_patterns)] + match VariantType::STRING { + VariantType { ord: i32::MIN..0 } => panic!("ord is out of defined range!"), + NIL => (), + BOOL => (), + I64 => (), + F64 => (), + GSTRING => (), + VECTOR2 => (), + VECTOR2I => (), + RECT2 => (), + RECT2I => (), + VECTOR3 => (), + VECTOR3I => (), + TRANSFORM2D => (), + VECTOR4 => (), + VECTOR4I => (), + PLANE => (), + QUATERNION => (), + AABB => (), + BASIS => (), + TRANSFORM3D => (), + PROJECTION => (), + COLOR => (), + STRING_NAME => (), + NODE_PATH => (), + RID => (), + OBJECT => (), + CALLABLE => (), + SIGNAL => (), + DICTIONARY => (), + ARRAY => (), + PACKED_BYTE_ARRAY => (), + PACKED_INT32_ARRAY => (), + PACKED_INT64_ARRAY => (), + PACKED_FLOAT32_ARRAY => (), + PACKED_FLOAT64_ARRAY => (), + PACKED_STRING_ARRAY => (), + PACKED_VECTOR2_ARRAY => (), + PACKED_VECTOR3_ARRAY => (), + PACKED_COLOR_ARRAY => (), + + #[cfg(since_api = "4.3")] + PACKED_VECTOR4_ARRAY => (), + VariantType { ord: MAX.. } => panic!("ord is out of defined range!"), + } +}; + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Explicit impls diff --git a/godot-core/src/meta/mod.rs b/godot-core/src/meta/mod.rs index 8782dab2e..7f5eaacff 100644 --- a/godot-core/src/meta/mod.rs +++ b/godot-core/src/meta/mod.rs @@ -49,10 +49,11 @@ mod class_name; mod godot_convert; mod method_info; mod property_info; -mod sealed; mod signature; mod traits; +pub(crate) mod sealed; + pub mod error; pub use args::*; diff --git a/godot-core/src/meta/sealed.rs b/godot-core/src/meta/sealed.rs index 37ab1b08b..a8a82953e 100644 --- a/godot-core/src/meta/sealed.rs +++ b/godot-core/src/meta/sealed.rs @@ -23,6 +23,9 @@ impl Sealed for Vector4 {} impl Sealed for Vector2i {} impl Sealed for Vector3i {} impl Sealed for Vector4i {} +impl Sealed for Vector2Axis {} +impl Sealed for Vector3Axis {} +impl Sealed for Vector4Axis {} impl Sealed for Quaternion {} impl Sealed for Color {} impl Sealed for GString {} @@ -72,3 +75,12 @@ where T::Ffi: GodotNullableFfi, { } +impl Sealed for (T1,) {} +impl Sealed for (T1, T2) {} +impl Sealed for (T1, T2, T3) {} +impl Sealed for (T1, T2, T3, T4) {} +impl Sealed for (T1, T2, T3, T4, T5) {} +impl Sealed for (T1, T2, T3, T4, T5, T6) {} +impl Sealed for (T1, T2, T3, T4, T5, T6, T7) {} +impl Sealed for (T1, T2, T3, T4, T5, T6, T7, T8) {} +impl Sealed for (T1, T2, T3, T4, T5, T6, T7, T8, T9) {} diff --git a/godot-core/src/task/futures.rs b/godot-core/src/task/futures.rs index 7a13417fc..6a49da027 100644 --- a/godot-core/src/task/futures.rs +++ b/godot-core/src/task/futures.rs @@ -11,26 +11,34 @@ use std::future::{Future, IntoFuture}; use std::pin::Pin; use std::sync::{Arc, Mutex}; use std::task::{Context, Poll, Waker}; +use std::thread::ThreadId; use crate::builtin::{Callable, RustCallable, Signal, Variant}; use crate::classes::object::ConnectFlags; +use crate::meta::sealed::Sealed; use crate::meta::ParamTuple; -use crate::obj::{EngineBitfield, WithBaseField}; +use crate::obj::{EngineBitfield, Gd, GodotClass, WithBaseField}; use crate::registry::signal::TypedSignal; +pub(crate) use crate::impl_dynamic_send; + /// The panicking counter part to the [`FallibleSignalFuture`]. /// /// This future works in the same way as `FallibleSignalFuture`, but panics when the signal object is freed, instead of resolving to a /// [`Result::Err`]. -pub struct SignalFuture(FallibleSignalFuture); +/// +/// # Panics +/// - If the signal object is freed before the signal has been emitted. +/// - If one of the signal arguments is `!Send`, but the signal was emitted on a different thread. +pub struct SignalFuture(FallibleSignalFuture); -impl SignalFuture { +impl SignalFuture { fn new(signal: Signal) -> Self { Self(FallibleSignalFuture::new(signal)) } } -impl Future for SignalFuture { +impl Future for SignalFuture { type Output = R; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -62,12 +70,11 @@ impl Default for SignalFutureData { } // Only public for itest. -#[cfg_attr(feature = "trace", derive(Default))] -pub struct SignalFutureResolver { - data: Arc>>, +pub struct SignalFutureResolver { + data: Arc>>, } -impl Clone for SignalFutureResolver { +impl Clone for SignalFutureResolver { fn clone(&self) -> Self { Self { data: self.data.clone(), @@ -75,29 +82,37 @@ impl Clone for SignalFutureResolver { } } -impl SignalFutureResolver { - fn new(data: Arc>>) -> Self { +/// For itest to construct and test a resolver. +#[cfg(feature = "trace")] +pub fn create_test_signal_future_resolver() -> SignalFutureResolver { + SignalFutureResolver { + data: Arc::new(Mutex::new(SignalFutureData::default())), + } +} + +impl SignalFutureResolver { + fn new(data: Arc>>) -> Self { Self { data } } } -impl std::hash::Hash for SignalFutureResolver { +impl std::hash::Hash for SignalFutureResolver { fn hash(&self, state: &mut H) { state.write_usize(Arc::as_ptr(&self.data) as usize); } } -impl PartialEq for SignalFutureResolver { +impl PartialEq for SignalFutureResolver { fn eq(&self, other: &Self) -> bool { Arc::ptr_eq(&self.data, &other.data) } } -impl RustCallable for SignalFutureResolver { +impl RustCallable for SignalFutureResolver { fn invoke(&mut self, args: &[&Variant]) -> Result { let waker = { let mut data = self.data.lock().unwrap(); - data.state = SignalFutureState::Ready(R::from_variant_array(args)); + data.state = SignalFutureState::Ready(R::from_variant_array(args).into_dynamic_send()); // We no longer need the waker after we resolved. If the future is polled again, we'll also get a new waker. data.waker.take() @@ -111,7 +126,7 @@ impl RustCallable for SignalFutureResolver { } } -impl Display for SignalFutureResolver { +impl Display for SignalFutureResolver { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "SignalFutureResolver::<{}>", std::any::type_name::()) } @@ -119,7 +134,7 @@ impl Display for SignalFutureResolver { // This resolver will change the futures state when it's being dropped (i.e. the engine removes all connected signal callables). By marking // the future as dead we can resolve it to an error value the next time it gets polled. -impl Drop for SignalFutureResolver { +impl Drop for SignalFutureResolver { fn drop(&mut self) { let mut data = self.data.lock().unwrap(); @@ -163,13 +178,16 @@ impl SignalFutureState { /// A future that tries to resolve as soon as the provided Godot signal was emitted. /// /// The future might resolve to an error if the signal object is freed before the signal is emitted. -pub struct FallibleSignalFuture { - data: Arc>>, +/// +/// # Panics +/// - If one of the signal arguments is `!Send`, but the signal was emitted on a different thread. +pub struct FallibleSignalFuture { + data: Arc>>, callable: SignalFutureResolver, signal: Signal, } -impl FallibleSignalFuture { +impl FallibleSignalFuture { fn new(signal: Signal) -> Self { debug_assert!( !signal.is_null(), @@ -199,11 +217,20 @@ impl FallibleSignalFuture { let value = data.state.take(); + // Drop the data mutex lock to prevent the mutext from getting poisoned by the potential later panic. + drop(data); + match value { SignalFutureState::Pending => Poll::Pending, SignalFutureState::Dropped => unreachable!(), SignalFutureState::Dead => Poll::Ready(Err(FallibleSignalFutureError)), - SignalFutureState::Ready(value) => Poll::Ready(Ok(value)), + SignalFutureState::Ready(value) => { + let Some(value) = DynamicSend::extract_if_safe(value) else { + panic!("the awaited signal was not emitted on the main-thread, but contained a non Send argument"); + }; + + Poll::Ready(Ok(value)) + } } } } @@ -225,7 +252,7 @@ impl Display for FallibleSignalFutureError { impl std::error::Error for FallibleSignalFutureError {} -impl Future for FallibleSignalFuture { +impl Future for FallibleSignalFuture { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -233,7 +260,7 @@ impl Future for FallibleSignalFuture { } } -impl Drop for FallibleSignalFuture { +impl Drop for FallibleSignalFuture { fn drop(&mut self) { // The callable might alredy be destroyed, this occurs during engine shutdown. if self.signal.object().is_none() { @@ -264,7 +291,7 @@ impl Signal { /// /// Since the `Signal` type does not contain information on the signal argument types, the future output type has to be inferred from /// the call to this function. - pub fn to_fallible_future(&self) -> FallibleSignalFuture { + pub fn to_fallible_future(&self) -> FallibleSignalFuture { FallibleSignalFuture::new(self.clone()) } @@ -275,12 +302,12 @@ impl Signal { /// /// Since the `Signal` type does not contain information on the signal argument types, the future output type has to be inferred from /// the call to this function. - pub fn to_future(&self) -> SignalFuture { + pub fn to_future(&self) -> SignalFuture { SignalFuture::new(self.clone()) } } -impl TypedSignal<'_, C, R> { +impl TypedSignal<'_, C, R> { /// Creates a fallible future for this signal. /// /// The future will resolve the next time the signal is emitted. @@ -298,7 +325,7 @@ impl TypedSignal<'_, C, R> { } } -impl IntoFuture for &TypedSignal<'_, C, R> { +impl IntoFuture for &TypedSignal<'_, C, R> { type Output = R; type IntoFuture = SignalFuture; @@ -308,18 +335,168 @@ impl IntoFuture for &TypedSignal< } } +/// Convert a value into a type that is [`Send`] at compile-time while the value might not be. +/// +/// This allows to turn any implementor into a type that is `Send`, but requires to also implement [`DynamicSend`] as well. +/// The later trait will verify if a value can actually be sent between threads at runtime. +pub trait IntoDynamicSend: Sealed { + type Target: DynamicSend; + + fn into_dynamic_send(self) -> Self::Target; +} + +/// Runtime-checked `Send` capability. +/// +/// Implemented for types that need a static `Send` bound, but where it is determined at runtime whether sending a value was +/// actually safe. Only allows to extract the value if sending across threads is safe, thus fulfilling the `Send` supertrait. +/// +/// # Safety +/// The implementor has to guarantee that `extract_if_safe` returns `None`, if the value has been sent between threads while being `!Send`. +/// +/// To uphold the `Send` supertrait guarantees, no public API apart from `extract_if_safe` must exist that would give access to the inner value from another thread. +pub unsafe trait DynamicSend: Send + Sealed { + type Inner; + + fn extract_if_safe(self) -> Option; +} + +/// Value that can be sent across threads, but only accessed on its original thread. +pub struct ThreadConfined { + value: T, + thread_id: ThreadId, +} + +// SAFETY: This type can always be sent across threads, but the inner value can only be accessed on its original thread. +unsafe impl Send for ThreadConfined {} + +impl ThreadConfined { + pub(crate) fn new(value: T) -> Self { + Self { + value, + thread_id: std::thread::current().id(), + } + } + + pub(crate) fn extract(self) -> Option { + (self.thread_id == std::thread::current().id()).then_some(self.value) + } +} + +unsafe impl DynamicSend for ThreadConfined> { + type Inner = Gd; + + fn extract_if_safe(self) -> Option { + self.extract() + } +} + +impl Sealed for ThreadConfined> {} + +impl IntoDynamicSend for Gd { + type Target = ThreadConfined; + + fn into_dynamic_send(self) -> Self::Target { + ThreadConfined::new(self) + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Generated impls + +#[macro_export(local_inner_macros)] +macro_rules! impl_dynamic_send { + (Send; $($ty:ty),+) => { + $( + unsafe impl $crate::task::DynamicSend for $ty { + type Inner = Self; + + fn extract_if_safe(self) -> Option { + Some(self) + } + } + + impl $crate::task::IntoDynamicSend for $ty { + type Target = Self; + fn into_dynamic_send(self) -> Self::Target { + self + } + } + )+ + }; + + (Send; builtin::{$($ty:ident),+}) => { + impl_dynamic_send!(Send; $($crate::builtin::$ty),+); + }; + + (tuple; $($arg:ident: $ty:ident),*) => { + unsafe impl<$($ty: $crate::task::DynamicSend ),*> $crate::task::DynamicSend for ($($ty,)*) { + type Inner = ($($ty::Inner,)*); + + fn extract_if_safe(self) -> Option { + #[allow(non_snake_case)] + let ($($arg,)*) = self; + + #[allow(clippy::unused_unit)] + match ($($arg.extract_if_safe(),)*) { + ($(Some($arg),)*) => Some(($($arg,)*)), + + #[allow(unreachable_patterns)] + _ => None, + } + } + } + + impl<$($ty: $crate::task::IntoDynamicSend),*> $crate::task::IntoDynamicSend for ($($ty,)*) { + type Target = ($($ty::Target,)*); + + fn into_dynamic_send(self) -> Self::Target { + #[allow(non_snake_case)] + let ($($arg,)*) = self; + + #[allow(clippy::unused_unit)] + ($($arg.into_dynamic_send(),)*) + } + } + }; + + (!Send; $($ty:ident),+) => { + $( + impl $crate::meta::sealed::Sealed for $crate::task::ThreadConfined<$crate::builtin::$ty> {} + + unsafe impl $crate::task::DynamicSend for $crate::task::ThreadConfined<$crate::builtin::$ty> { + type Inner = $crate::builtin::$ty; + + fn extract_if_safe(self) -> Option { + self.extract() + } + } + + impl $crate::task::IntoDynamicSend for $crate::builtin::$ty { + type Target = $crate::task::ThreadConfined<$crate::builtin::$ty>; + + fn into_dynamic_send(self) -> Self::Target { + $crate::task::ThreadConfined::new(self) + } + } + )+ + }; +} + #[cfg(test)] mod tests { - use crate::sys; use std::sync::Arc; + use crate::classes::Object; + use crate::obj::Gd; + use crate::sys; + use super::SignalFutureResolver; /// Test that the hash of a cloned future resolver is equal to its original version. With this equality in place, we can create new /// Callables that are equal to their original version but have separate reference counting. #[test] fn future_resolver_cloned_hash() { - let resolver_a = SignalFutureResolver::::new(Arc::default()); + let resolver_a = SignalFutureResolver::<(Gd, i64)>::new(Arc::default()); let resolver_b = resolver_a.clone(); let hash_a = sys::hash_value(&resolver_a); diff --git a/godot-core/src/task/mod.rs b/godot-core/src/task/mod.rs index 0834fbdf4..4ee359956 100644 --- a/godot-core/src/task/mod.rs +++ b/godot-core/src/task/mod.rs @@ -15,12 +15,15 @@ mod async_runtime; mod futures; pub(crate) use async_runtime::cleanup; +pub(crate) use futures::{impl_dynamic_send, ThreadConfined}; pub use async_runtime::{spawn, TaskHandle}; -pub use futures::{FallibleSignalFuture, FallibleSignalFutureError, SignalFuture}; +pub use futures::{ + DynamicSend, FallibleSignalFuture, FallibleSignalFutureError, IntoDynamicSend, SignalFuture, +}; // Only exported for itest. #[cfg(feature = "trace")] pub use async_runtime::has_godot_task_panicked; #[cfg(feature = "trace")] -pub use futures::SignalFutureResolver; +pub use futures::{create_test_signal_future_resolver, SignalFutureResolver}; diff --git a/itest/rust/Cargo.toml b/itest/rust/Cargo.toml index 92230c43a..015437632 100644 --- a/itest/rust/Cargo.toml +++ b/itest/rust/Cargo.toml @@ -24,6 +24,7 @@ serde = ["dep:serde", "dep:serde_json", "godot/serde"] godot = { path = "../../godot", default-features = false, features = ["__trace"] } serde = { version = "1", features = ["derive"], optional = true } serde_json = { version = "1.0", optional = true } +pin-project-lite = { version = "0.2" } [build-dependencies] godot-bindings = { path = "../../godot-bindings" } # emit_godot_version_cfg diff --git a/itest/rust/src/engine_tests/async_test.rs b/itest/rust/src/engine_tests/async_test.rs index 3e62d5ede..3985abf2c 100644 --- a/itest/rust/src/engine_tests/async_test.rs +++ b/itest/rust/src/engine_tests/async_test.rs @@ -12,9 +12,9 @@ use godot::classes::{Object, RefCounted}; use godot::meta::ToGodot; use godot::obj::{Base, Gd, NewAlloc, NewGd}; use godot::prelude::{godot_api, GodotClass}; -use godot::task::{self, SignalFuture, SignalFutureResolver, TaskHandle}; +use godot::task::{self, create_test_signal_future_resolver, SignalFuture, TaskHandle}; -use crate::framework::{itest, TestContext}; +use crate::framework::{expect_async_panic, itest, TestContext}; #[derive(GodotClass)] #[class(init)] @@ -37,14 +37,21 @@ fn start_async_task() -> TaskHandle { object.add_user_signal("custom_signal"); let task_handle = task::spawn(async move { - let signal_future: SignalFuture<(u8,)> = signal.to_future(); - let (result,) = signal_future.await; + let signal_future: SignalFuture<(u8, Gd)> = signal.to_future(); + let (result, object) = signal_future.await; assert_eq!(result, 10); + assert!(object.is_instance_valid()); + drop(object_ref); }); - object.emit_signal("custom_signal", &[10.to_variant()]); + let ref_counted_arg = RefCounted::new_gd(); + + object.emit_signal( + "custom_signal", + &[10.to_variant(), ref_counted_arg.to_variant()], + ); task_handle } @@ -80,11 +87,84 @@ fn async_task_fallible_signal_future() -> TaskHandle { handle } +#[itest(async)] +fn async_task_signal_future_panic() -> TaskHandle { + let mut obj = Object::new_alloc(); + + let signal = Signal::from_object_signal(&obj, "script_changed"); + + let handle = task::spawn(expect_async_panic( + "future should panic when the signal object is dropped", + async move { + signal.to_future::<()>().await; + }, + )); + + obj.call_deferred("free", &[]); + + handle +} + +#[cfg(feature = "experimental-threads")] +#[itest(async)] +fn signal_future_non_send_arg_panic() -> TaskHandle { + use crate::framework::ThreadCrosser; + + let mut object = RefCounted::new_gd(); + let signal = Signal::from_object_signal(&object, "custom_signal"); + + object.add_user_signal("custom_signal"); + + let handle = task::spawn(expect_async_panic( + "future should panic when the Gd is sent between threads", + async move { + signal.to_future::<(Gd,)>().await; + }, + )); + + let object = ThreadCrosser::new(object); + + std::thread::spawn(move || { + let mut object = unsafe { object.extract() }; + + object.emit_signal("custom_signal", &[RefCounted::new_gd().to_variant()]) + }); + + handle +} + +#[cfg(feature = "experimental-threads")] +#[itest(async)] +fn signal_future_send_arg_no_panic() -> TaskHandle { + use crate::framework::ThreadCrosser; + + let mut object = RefCounted::new_gd(); + let signal = Signal::from_object_signal(&object, "custom_signal"); + + object.add_user_signal("custom_signal"); + + let handle = task::spawn(async move { + let (value,) = signal.to_future::<(u8,)>().await; + + assert_eq!(value, 1); + }); + + let object = ThreadCrosser::new(object); + + std::thread::spawn(move || { + let mut object = unsafe { object.extract() }; + + object.emit_signal("custom_signal", &[1u8.to_variant()]) + }); + + handle +} + // Test that two callables created from the same future resolver (but cloned) are equal, while they are not equal to an unrelated // callable. #[itest] fn resolver_callabable_equality() { - let resolver = SignalFutureResolver::<(u8,)>::default(); + let resolver = create_test_signal_future_resolver::<(u8,)>(); let callable = Callable::from_custom(resolver.clone()); let cloned_callable = Callable::from_custom(resolver.clone()); diff --git a/itest/rust/src/framework/mod.rs b/itest/rust/src/framework/mod.rs index 37a9682c2..bb8aad3a7 100644 --- a/itest/rust/src/framework/mod.rs +++ b/itest/rust/src/framework/mod.rs @@ -203,6 +203,49 @@ pub fn expect_panic(context: &str, code: impl FnOnce()) { ); } +pin_project_lite::pin_project! { + pub struct ExpectPanicFuture { + context: &'static str, + #[pin] + future: T, + } +} + +impl std::future::Future for ExpectPanicFuture { + type Output = (); + + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + let projection = self.project(); + let future = projection.future; + + // Run code that should panic, restore hook + gdext panic printing. + let panic = suppress_panic_log(move || { + panic::catch_unwind(panic::AssertUnwindSafe(move || future.poll(cx))) + }); + + match panic { + Ok(std::task::Poll::Pending) => std::task::Poll::Pending, + Err(_) => std::task::Poll::Ready(()), + Ok(std::task::Poll::Ready(_)) => { + panic!( + "code should have panicked but did not: {}", + projection.context + ); + } + } + } +} + +pub fn expect_async_panic( + context: &'static str, + future: T, +) -> ExpectPanicFuture { + ExpectPanicFuture { context, future } +} + pub fn expect_debug_panic_or_release_ok(_context: &str, code: impl FnOnce()) { #[cfg(debug_assertions)] expect_panic(_context, code);