Skip to content

Commit

Permalink
Move spin to bevy_platform_support out of other crates (#17470)
Browse files Browse the repository at this point in the history
# 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 <benjamin.brienen@outlook.com>
  • Loading branch information
3 people authored Jan 23, 2025
1 parent dd2d84b commit 04990fc
Show file tree
Hide file tree
Showing 22 changed files with 753 additions and 147 deletions.
5 changes: 0 additions & 5 deletions crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]

Expand Down Expand Up @@ -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"
Expand Down
15 changes: 1 addition & 14 deletions crates/bevy_ecs/src/intern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -143,24 +138,16 @@ impl<T: Internable + ?Sized> Interner<T> {
/// will return [`Interned<T>`] using the same static reference.
pub fn intern(&self, value: &T) -> Interned<T> {
{
#[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 {
Expand Down
15 changes: 14 additions & 1 deletion crates/bevy_platform_support/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ std = [
"critical-section?/std",
"portable-atomic?/std",
"portable-atomic-util?/std",
"spin/std",
]

alloc = ["portable-atomic-util?/alloc"]
Expand All @@ -31,14 +32,26 @@ 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 }
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 }
Expand Down
25 changes: 25 additions & 0 deletions crates/bevy_platform_support/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
30 changes: 0 additions & 30 deletions crates/bevy_platform_support/src/sync.rs

This file was deleted.

17 changes: 17 additions & 0 deletions crates/bevy_platform_support/src/sync/atomic.rs
Original file line number Diff line number Diff line change
@@ -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;
66 changes: 66 additions & 0 deletions crates/bevy_platform_support/src/sync/barrier.rs
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
11 changes: 11 additions & 0 deletions crates/bevy_platform_support/src/sync/lazy_lock.rs
Original file line number Diff line number Diff line change
@@ -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;
}
33 changes: 33 additions & 0 deletions crates/bevy_platform_support/src/sync/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
108 changes: 108 additions & 0 deletions crates/bevy_platform_support/src/sync/mutex.rs
Original file line number Diff line number Diff line change
@@ -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<T: ?Sized> {
inner: spin::Mutex<T>,
}

impl<T> Mutex<T> {
/// 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<T: ?Sized> Mutex<T> {
/// 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<MutexGuard<'_, T>> {
Ok(self.inner.lock())
}

/// Attempts to acquire this lock.
///
/// See the standard library for further details.
pub fn try_lock(&self) -> TryLockResult<MutexGuard<'_, T>> {
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<T>
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<T> From<T> for Mutex<T> {
fn from(t: T) -> Self {
Mutex::new(t)
}
}

impl<T: ?Sized + Default> Default for Mutex<T> {
fn default() -> Mutex<T> {
Mutex::new(Default::default())
}
}

impl<T: ?Sized + fmt::Debug> fmt::Debug for Mutex<T> {
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!("<locked>"));
}
}
d.field("poisoned", &false);
d.finish_non_exhaustive()
}
}
}
Loading

0 comments on commit 04990fc

Please sign in to comment.