Skip to content

Commit 04990fc

Browse files
bushrat011899BD103BenjaminBrienen
authored
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 <[email protected]> Co-authored-by: Benjamin Brienen <[email protected]>
1 parent dd2d84b commit 04990fc

22 files changed

+753
-147
lines changed

crates/bevy_ecs/Cargo.toml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ portable-atomic = [
9393
"bevy_tasks?/portable-atomic",
9494
"bevy_platform_support/portable-atomic",
9595
"concurrent-queue/portable-atomic",
96-
"spin/portable_atomic",
9796
"bevy_reflect?/portable-atomic",
9897
]
9998

@@ -128,10 +127,6 @@ arrayvec = { version = "0.7.4", default-features = false, optional = true }
128127
smallvec = { version = "1", features = ["union", "const_generics"] }
129128
indexmap = { version = "2.5.0", default-features = false }
130129
variadics_please = { version = "1.1", default-features = false }
131-
spin = { version = "0.9.8", default-features = false, features = [
132-
"spin_mutex",
133-
"rwlock",
134-
] }
135130
tracing = { version = "0.1", default-features = false, optional = true }
136131
log = { version = "0.4", default-features = false }
137132
bumpalo = "3"

crates/bevy_ecs/src/intern.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@
77
use alloc::{borrow::ToOwned, boxed::Box};
88
use core::{fmt::Debug, hash::Hash, ops::Deref};
99

10-
#[cfg(feature = "std")]
11-
use std::sync::{PoisonError, RwLock};
12-
13-
#[cfg(not(feature = "std"))]
14-
use spin::rwlock::RwLock;
15-
10+
use bevy_platform_support::sync::{PoisonError, RwLock};
1611
use bevy_utils::{FixedHasher, HashSet};
1712

