From 04990fcd275f4fb3540118cc4de29c6be3fcfe83 Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Thu, 23 Jan 2025 16:27:02 +1100 Subject: [PATCH] Move `spin` to `bevy_platform_support` out of other crates (#17470) # Objective - Contributes to #16877 ## Solution - Expanded `bevy_platform_support::sync` module to provide API-compatible replacements for `std` items such as `RwLock`, `Mutex`, and `OnceLock`. - Removed `spin` from all crates except `bevy_platform_support`. ## Testing - CI --- ## Notes - The sync primitives, while verbose, entirely rely on `spin` for their implementation requiring no `unsafe` and not changing the status-quo on _how_ locks actually work within Bevy. This is just a refactoring to consolidate the "hacks" and workarounds required to get a consistent experience when either using `std::sync` or `spin`. - I have opted to rely on `std::sync` for `std` compatible locks, maintaining the status quo. However, now that we have these locks factored out into the own module, it would be trivial to investigate alternate locking backends, such as `parking_lot`. - API for these locking types is entirely based on `std`. I have implemented methods and types which aren't currently in use within Bevy (e.g., `LazyLock` and `Once`) for the sake of completeness. As the standard library is highly stable, I don't expect the Bevy and `std` implementations to drift apart much if at all. --------- Co-authored-by: BD103 <59022059+BD103@users.noreply.github.com> Co-authored-by: Benjamin Brienen --- crates/bevy_ecs/Cargo.toml | 5 - crates/bevy_ecs/src/intern.rs | 15 +- crates/bevy_platform_support/Cargo.toml | 15 +- crates/bevy_platform_support/src/lib.rs | 25 ++ crates/bevy_platform_support/src/sync.rs | 30 --- .../bevy_platform_support/src/sync/atomic.rs | 17 ++ .../bevy_platform_support/src/sync/barrier.rs | 66 ++++++ .../src/sync/lazy_lock.rs | 11 + crates/bevy_platform_support/src/sync/mod.rs | 33 +++ .../bevy_platform_support/src/sync/mutex.rs | 108 +++++++++ crates/bevy_platform_support/src/sync/once.rs | 216 ++++++++++++++++++ .../bevy_platform_support/src/sync/poison.rs | 107 +++++++++ .../bevy_platform_support/src/sync/rwlock.rs | 124 ++++++++++ crates/bevy_reflect/Cargo.toml | 6 - crates/bevy_reflect/src/type_registry.rs | 36 +-- crates/bevy_reflect/src/utility.rs | 31 +-- crates/bevy_tasks/Cargo.toml | 7 - .../src/single_threaded_task_pool.rs | 9 +- crates/bevy_tasks/src/usages.rs | 21 +- crates/bevy_window/Cargo.toml | 5 +- crates/bevy_window/src/lib.rs | 6 +- crates/bevy_window/src/raw_handle.rs | 7 +- 22 files changed, 753 insertions(+), 147 deletions(-) delete mode 100644 crates/bevy_platform_support/src/sync.rs create mode 100644 crates/bevy_platform_support/src/sync/atomic.rs create mode 100644 crates/bevy_platform_support/src/sync/barrier.rs create mode 100644 crates/bevy_platform_support/src/sync/lazy_lock.rs create mode 100644 crates/bevy_platform_support/src/sync/mod.rs create mode 100644 crates/bevy_platform_support/src/sync/mutex.rs create mode 100644 crates/bevy_platform_support/src/sync/once.rs create mode 100644 crates/bevy_platform_support/src/sync/poison.rs create mode 100644 crates/bevy_platform_support/src/sync/rwlock.rs diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index ff9b89405f..376a61f1e9 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -93,7 +93,6 @@ portable-atomic = [ "bevy_tasks?/portable-atomic", "bevy_platform_support/portable-atomic", "concurrent-queue/portable-atomic", - "spin/portable_atomic", "bevy_reflect?/portable-atomic", ] @@ -128,10 +127,6 @@ arrayvec = { version = "0.7.4", default-features = false, optional = true } smallvec = { version = "1", features = ["union", "const_generics"] } indexmap = { version = "2.5.0", default-features = false } variadics_please = { version = "1.1", default-features = false } -spin = { version = "0.9.8", default-features = false, features = [ - "spin_mutex", - "rwlock", -] } tracing = { version = "0.1", default-features = false, optional = true } log = { version = "0.4", default-features = false } bumpalo = "3" diff --git a/crates/bevy_ecs/src/intern.rs b/crates/bevy_ecs/src/intern.rs index 18e866e87d..68b9c1bcfb 100644 --- a/crates/bevy_ecs/src/intern.rs +++ b/crates/bevy_ecs/src/intern.rs @@ -7,12 +7,7 @@ use alloc::{borrow::ToOwned, boxed::Box}; use core::{fmt::Debug, hash::Hash, ops::Deref}; -#[cfg(feature = "std")] -use std::sync::{PoisonError, RwLock}; - -#[cfg(not(feature = "std"))] -use spin::rwlock::RwLock; - +use bevy_platform_support::sync::{PoisonError, RwLock}; use bevy_utils::{FixedHasher, HashSet}; /// An interned value. Will stay valid until the end of the program and will not drop. @@ -143,24 +138,16 @@ impl Interner { /// will return [`Interned`] using the same static reference. pub fn intern(&self, value: &T) -> Interned { { - #[cfg(feature = "std")] let set = self.0.read().unwrap_or_else(PoisonError::into_inner); - #[cfg(not(feature = "std"))] - let set = self.0.read(); - if let Some(value) = set.get(value) { return Interned(*value); } } { - #[cfg(feature = "std")] let mut set = self.0.write().unwrap_or_else(PoisonError::into_inner); - #[cfg(not(feature = "std"))] - let mut set = self.0.write(); - if let Some(value) = set.get(value) { Interned(*value) } else { diff --git a/crates/bevy_platform_support/Cargo.toml b/crates/bevy_platform_support/Cargo.toml index 91a7f4f6ca..7ab97bbc42 100644 --- a/crates/bevy_platform_support/Cargo.toml +++ b/crates/bevy_platform_support/Cargo.toml @@ -21,6 +21,7 @@ std = [ "critical-section?/std", "portable-atomic?/std", "portable-atomic-util?/std", + "spin/std", ] alloc = ["portable-atomic-util?/alloc"] @@ -31,7 +32,11 @@ critical-section = ["dep:critical-section", "portable-atomic?/critical-section"] ## `portable-atomic` provides additional platform support for atomic types and ## operations, even on targets without native support. -portable-atomic = ["dep:portable-atomic", "dep:portable-atomic-util"] +portable-atomic = [ + "dep:portable-atomic", + "dep:portable-atomic-util", + "spin/portable-atomic", +] [dependencies] critical-section = { version = "1.2.0", default-features = false, optional = true } @@ -39,6 +44,14 @@ portable-atomic = { version = "1", default-features = false, features = [ "fallback", ], optional = true } portable-atomic-util = { version = "0.2.4", default-features = false, optional = true } +spin = { version = "0.9.8", default-features = false, features = [ + "mutex", + "spin_mutex", + "rwlock", + "once", + "lazy", + "barrier", +] } [target.'cfg(target_arch = "wasm32")'.dependencies] web-time = { version = "1.1", default-features = false } diff --git a/crates/bevy_platform_support/src/lib.rs b/crates/bevy_platform_support/src/lib.rs index d93295a28c..ae571e3286 100644 --- a/crates/bevy_platform_support/src/lib.rs +++ b/crates/bevy_platform_support/src/lib.rs @@ -17,3 +17,28 @@ extern crate alloc; pub mod sync; pub mod time; + +/// Frequently used items which would typically be included in most contexts. +/// +/// When adding `no_std` support to a crate for the first time, often there's a substantial refactor +/// required due to the change in implicit prelude from `std::prelude` to `core::prelude`. +/// This unfortunately leaves out many items from `alloc`, even if the crate unconditionally +/// includes that crate. +/// +/// This prelude aims to ease the transition by re-exporting items from `alloc` which would +/// otherwise be included in the `std` implicit prelude. +pub mod prelude { + #[cfg(feature = "alloc")] + pub use alloc::{ + borrow::ToOwned, boxed::Box, format, string::String, string::ToString, vec, vec::Vec, + }; + + // Items from `std::prelude` that are missing in this module: + // * dbg + // * eprint + // * eprintln + // * is_x86_feature_detected + // * print + // * println + // * thread_local +} diff --git a/crates/bevy_platform_support/src/sync.rs b/crates/bevy_platform_support/src/sync.rs deleted file mode 100644 index 8fd47989da..0000000000 --- a/crates/bevy_platform_support/src/sync.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Provides various synchronization alternatives to language primitives. - -#[cfg(feature = "alloc")] -pub use arc::{Arc, Weak}; - -pub mod atomic { - //! Provides various atomic alternatives to language primitives. - //! - //! Certain platforms lack complete atomic support, requiring the use of a fallback - //! such as `portable-atomic`. - //! Using these types will ensure the correct atomic provider is used without the need for - //! feature gates in your own code. - - pub use atomic::{ - AtomicBool, AtomicI16, AtomicI32, AtomicI64, AtomicI8, AtomicIsize, AtomicPtr, AtomicU16, - AtomicU32, AtomicU64, AtomicU8, AtomicUsize, Ordering, - }; - - #[cfg(not(feature = "portable-atomic"))] - use core::sync::atomic; - - #[cfg(feature = "portable-atomic")] - use portable_atomic as atomic; -} - -#[cfg(all(feature = "alloc", feature = "portable-atomic"))] -use portable_atomic_util as arc; - -#[cfg(all(feature = "alloc", not(feature = "portable-atomic")))] -use alloc::sync as arc; diff --git a/crates/bevy_platform_support/src/sync/atomic.rs b/crates/bevy_platform_support/src/sync/atomic.rs new file mode 100644 index 0000000000..9e8eadb3df --- /dev/null +++ b/crates/bevy_platform_support/src/sync/atomic.rs @@ -0,0 +1,17 @@ +//! Provides various atomic alternatives to language primitives. +//! +//! Certain platforms lack complete atomic support, requiring the use of a fallback +//! such as `portable-atomic`. +//! Using these types will ensure the correct atomic provider is used without the need for +//! feature gates in your own code. + +pub use atomic::{ + AtomicBool, AtomicI16, AtomicI32, AtomicI64, AtomicI8, AtomicIsize, AtomicPtr, AtomicU16, + AtomicU32, AtomicU64, AtomicU8, AtomicUsize, Ordering, +}; + +#[cfg(not(feature = "portable-atomic"))] +use core::sync::atomic; + +#[cfg(feature = "portable-atomic")] +use portable_atomic as atomic; diff --git a/crates/bevy_platform_support/src/sync/barrier.rs b/crates/bevy_platform_support/src/sync/barrier.rs new file mode 100644 index 0000000000..6c179d81d6 --- /dev/null +++ b/crates/bevy_platform_support/src/sync/barrier.rs @@ -0,0 +1,66 @@ +//! Provides `Barrier` and `BarrierWaitResult` + +pub use barrier::{Barrier, BarrierWaitResult}; + +#[cfg(feature = "std")] +use std::sync as barrier; + +#[cfg(not(feature = "std"))] +mod barrier { + use core::fmt; + + /// Fallback implementation of `Barrier` from the standard library. + pub struct Barrier { + inner: spin::Barrier, + } + + impl Barrier { + /// Creates a new barrier that can block a given number of threads. + /// + /// See the standard library for further details. + #[must_use] + pub const fn new(n: usize) -> Self { + Self { + inner: spin::Barrier::new(n), + } + } + + /// Blocks the current thread until all threads have rendezvoused here. + /// + /// See the standard library for further details. + pub fn wait(&self) -> BarrierWaitResult { + BarrierWaitResult { + inner: self.inner.wait(), + } + } + } + + impl fmt::Debug for Barrier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Barrier").finish_non_exhaustive() + } + } + + /// Fallback implementation of `BarrierWaitResult` from the standard library. + pub struct BarrierWaitResult { + inner: spin::barrier::BarrierWaitResult, + } + + impl BarrierWaitResult { + /// Returns `true` if this thread is the "leader thread" for the call to [`Barrier::wait()`]. + /// + /// See the standard library for further details. + #[must_use] + pub fn is_leader(&self) -> bool { + self.inner.is_leader() + } + } + + impl fmt::Debug for BarrierWaitResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BarrierWaitResult") + .field("is_leader", &self.is_leader()) + .finish() + } + } +} diff --git a/crates/bevy_platform_support/src/sync/lazy_lock.rs b/crates/bevy_platform_support/src/sync/lazy_lock.rs new file mode 100644 index 0000000000..8a13c1bef2 --- /dev/null +++ b/crates/bevy_platform_support/src/sync/lazy_lock.rs @@ -0,0 +1,11 @@ +//! Provides `LazyLock` + +pub use lazy_lock::LazyLock; + +#[cfg(feature = "std")] +use std::sync as lazy_lock; + +#[cfg(not(feature = "std"))] +mod lazy_lock { + pub use spin::Lazy as LazyLock; +} diff --git a/crates/bevy_platform_support/src/sync/mod.rs b/crates/bevy_platform_support/src/sync/mod.rs new file mode 100644 index 0000000000..edb0217262 --- /dev/null +++ b/crates/bevy_platform_support/src/sync/mod.rs @@ -0,0 +1,33 @@ +//! Provides various synchronization alternatives to language primitives. +//! +//! Currently missing from this module are the following items: +//! * `Condvar` +//! * `WaitTimeoutResult` +//! * `mpsc` +//! +//! Otherwise, this is a drop-in replacement for `std::sync`. + +pub use barrier::{Barrier, BarrierWaitResult}; +pub use lazy_lock::LazyLock; +pub use mutex::{Mutex, MutexGuard}; +pub use once::{Once, OnceLock, OnceState}; +pub use poison::{LockResult, PoisonError, TryLockError, TryLockResult}; +pub use rwlock::{RwLock, RwLockReadGuard, RwLockWriteGuard}; + +#[cfg(feature = "alloc")] +pub use arc::{Arc, Weak}; + +pub mod atomic; + +mod barrier; +mod lazy_lock; +mod mutex; +mod once; +mod poison; +mod rwlock; + +#[cfg(all(feature = "alloc", feature = "portable-atomic"))] +use portable_atomic_util as arc; + +#[cfg(all(feature = "alloc", not(feature = "portable-atomic")))] +use alloc::sync as arc; diff --git a/crates/bevy_platform_support/src/sync/mutex.rs b/crates/bevy_platform_support/src/sync/mutex.rs new file mode 100644 index 0000000000..a059d670e9 --- /dev/null +++ b/crates/bevy_platform_support/src/sync/mutex.rs @@ -0,0 +1,108 @@ +//! Provides `Mutex` and `MutexGuard` + +pub use mutex::{Mutex, MutexGuard}; + +#[cfg(feature = "std")] +use std::sync as mutex; + +#[cfg(not(feature = "std"))] +mod mutex { + use crate::sync::{LockResult, TryLockError, TryLockResult}; + use core::fmt; + + pub use spin::MutexGuard; + + /// Fallback implementation of `Mutex` from the standard library. + pub struct Mutex { + inner: spin::Mutex, + } + + impl Mutex { + /// Creates a new mutex in an unlocked state ready for use. + /// + /// See the standard library for further details. + pub const fn new(t: T) -> Self { + Self { + inner: spin::Mutex::new(t), + } + } + } + + impl Mutex { + /// Acquires a mutex, blocking the current thread until it is able to do so. + /// + /// See the standard library for further details. + pub fn lock(&self) -> LockResult> { + Ok(self.inner.lock()) + } + + /// Attempts to acquire this lock. + /// + /// See the standard library for further details. + pub fn try_lock(&self) -> TryLockResult> { + self.inner.try_lock().ok_or(TryLockError::WouldBlock) + } + + /// Determines whether the mutex is poisoned. + /// + /// See the standard library for further details. + pub fn is_poisoned(&self) -> bool { + false + } + + /// Clear the poisoned state from a mutex. + /// + /// See the standard library for further details. + pub fn clear_poison(&self) { + // no-op + } + + /// Consumes this mutex, returning the underlying data. + /// + /// See the standard library for further details. + pub fn into_inner(self) -> LockResult + where + T: Sized, + { + Ok(self.inner.into_inner()) + } + + /// Returns a mutable reference to the underlying data. + /// + /// See the standard library for further details. + pub fn get_mut(&mut self) -> LockResult<&mut T> { + Ok(self.inner.get_mut()) + } + } + + impl From for Mutex { + fn from(t: T) -> Self { + Mutex::new(t) + } + } + + impl Default for Mutex { + fn default() -> Mutex { + Mutex::new(Default::default()) + } + } + + impl fmt::Debug for Mutex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("Mutex"); + match self.try_lock() { + Ok(guard) => { + d.field("data", &&*guard); + } + Err(TryLockError::Poisoned(err)) => { + d.field("data", &&**err.get_ref()); + } + Err(TryLockError::WouldBlock) => { + d.field("data", &format_args!("")); + } + } + d.field("poisoned", &false); + d.finish_non_exhaustive() + } + } +} diff --git a/crates/bevy_platform_support/src/sync/once.rs b/crates/bevy_platform_support/src/sync/once.rs new file mode 100644 index 0000000000..2ae733f387 --- /dev/null +++ b/crates/bevy_platform_support/src/sync/once.rs @@ -0,0 +1,216 @@ +//! Provides `Once`, `OnceState`, `OnceLock` + +pub use once::{Once, OnceLock, OnceState}; + +#[cfg(feature = "std")] +use std::sync as once; + +#[cfg(not(feature = "std"))] +mod once { + use core::{ + fmt, + panic::{RefUnwindSafe, UnwindSafe}, + }; + + /// Fallback implementation of `OnceLock` from the standard library. + pub struct OnceLock { + inner: spin::Once, + } + + impl OnceLock { + /// Creates a new empty cell. + /// + /// See the standard library for further details. + #[must_use] + pub const fn new() -> Self { + Self { + inner: spin::Once::new(), + } + } + + /// Gets the reference to the underlying value. + /// + /// See the standard library for further details. + pub fn get(&self) -> Option<&T> { + self.inner.get() + } + + /// Gets the mutable reference to the underlying value. + /// + /// See the standard library for further details. + pub fn get_mut(&mut self) -> Option<&mut T> { + self.inner.get_mut() + } + + /// Sets the contents of this cell to `value`. + /// + /// See the standard library for further details. + pub fn set(&self, value: T) -> Result<(), T> { + let mut value = Some(value); + + self.inner.call_once(|| value.take().unwrap()); + + match value { + Some(value) => Err(value), + None => Ok(()), + } + } + + /// Gets the contents of the cell, initializing it with `f` if the cell + /// was empty. + /// + /// See the standard library for further details. + pub fn get_or_init(&self, f: F) -> &T + where + F: FnOnce() -> T, + { + self.inner.call_once(f) + } + + /// Consumes the `OnceLock`, returning the wrapped value. Returns + /// `None` if the cell was empty. + /// + /// See the standard library for further details. + pub fn into_inner(mut self) -> Option { + self.take() + } + + /// Takes the value out of this `OnceLock`, moving it back to an uninitialized state. + /// + /// See the standard library for further details. + pub fn take(&mut self) -> Option { + if self.inner.is_completed() { + let mut inner = spin::Once::new(); + + core::mem::swap(&mut self.inner, &mut inner); + + inner.try_into_inner() + } else { + None + } + } + } + + impl RefUnwindSafe for OnceLock {} + impl UnwindSafe for OnceLock {} + + impl Default for OnceLock { + fn default() -> OnceLock { + OnceLock::new() + } + } + + impl fmt::Debug for OnceLock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_tuple("OnceLock"); + match self.get() { + Some(v) => d.field(v), + None => d.field(&format_args!("")), + }; + d.finish() + } + } + + impl Clone for OnceLock { + fn clone(&self) -> OnceLock { + let cell = Self::new(); + if let Some(value) = self.get() { + cell.set(value.clone()).ok().unwrap(); + } + cell + } + } + + impl From for OnceLock { + fn from(value: T) -> Self { + let cell = Self::new(); + cell.set(value).map(move |_| cell).ok().unwrap() + } + } + + impl PartialEq for OnceLock { + fn eq(&self, other: &OnceLock) -> bool { + self.get() == other.get() + } + } + + impl Eq for OnceLock {} + + /// Fallback implementation of `Once` from the standard library. + pub struct Once { + inner: OnceLock<()>, + } + + impl Once { + /// Creates a new `Once` value. + /// + /// See the standard library for further details. + pub const fn new() -> Self { + Self { + inner: OnceLock::new(), + } + } + + /// Performs an initialization routine once and only once. The given closure + /// will be executed if this is the first time `call_once` has been called, + /// and otherwise the routine will *not* be invoked. + /// + /// See the standard library for further details. + pub fn call_once(&self, f: F) { + self.inner.get_or_init(f); + } + + /// Performs the same function as [`call_once()`] except ignores poisoning. + /// + /// See the standard library for further details. + pub fn call_once_force(&self, f: F) { + const STATE: OnceState = OnceState { _private: () }; + + self.call_once(move || f(&STATE)); + } + + /// Returns `true` if some [`call_once()`] call has completed + /// successfully. Specifically, `is_completed` will return false in + /// the following situations: + /// * [`call_once()`] was not called at all, + /// * [`call_once()`] was called, but has not yet completed, + /// * the [`Once`] instance is poisoned + /// + /// See the standard library for further details. + pub fn is_completed(&self) -> bool { + self.inner.get().is_some() + } + } + + impl RefUnwindSafe for Once {} + impl UnwindSafe for Once {} + + impl fmt::Debug for Once { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Once").finish_non_exhaustive() + } + } + + /// Fallback implementation of `OnceState` from the standard library. + pub struct OnceState { + _private: (), + } + + impl OnceState { + /// Returns `true` if the associated [`Once`] was poisoned prior to the + /// invocation of the closure passed to [`Once::call_once_force()`]. + /// + /// See the standard library for further details. + pub fn is_poisoned(&self) -> bool { + false + } + } + + impl fmt::Debug for OnceState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OnceState") + .field("poisoned", &self.is_poisoned()) + .finish() + } + } +} diff --git a/crates/bevy_platform_support/src/sync/poison.rs b/crates/bevy_platform_support/src/sync/poison.rs new file mode 100644 index 0000000000..0aa8e168c2 --- /dev/null +++ b/crates/bevy_platform_support/src/sync/poison.rs @@ -0,0 +1,107 @@ +//! Provides `LockResult`, `PoisonError`, `TryLockError`, `TryLockResult` + +pub use poison::{LockResult, PoisonError, TryLockError, TryLockResult}; + +#[cfg(feature = "std")] +use std::sync as poison; + +#[cfg(not(feature = "std"))] +mod poison { + use core::{error::Error, fmt}; + + /// Fallback implementation of `PoisonError` from the standard library. + pub struct PoisonError { + guard: T, + } + + impl fmt::Debug for PoisonError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PoisonError").finish_non_exhaustive() + } + } + + impl fmt::Display for PoisonError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + "poisoned lock: another task failed inside".fmt(f) + } + } + + impl Error for PoisonError {} + + impl PoisonError { + /// Creates a `PoisonError`. + /// + /// See the standard library for further details. + #[cfg(panic = "unwind")] + pub fn new(guard: T) -> PoisonError { + PoisonError { guard } + } + + /// Consumes this error indicating that a lock is poisoned, returning the + /// underlying guard to allow access regardless. + /// + /// See the standard library for further details. + pub fn into_inner(self) -> T { + self.guard + } + + /// Reaches into this error indicating that a lock is poisoned, returning a + /// reference to the underlying guard to allow access regardless. + /// + /// See the standard library for further details. + pub fn get_ref(&self) -> &T { + &self.guard + } + + /// Reaches into this error indicating that a lock is poisoned, returning a + /// mutable reference to the underlying guard to allow access regardless. + /// + /// See the standard library for further details. + pub fn get_mut(&mut self) -> &mut T { + &mut self.guard + } + } + + /// Fallback implementation of `TryLockError` from the standard library. + pub enum TryLockError { + /// The lock could not be acquired because another thread failed while holding + /// the lock. + Poisoned(PoisonError), + /// The lock could not be acquired at this time because the operation would + /// otherwise block. + WouldBlock, + } + + impl From> for TryLockError { + fn from(err: PoisonError) -> TryLockError { + TryLockError::Poisoned(err) + } + } + + impl fmt::Debug for TryLockError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + TryLockError::Poisoned(..) => "Poisoned(..)".fmt(f), + TryLockError::WouldBlock => "WouldBlock".fmt(f), + } + } + } + + impl fmt::Display for TryLockError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + TryLockError::Poisoned(..) => "poisoned lock: another task failed inside", + TryLockError::WouldBlock => "try_lock failed because the operation would block", + } + .fmt(f) + } + } + + impl Error for TryLockError {} + + /// Fallback implementation of `LockResult` from the standard library. + pub type LockResult = Result>; + + /// Fallback implementation of `TryLockResult` from the standard library. + pub type TryLockResult = Result>; +} diff --git a/crates/bevy_platform_support/src/sync/rwlock.rs b/crates/bevy_platform_support/src/sync/rwlock.rs new file mode 100644 index 0000000000..627da73f32 --- /dev/null +++ b/crates/bevy_platform_support/src/sync/rwlock.rs @@ -0,0 +1,124 @@ +//! TODO: Implement `RwLock`, `RwLockReadGuard`, `RwLockWriteGuard` + +pub use rwlock::{RwLock, RwLockReadGuard, RwLockWriteGuard}; + +#[cfg(feature = "std")] +use std::sync as rwlock; + +#[cfg(not(feature = "std"))] +mod rwlock { + use crate::sync::{LockResult, TryLockError, TryLockResult}; + use core::fmt; + + pub use spin::rwlock::{RwLockReadGuard, RwLockWriteGuard}; + + /// Fallback implementation of `RwLock` from the standard library. + pub struct RwLock { + inner: spin::RwLock, + } + + impl RwLock { + /// Creates a new instance of an `RwLock` which is unlocked. + /// + /// See the standard library for further details. + pub const fn new(t: T) -> RwLock { + Self { + inner: spin::RwLock::new(t), + } + } + } + + impl RwLock { + /// Locks this `RwLock` with shared read access, blocking the current thread + /// until it can be acquired. + /// + /// See the standard library for further details. + pub fn read(&self) -> LockResult> { + Ok(self.inner.read()) + } + + /// Attempts to acquire this `RwLock` with shared read access. + /// + /// See the standard library for further details. + pub fn try_read(&self) -> TryLockResult> { + self.inner.try_read().ok_or(TryLockError::WouldBlock) + } + + /// Locks this `RwLock` with exclusive write access, blocking the current + /// thread until it can be acquired. + /// + /// See the standard library for further details. + pub fn write(&self) -> LockResult> { + Ok(self.inner.write()) + } + + /// Attempts to lock this `RwLock` with exclusive write access. + /// + /// See the standard library for further details. + pub fn try_write(&self) -> TryLockResult> { + self.inner.try_write().ok_or(TryLockError::WouldBlock) + } + + /// Determines whether the lock is poisoned. + /// + /// See the standard library for further details. + pub fn is_poisoned(&self) -> bool { + false + } + + /// Clear the poisoned state from a lock. + /// + /// See the standard library for further details. + pub fn clear_poison(&self) { + // no-op + } + + /// Consumes this `RwLock`, returning the underlying data. + /// + /// See the standard library for further details. + pub fn into_inner(self) -> LockResult + where + T: Sized, + { + Ok(self.inner.into_inner()) + } + + /// Returns a mutable reference to the underlying data. + /// + /// See the standard library for further details. + pub fn get_mut(&mut self) -> LockResult<&mut T> { + Ok(self.inner.get_mut()) + } + } + + impl fmt::Debug for RwLock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("RwLock"); + match self.try_read() { + Ok(guard) => { + d.field("data", &&*guard); + } + Err(TryLockError::Poisoned(err)) => { + d.field("data", &&**err.get_ref()); + } + Err(TryLockError::WouldBlock) => { + d.field("data", &format_args!("")); + } + } + d.field("poisoned", &false); + d.finish_non_exhaustive() + } + } + + impl Default for RwLock { + fn default() -> RwLock { + RwLock::new(Default::default()) + } + } + + impl From for RwLock { + fn from(t: T) -> Self { + RwLock::new(t) + } + } +} diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 0ca295a4af..4ad1963488 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -58,7 +58,6 @@ std = [ "erased-serde/std", "downcast-rs/std", "serde/std", - "spin/std", "glam?/std", "smol_str?/std", "uuid?/std", @@ -76,7 +75,6 @@ critical-section = [ ## operations, even on targets without native support. portable-atomic = [ "bevy_platform_support/portable-atomic", - "spin/portable_atomic", "bevy_utils/portable-atomic", ] @@ -103,10 +101,6 @@ downcast-rs = { version = "2", default-features = false } thiserror = { version = "2", default-features = false } derive_more = { version = "1", default-features = false, features = ["from"] } serde = { version = "1", default-features = false, features = ["alloc"] } -spin = { version = "0.9.8", default-features = false, features = [ - "once", - "rwlock", -] } assert_type_match = "0.1.1" smallvec = { version = "1.11", default-features = false, optional = true } glam = { version = "0.29", default-features = false, features = [ diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index aedff72c1e..f8b89ce551 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -1,6 +1,6 @@ use crate::{serde::Serializable, FromReflect, Reflect, TypeInfo, TypePath, Typed}; use alloc::{boxed::Box, string::String}; -use bevy_platform_support::sync::Arc; +use bevy_platform_support::sync::{Arc, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard}; use bevy_ptr::{Ptr, PtrMut}; use bevy_utils::{HashMap, HashSet, TypeIdMap}; use core::{ @@ -11,12 +11,6 @@ use core::{ use downcast_rs::{impl_downcast, Downcast}; use serde::Deserialize; -#[cfg(feature = "std")] -use std::sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard}; - -#[cfg(not(feature = "std"))] -use spin::rwlock::{RwLock, RwLockReadGuard, RwLockWriteGuard}; - /// A registry of [reflected] types. /// /// This struct is used as the central store for type information. @@ -46,12 +40,12 @@ pub struct TypeRegistryArc { impl Debug for TypeRegistryArc { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let read_lock = self.internal.read(); - - #[cfg(feature = "std")] - let read_lock = read_lock.unwrap_or_else(PoisonError::into_inner); - - read_lock.type_path_to_id.keys().fmt(f) + self.internal + .read() + .unwrap_or_else(PoisonError::into_inner) + .type_path_to_id + .keys() + .fmt(f) } } @@ -433,22 +427,14 @@ impl TypeRegistry { impl TypeRegistryArc { /// Takes a read lock on the underlying [`TypeRegistry`]. pub fn read(&self) -> RwLockReadGuard<'_, TypeRegistry> { - let read_lock = self.internal.read(); - - #[cfg(feature = "std")] - let read_lock = read_lock.unwrap_or_else(PoisonError::into_inner); - - read_lock + self.internal.read().unwrap_or_else(PoisonError::into_inner) } /// Takes a write lock on the underlying [`TypeRegistry`]. pub fn write(&self) -> RwLockWriteGuard<'_, TypeRegistry> { - let write_lock = self.internal.write(); - - #[cfg(feature = "std")] - let write_lock = write_lock.unwrap_or_else(PoisonError::into_inner); - - write_lock + self.internal + .write() + .unwrap_or_else(PoisonError::into_inner) } } diff --git a/crates/bevy_reflect/src/utility.rs b/crates/bevy_reflect/src/utility.rs index 996a661c70..54ccc282bb 100644 --- a/crates/bevy_reflect/src/utility.rs +++ b/crates/bevy_reflect/src/utility.rs @@ -2,18 +2,13 @@ use crate::TypeInfo; use alloc::boxed::Box; +use bevy_platform_support::sync::{OnceLock, PoisonError, RwLock}; use bevy_utils::{DefaultHasher, FixedHasher, NoOpHash, TypeIdMap}; use core::{ any::{Any, TypeId}, hash::BuildHasher, }; -#[cfg(feature = "std")] -use std::sync::{OnceLock, PoisonError, RwLock}; - -#[cfg(not(feature = "std"))] -use spin::{Once as OnceLock, RwLock}; - /// A type that can be stored in a ([`Non`])[`GenericTypeCell`]. /// /// [`Non`]: NonGenericTypeCell @@ -122,11 +117,7 @@ impl NonGenericTypeCell { where F: FnOnce() -> T::Stored, { - #[cfg(feature = "std")] - return self.0.get_or_init(f); - - #[cfg(not(feature = "std"))] - return self.0.call_once(f); + self.0.get_or_init(f) } } @@ -259,12 +250,11 @@ impl GenericTypeCell { /// /// This method will then return the correct [`TypedProperty`] reference for the given type `T`. fn get_by_type_id(&self, type_id: TypeId) -> Option<&T::Stored> { - let read_lock = self.0.read(); - - #[cfg(feature = "std")] - let read_lock = read_lock.unwrap_or_else(PoisonError::into_inner); - - read_lock.get(&type_id).copied() + self.0 + .read() + .unwrap_or_else(PoisonError::into_inner) + .get(&type_id) + .copied() } /// Returns a reference to the [`TypedProperty`] stored in the cell. @@ -282,12 +272,7 @@ impl GenericTypeCell { } fn insert_by_type_id(&self, type_id: TypeId, value: T::Stored) -> &T::Stored { - let write_lock = self.0.write(); - - #[cfg(feature = "std")] - let write_lock = write_lock.unwrap_or_else(PoisonError::into_inner); - - let mut write_lock = write_lock; + let mut write_lock = self.0.write().unwrap_or_else(PoisonError::into_inner); write_lock .entry(type_id) diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index 3e0c9720c9..1020c6112d 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -13,7 +13,6 @@ default = ["std", "async_executor"] std = [ "futures-lite/std", "async-task/std", - "spin/std", "edge-executor?/std", "bevy_platform_support/std", ] @@ -28,7 +27,6 @@ portable-atomic = [ "bevy_platform_support/portable-atomic", "edge-executor?/portable-atomic", "async-task/portable-atomic", - "spin/portable_atomic", ] [dependencies] @@ -40,11 +38,6 @@ futures-lite = { version = "2.0.1", default-features = false, features = [ "alloc", ] } async-task = { version = "4.4.0", default-features = false } -spin = { version = "0.9.8", default-features = false, features = [ - "spin_mutex", - "rwlock", - "once", -] } derive_more = { version = "1", default-features = false, features = [ "deref", "deref_mut", diff --git a/crates/bevy_tasks/src/single_threaded_task_pool.rs b/crates/bevy_tasks/src/single_threaded_task_pool.rs index b928fa9b21..fc1a73e754 100644 --- a/crates/bevy_tasks/src/single_threaded_task_pool.rs +++ b/crates/bevy_tasks/src/single_threaded_task_pool.rs @@ -7,6 +7,9 @@ use crate::Task; #[cfg(feature = "std")] use std::thread_local; +#[cfg(not(feature = "std"))] +use bevy_platform_support::sync::{Mutex, PoisonError}; + #[cfg(all( feature = "std", any(feature = "async_executor", feature = "edge_executor") @@ -63,7 +66,7 @@ static LOCAL_EXECUTOR: LocalExecutor<'static> = const { LocalExecutor::new() }; type ScopeResult = alloc::rc::Rc>>; #[cfg(not(feature = "std"))] -type ScopeResult = Arc>>; +type ScopeResult = Arc>>; /// Used to create a [`TaskPool`]. #[derive(Debug, Default, Clone)] @@ -214,7 +217,7 @@ impl TaskPool { #[cfg(not(feature = "std"))] { - let mut lock = result.lock(); + let mut lock = result.lock().unwrap_or_else(PoisonError::into_inner); lock.take().unwrap() } }) @@ -343,7 +346,7 @@ impl<'scope, 'env, T: Send + 'env> Scope<'scope, 'env, T> { #[cfg(not(feature = "std"))] { - let mut lock = result.lock(); + let mut lock = result.lock().unwrap_or_else(PoisonError::into_inner); *lock = Some(temp_result); } }; diff --git a/crates/bevy_tasks/src/usages.rs b/crates/bevy_tasks/src/usages.rs index 385b30fdb4..82da333ef4 100644 --- a/crates/bevy_tasks/src/usages.rs +++ b/crates/bevy_tasks/src/usages.rs @@ -1,20 +1,11 @@ use super::TaskPool; +use bevy_platform_support::sync::OnceLock; use core::ops::Deref; -#[cfg(feature = "std")] -use std::sync::OnceLock; - -#[cfg(not(feature = "std"))] -use spin::Once; - macro_rules! taskpool { ($(#[$attr:meta])* ($static:ident, $type:ident)) => { - #[cfg(feature = "std")] static $static: OnceLock<$type> = OnceLock::new(); - #[cfg(not(feature = "std"))] - static $static: Once<$type> = Once::new(); - $(#[$attr])* #[derive(Debug)] pub struct $type(TaskPool); @@ -22,15 +13,7 @@ macro_rules! taskpool { impl $type { #[doc = concat!(" Gets the global [`", stringify!($type), "`] instance, or initializes it with `f`.")] pub fn get_or_init(f: impl FnOnce() -> TaskPool) -> &'static Self { - #[cfg(feature = "std")] - { - $static.get_or_init(|| Self(f())) - } - - #[cfg(not(feature = "std"))] - { - $static.call_once(|| Self(f())) - } + $static.get_or_init(|| Self(f())) } #[doc = concat!(" Attempts to get the global [`", stringify!($type), "`] instance, \ diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index 7e8ec86a6c..eb6dd3d782 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -42,6 +42,7 @@ std = [ "bevy_reflect?/std", "serde?/std", "raw-window-handle/std", + "bevy_platform_support/std", ] ## Uses the `libm` maths library instead of the one provided in `std` and `core`. @@ -58,6 +59,7 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-featu "smol_str", ], optional = true } bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } +bevy_platform_support = { path = "../bevy_platform_support", version = "0.16.0-dev", default-features = false } # other serde = { version = "1.0", features = [ @@ -69,9 +71,6 @@ raw-window-handle = { version = "0.6", features = [ ], default-features = false } smol_str = { version = "0.2", default-features = false } log = { version = "0.4", default-features = false } -spin = { version = "0.9.8", default-features = false, features = [ - "spin_mutex", -] } [target.'cfg(target_os = "android")'.dependencies] android-activity = "0.6" diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index 8ef4cb9c0c..add17c7efe 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -19,11 +19,7 @@ extern crate alloc; use alloc::sync::Arc; -#[cfg(feature = "std")] -use std::sync::Mutex; - -#[cfg(not(feature = "std"))] -use spin::mutex::Mutex; +use bevy_platform_support::sync::Mutex; mod event; mod monitor; diff --git a/crates/bevy_window/src/raw_handle.rs b/crates/bevy_window/src/raw_handle.rs index c416a0f3f1..3894eb283e 100644 --- a/crates/bevy_window/src/raw_handle.rs +++ b/crates/bevy_window/src/raw_handle.rs @@ -5,18 +5,13 @@ use alloc::sync::Arc; use bevy_ecs::prelude::Component; +use bevy_platform_support::sync::Mutex; use core::{any::Any, marker::PhantomData, ops::Deref}; use raw_window_handle::{ DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle, WindowHandle, }; -#[cfg(feature = "std")] -use std::sync::Mutex; - -#[cfg(not(feature = "std"))] -use spin::mutex::Mutex; - /// A wrapper over a window. /// /// This allows us to extend the lifetime of the window, so it doesn't get eagerly dropped while a