1813
/// An interned value. Will stay valid until the end of the program and will not drop.
@@ -143,24 +138,16 @@ impl<T: Internable + ?Sized> Interner<T> {
143138
/// will return [`Interned<T>`] using the same static reference.
144139
pub fn intern(&self, value: &T) -> Interned<T> {
145140
{
146-
#[cfg(feature = "std")]
147141
let set = self.0.read().unwrap_or_else(PoisonError::into_inner);
148142

149-
#[cfg(not(feature = "std"))]
150-
let set = self.0.read();
151-
152143
if let Some(value) = set.get(value) {
153144
return Interned(*value);
154145
}
155146
}
156147

157148
{
158-
#[cfg(feature = "std")]
159149
let mut set = self.0.write().unwrap_or_else(PoisonError::into_inner);
160150

161-
#[cfg(not(feature = "std"))]
162-
let mut set = self.0.write();
163-
164151
if let Some(value) = set.get(value) {
165152
Interned(*value)
166153
} else {

crates/bevy_platform_support/Cargo.toml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ std = [
2121
"critical-section?/std",
2222
"portable-atomic?/std",
2323
"portable-atomic-util?/std",
24+
"spin/std",
2425
]
2526

2627
alloc = ["portable-atomic-util?/alloc"]
@@ -31,14 +32,26 @@ critical-section = ["dep:critical-section", "portable-atomic?/critical-section"]
3132

3233
## `portable-atomic` provides additional platform support for atomic types and
3334
## operations, even on targets without native support.
34-
portable-atomic = ["dep:portable-atomic", "dep:portable-atomic-util"]
35+
portable-atomic = [
36+
"dep:portable-atomic",
37+
"dep:portable-atomic-util",
38+
"spin/portable-atomic",
39+
]
3540

3641
[dependencies]
3742
critical-section = { version = "1.2.0", default-features = false, optional = true }
3843
portable-atomic = { version = "1", default-features = false, features = [
3944
"fallback",
4045
], optional = true }
4146
portable-atomic-util = { version = "0.2.4", default-features = false, optional = true }
47+
spin = { version = "0.9.8", default-features = false, features = [
48+
"mutex",
49+
"spin_mutex",
50+
"rwlock",
51+
"once",
52+
"lazy",
53+
"barrier",
54+
] }
4255

4356
[target.'cfg(target_arch = "wasm32")'.dependencies]
4457
web-time = { version = "1.1", default-features = false }

crates/bevy_platform_support/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,28 @@ extern crate alloc;
1717

1818
pub mod sync;
1919
pub mod time;
20+
21+
/// Frequently used items which would typically be included in most contexts.
22+
///
23+
/// When adding `no_std` support to a crate for the first time, often there's a substantial refactor
24+
/// required due to the change in implicit prelude from `std::prelude` to `core::prelude`.
25+
/// This unfortunately leaves out many items from `alloc`, even if the crate unconditionally
26+
/// includes that crate.
27+
///
28+
/// This prelude aims to ease the transition by re-exporting items from `alloc` which would
29+
/// otherwise be included in the `std` implicit prelude.
30+
pub mod prelude {
31+
#[cfg(feature = "alloc")]
32+
pub use alloc::{
33+
borrow::ToOwned, boxed::Box, format, string::String, string::ToString, vec, vec::Vec,
34+
};
35+
36+
// Items from `std::prelude` that are missing in this module:
37+
// * dbg
38+
// * eprint
39+
// * eprintln
40+
// * is_x86_feature_detected
41+
// * print
42+
// * println
43+
// * thread_local
44+
}

crates/bevy_platform_support/src/sync.rs

Lines changed: 0 additions & 30 deletions
This file was deleted.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//! Provides various atomic alternatives to language primitives.
2+
//!
3+
//! Certain platforms lack complete atomic support, requiring the use of a fallback
4+
//! such as `portable-atomic`.
5+
//! Using these types will ensure the correct atomic provider is used without the need for
6+
//! feature gates in your own code.
7+
8+
pub use atomic::{
9+
AtomicBool, AtomicI16, AtomicI32, AtomicI64, AtomicI8, AtomicIsize, AtomicPtr, AtomicU16,
10+
AtomicU32, AtomicU64, AtomicU8, AtomicUsize, Ordering,
11+
};
12+
13+
#[cfg(not(feature = "portable-atomic"))]
14+
use core::sync::atomic;
15+
16+
#[cfg(feature = "portable-atomic")]
17+
use portable_atomic as atomic;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//! Provides `Barrier` and `BarrierWaitResult`
2+
3+
pub use barrier::{Barrier, BarrierWaitResult};
4+
5+
#[cfg(feature = "std")]
6+
use std::sync as barrier;
7+
8+
#[cfg(not(feature = "std"))]
9+
mod barrier {
10+
use core::fmt;
11+
12+
/// Fallback implementation of `Barrier` from the standard library.
13+
pub struct Barrier {
14+
inner: spin::Barrier,
15+
}
16+
17+
impl Barrier {
18+
/// Creates a new barrier that can block a given number of threads.
19+
///
20+
/// See the standard library for further details.
21+
#[must_use]
22+
pub const fn new(n: usize) -> Self {
23+
Self {
24+
inner: spin::Barrier::new(n),
25+
}
26+
}
27+
28+
/// Blocks the current thread until all threads have rendezvoused here.
29+
///
30+
/// See the standard library for further details.
31+
pub fn wait(&self) -> BarrierWaitResult {
32+
BarrierWaitResult {
33+
inner: self.inner.wait(),
34+
}
35+
}
36+
}
37+
38+
impl fmt::Debug for Barrier {
39+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40+
f.debug_struct("Barrier").finish_non_exhaustive()
41+
}
42+
}
43+
44+
/// Fallback implementation of `BarrierWaitResult` from the standard library.
45+
pub struct BarrierWaitResult {
46+
inner: spin::barrier::BarrierWaitResult,
47+
}
48+
49+
impl BarrierWaitResult {
50+
/// Returns `true` if this thread is the "leader thread" for the call to [`Barrier::wait()`].
51+
///
52+
/// See the standard library for further details.
53+
#[must_use]
54+
pub fn is_leader(&self) -> bool {
55+
self.inner.is_leader()
56+
}
57+
}
58+
59+
impl fmt::Debug for BarrierWaitResult {
60+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61+
f.debug_struct("BarrierWaitResult")
62+
.field("is_leader", &self.is_leader())
63+
.finish()
64+
}
65+
}
66+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//! Provides `LazyLock`
2+
3+
pub use lazy_lock::LazyLock;
4+
5+
#[cfg(feature = "std")]
6+
use std::sync as lazy_lock;
7+
8+
#[cfg(not(feature = "std"))]
9+
mod lazy_lock {
10+
pub use spin::Lazy as LazyLock;
11+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//! Provides various synchronization alternatives to language primitives.
2+
//!
3+
//! Currently missing from this module are the following items:
4+
//! * `Condvar`
5+
//! * `WaitTimeoutResult`
6+
//! * `mpsc`
7+
//!
8+
//! Otherwise, this is a drop-in replacement for `std::sync`.
9+
10+
pub use barrier::{Barrier, BarrierWaitResult};
11+
pub use lazy_lock::LazyLock;
12+
pub use mutex::{Mutex, MutexGuard};
13+
pub use once::{Once, OnceLock, OnceState};
14+
pub use poison::{LockResult, PoisonError, TryLockError, TryLockResult};
15+
pub use rwlock::{RwLock, RwLockReadGuard, RwLockWriteGuard};
16+
17+
#[cfg(feature = "alloc")]
18+
pub use arc::{Arc, Weak};
19+
20+
pub mod atomic;
21+
22+
mod barrier;
23+
mod lazy_lock;
24+
mod mutex;
25+
mod once;
26+
mod poison;
27+
mod rwlock;
28+
29+
#[cfg(all(feature = "alloc", feature = "portable-atomic"))]
30+
use portable_atomic_util as arc;
31+
32+
#[cfg(all(feature = "alloc", not(feature = "portable-atomic")))]
33+
use alloc::sync as arc;
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//! Provides `Mutex` and `MutexGuard`
2+
3+
pub use mutex::{Mutex, MutexGuard};
4+
5+
#[cfg(feature = "std")]
6+
use std::sync as mutex;
7+
8+
#[cfg(not(feature = "std"))]
9+
mod mutex {
10+
use crate::sync::{LockResult, TryLockError, TryLockResult};
11+
use core::fmt;
12+
13+
pub use spin::MutexGuard;
14+
15+
/// Fallback implementation of `Mutex` from the standard library.
16+
pub struct Mutex<T: ?Sized> {
17+
inner: spin::Mutex<T>,
18+
}
19+
20+
impl<T> Mutex<T> {
21+
/// Creates a new mutex in an unlocked state ready for use.
22+
///
23+
/// See the standard library for further details.
24+
pub const fn new(t: T) -> Self {
25+
Self {
26+
inner: spin::Mutex::new(t),
27+
}
28+
}
29+
}
30+
31+
impl<T: ?Sized> Mutex<T> {
32+
/// Acquires a mutex, blocking the current thread until it is able to do so.
33+
///
34+
/// See the standard library for further details.
35+
pub fn lock(&self) -> LockResult<MutexGuard<'_, T>> {
36+
Ok(self.inner.lock())
37+
}
38+
39+
/// Attempts to acquire this lock.
40+
///
41+
/// See the standard library for further details.
42+
pub fn try_lock(&self) -> TryLockResult<MutexGuard<'_, T>> {
43+
self.inner.try_lock().ok_or(TryLockError::WouldBlock)
44+
}
45+
46+
/// Determines whether the mutex is poisoned.
47+
///
48+
/// See the standard library for further details.
49+
pub fn is_poisoned(&self) -> bool {
50+
false
51+
}
52+
53+
/// Clear the poisoned state from a mutex.
54+
///
55+
/// See the standard library for further details.
56+
pub fn clear_poison(&self) {
57+
// no-op
58+
}
59+
60+
/// Consumes this mutex, returning the underlying data.
61+
///
62+
/// See the standard library for further details.
63+
pub fn into_inner(self) -> LockResult<T>
64+
where
65+
T: Sized,
66+
{
67+
Ok(self.inner.into_inner())
68+
}
69+
70+
/// Returns a mutable reference to the underlying data.
71+
///
72+
/// See the standard library for further details.
73+
pub fn get_mut(&mut self) -> LockResult<&mut T> {
74+
Ok(self.inner.get_mut())
75+
}
76+
}
77+
78+
impl<T> From<T> for Mutex<T> {
79+
fn from(t: T) -> Self {
80+
Mutex::new(t)
81+
}
82+
}
83+
84+
impl<T: ?Sized + Default> Default for Mutex<T> {
85+
fn default() -> Mutex<T> {
86+
Mutex::new(Default::default())
87+
}
88+
}
89+
90+
impl<T: ?Sized + fmt::Debug> fmt::Debug for Mutex<T> {
91+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92+
let mut d = f.debug_struct("Mutex");
93+
match self.try_lock() {
94+
Ok(guard) => {
95+
d.field("data", &&*guard);
96+
}
97+
Err(TryLockError::Poisoned(err)) => {
98+
d.field("data", &&**err.get_ref());
99+
}
100+
Err(TryLockError::WouldBlock) => {
101+
d.field("data", &format_args!("<locked>"));
102+
}
103+
}
104+
d.field("poisoned", &false);
105+
d.finish_non_exhaustive()
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)