Automatically enable portable-atomic when required (#17570)

# Objective

- Contributes to #15460
- Reduce quantity and complexity of feature gates across Bevy

## Solution

- Used `target_has_atomic` configuration variable to automatically
detect impartial atomic support and automatically switch to
`portable-atomic` over the standard library on an as-required basis.

## Testing

- CI

## Notes

To explain the technique employed here, consider getting `Arc` either
from `alloc::sync` _or_ `portable-atomic-util`. First, we can inspect
the `alloc` crate to see that you only have access to `Arc` _if_
`target_has_atomic = "ptr"`. We add a target dependency for this
particular configuration _inverted_:

```toml
[target.'cfg(not(target_has_atomic = "ptr"))'.dependencies]
portable-atomic-util = { version = "0.2.4", default-features = false }
```

This ensures we only have the dependency when it is needed, and it is
entirely excluded from the dependency graph when it is not. Next, we
adjust our configuration flags to instead of checking for `feature =
"portable-atomic"` to instead check for `target_has_atomic = "ptr"`:

```rust
// `alloc` feature flag hidden for brevity

#[cfg(not(target_has_atomic = "ptr"))]
use portable_atomic_util as arc;

#[cfg(target_has_atomic = "ptr")]
use alloc::sync as arc;

pub use arc::{Arc, Weak};
```

The benefits of this technique are three-fold:

1. For platforms without full atomic support, the functionality is
enabled automatically.
2. For platforms with atomic support, the dependency is never included,
even if a feature was enabled using `--all-features` (for example)
3. The `portable-atomic` feature no longer needs to virally spread to
all user-facing crates, it's instead something handled within
`bevy_platform_support` (with some extras where other dependencies also
need their features enabled).
This commit is contained in:
Zachary Harrold 2025-02-25 07:52:46 +11:00 committed by GitHub
parent 7f14581495
commit 76e9bf9c99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 94 additions and 152 deletions

View File

@ -50,15 +50,6 @@ critical-section = [
"bevy_input_focus/critical-section",
]
## `portable-atomic` provides additional platform support for atomic types and
## operations, even on targets without native support.
portable-atomic = [
"bevy_app/portable-atomic",
"bevy_ecs/portable-atomic",
"bevy_reflect?/portable-atomic",
"bevy_input_focus/portable-atomic",
]
## Uses the `libm` maths library instead of the one provided in `std` and `core`.
libm = ["bevy_input_focus/libm"]

View File

@ -60,15 +60,6 @@ critical-section = [
"bevy_reflect?/critical-section",
]
## `portable-atomic` provides additional platform support for atomic types and
## operations, even on targets without native support.
portable-atomic = [
"bevy_tasks?/portable-atomic",
"bevy_ecs/portable-atomic",
"bevy_platform_support/portable-atomic",
"bevy_reflect?/portable-atomic",
]
[dependencies]
# bevy
bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" }

View File

@ -1,11 +1,3 @@
#![cfg_attr(
feature = "portable-atomic",
expect(
clippy::redundant_closure,
reason = "bevy_platform_support::sync::Arc has subtly different implicit behavior"
)
)]
use crate::{App, Plugin};
use alloc::string::ToString;

View File

@ -54,17 +54,6 @@ critical-section = [
"bevy_tasks?/critical-section",
]
## `portable-atomic` provides additional platform support for atomic types and
## operations, even on targets without native support.
portable-atomic = [
"bevy_ecs/portable-atomic",
"bevy_app/portable-atomic",
"bevy_platform_support/portable-atomic",
"bevy_time/portable-atomic",
"bevy_utils/portable-atomic",
"bevy_tasks?/portable-atomic",
]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false }

View File

@ -92,15 +92,6 @@ critical-section = [
"bevy_reflect?/critical-section",
]
## `portable-atomic` provides additional platform support for atomic types and
## operations, even on targets without native support.
portable-atomic = [
"bevy_tasks?/portable-atomic",
"bevy_platform_support/portable-atomic",
"concurrent-queue/portable-atomic",
"bevy_reflect?/portable-atomic",
]
[dependencies]
bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [
@ -139,6 +130,11 @@ tracing = { version = "0.1", default-features = false, optional = true }
log = { version = "0.4", default-features = false }
bumpalo = "3"
[target.'cfg(not(all(target_has_atomic = "8", target_has_atomic = "16", target_has_atomic = "32", target_has_atomic = "64", target_has_atomic = "ptr")))'.dependencies]
concurrent-queue = { version = "2.5.0", default-features = false, features = [
"portable-atomic",
] }
[dev-dependencies]
rand = "0.8"
static_assertions = "1.1.0"

View File

@ -2060,7 +2060,7 @@ impl RequiredComponents {
//
// This would be resolved by https://github.com/rust-lang/rust/issues/123430
#[cfg(feature = "portable-atomic")]
#[cfg(not(target_has_atomic = "ptr"))]
use alloc::boxed::Box;
type Constructor = dyn for<'a, 'b> Fn(
@ -2072,10 +2072,10 @@ impl RequiredComponents {
MaybeLocation,
);
#[cfg(feature = "portable-atomic")]
#[cfg(not(target_has_atomic = "ptr"))]
type Intermediate<T> = Box<T>;
#[cfg(not(feature = "portable-atomic"))]
#[cfg(target_has_atomic = "ptr")]
type Intermediate<T> = Arc<T>;
let boxed: Intermediate<Constructor> = Intermediate::new(

View File

@ -56,15 +56,6 @@ critical-section = [
"bevy_platform_support/critical-section",
]
## `portable-atomic` provides additional platform support for atomic types and
## operations, even on targets without native support.
portable-atomic = [
"bevy_app/portable-atomic",
"bevy_ecs/portable-atomic",
"bevy_reflect?/portable-atomic",
"bevy_platform_support/portable-atomic",
]
## Uses the `libm` maths library instead of the one provided in `std` and `core`.
libm = ["bevy_math/libm"]

View File

@ -55,15 +55,6 @@ critical-section = [
"bevy_input/critical-section",
]
## `portable-atomic` provides additional platform support for atomic types and
## operations, even on targets without native support.
portable-atomic = [
"bevy_app/portable-atomic",
"bevy_ecs/portable-atomic",
"bevy_reflect?/portable-atomic",
"bevy_input/portable-atomic",
]
## Uses the `libm` maths library instead of the one provided in `std` and `core`.
libm = ["bevy_math/libm", "bevy_window/libm"]

View File

@ -24,32 +24,20 @@ serialize = ["hashbrown/serde"]
std = [
"alloc",
"critical-section?/std",
"portable-atomic?/std",
"portable-atomic-util?/std",
"portable-atomic/std",
"portable-atomic-util/std",
"spin/std",
"foldhash/std",
]
alloc = ["portable-atomic-util?/alloc", "dep:hashbrown"]
alloc = ["portable-atomic-util/alloc", "dep:hashbrown"]
## `critical-section` provides the building blocks for synchronization primitives
## on all platforms, including `no_std`.
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",
"spin/portable_atomic",
]
critical-section = ["dep:critical-section", "portable-atomic/critical-section"]
[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",
@ -68,6 +56,17 @@ hashbrown = { version = "0.15.1", features = [
web-time = { version = "1.1", default-features = false }
getrandom = { version = "0.2.0", default-features = false, features = ["js"] }
[target.'cfg(not(all(target_has_atomic = "8", target_has_atomic = "16", target_has_atomic = "32", target_has_atomic = "64", target_has_atomic = "ptr")))'.dependencies]
portable-atomic = { version = "1", default-features = false, features = [
"fallback",
] }
spin = { version = "0.9.8", default-features = false, features = [
"portable_atomic",
] }
[target.'cfg(not(target_has_atomic = "ptr"))'.dependencies]
portable-atomic-util = { version = "0.2.4", default-features = false }
[lints]
workspace = true

View File

@ -5,13 +5,39 @@
//! 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,
};
pub use atomic_16::{AtomicI16, AtomicU16};
pub use atomic_32::{AtomicI32, AtomicU32};
pub use atomic_64::{AtomicI64, AtomicU64};
pub use atomic_8::{AtomicBool, AtomicI8, AtomicU8};
pub use atomic_ptr::{AtomicIsize, AtomicPtr, AtomicUsize};
pub use core::sync::atomic::Ordering;
#[cfg(not(feature = "portable-atomic"))]
use core::sync::atomic;
#[cfg(target_has_atomic = "8")]
use core::sync::atomic as atomic_8;
#[cfg(feature = "portable-atomic")]
use portable_atomic as atomic;
#[cfg(not(target_has_atomic = "8"))]
use portable_atomic as atomic_8;
#[cfg(target_has_atomic = "16")]
use core::sync::atomic as atomic_16;
#[cfg(not(target_has_atomic = "16"))]
use portable_atomic as atomic_16;
#[cfg(target_has_atomic = "32")]
use core::sync::atomic as atomic_32;
#[cfg(not(target_has_atomic = "32"))]
use portable_atomic as atomic_32;
#[cfg(target_has_atomic = "64")]
use core::sync::atomic as atomic_64;
#[cfg(not(target_has_atomic = "64"))]
use portable_atomic as atomic_64;
#[cfg(target_has_atomic = "ptr")]
use core::sync::atomic as atomic_ptr;
#[cfg(not(target_has_atomic = "ptr"))]
use portable_atomic as atomic_ptr;

View File

@ -26,8 +26,8 @@ mod once;
mod poison;
mod rwlock;
#[cfg(all(feature = "alloc", feature = "portable-atomic"))]
#[cfg(all(feature = "alloc", not(target_has_atomic = "ptr")))]
use portable_atomic_util as arc;
#[cfg(all(feature = "alloc", not(feature = "portable-atomic")))]
#[cfg(all(feature = "alloc", target_has_atomic = "ptr"))]
use alloc::sync as arc;

View File

@ -71,13 +71,6 @@ critical-section = [
"bevy_utils/critical-section",
]
## `portable-atomic` provides additional platform support for atomic types and
## operations, even on targets without native support.
portable-atomic = [
"bevy_platform_support/portable-atomic",
"bevy_utils/portable-atomic",
]
[dependencies]
# bevy
bevy_reflect_derive = { path = "derive", version = "0.16.0-dev" }

View File

@ -94,7 +94,7 @@ impl<'env> DynamicFunction<'env> {
) -> Self {
let arc = Arc::new(func);
#[cfg(feature = "portable-atomic")]
#[cfg(not(target_has_atomic = "ptr"))]
#[expect(
unsafe_code,
reason = "unsized coercion is an unstable feature for non-std types"

View File

@ -183,7 +183,7 @@ impl ConstParamInfo {
pub fn with_default<T: Reflect + 'static>(mut self, default: T) -> Self {
let arc = Arc::new(default);
#[cfg(feature = "portable-atomic")]
#[cfg(not(target_has_atomic = "ptr"))]
#[expect(
unsafe_code,
reason = "unsized coercion is an unstable feature for non-std types"

View File

@ -217,11 +217,6 @@ impl_reflect_opaque!(::core::num::Wrapping<T: Clone + Send + Sync>());
impl_reflect_opaque!(::core::num::Saturating<T: Clone + Send + Sync>());
impl_reflect_opaque!(::bevy_platform_support::sync::Arc<T: Send + Sync + ?Sized>);
// We check despite `portable-atomic` being enabled, if the standard library `Arc` is
// also available, and implement Reflect for it.
#[cfg(all(feature = "portable-atomic", target_has_atomic = "ptr"))]
impl_reflect_opaque!(::alloc::sync::Arc<T: Send + Sync + ?Sized>);
// `Serialize` and `Deserialize` only for platforms supported by serde:
// https://github.com/serde-rs/serde/blob/3ffb86fc70efd3d329519e2dddfa306cc04f167c/serde/src/de/impls.rs#L1732
#[cfg(all(any(unix, windows), feature = "std"))]

View File

@ -340,7 +340,7 @@ mod tests {
fn create_arc_dyn_enemy<T: Enemy>(enemy: T) -> Arc<dyn Enemy> {
let arc = Arc::new(enemy);
#[cfg(feature = "portable-atomic")]
#[cfg(not(target_has_atomic = "ptr"))]
#[expect(
unsafe_code,
reason = "unsized coercion is an unstable feature for non-std types"

View File

@ -46,16 +46,6 @@ critical-section = [
"bevy_platform_support/critical-section",
]
## `portable-atomic` provides additional platform support for atomic types and
## operations, even on targets without native support.
portable-atomic = [
"bevy_ecs/portable-atomic",
"bevy_utils/portable-atomic",
"bevy_app?/portable-atomic",
"bevy_reflect?/portable-atomic",
"bevy_platform_support/portable-atomic",
]
[dependencies]
# bevy
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false }

View File

@ -23,11 +23,6 @@ critical-section = [
"bevy_platform_support/critical-section",
"edge-executor?/critical-section",
]
portable-atomic = [
"bevy_platform_support/portable-atomic",
"edge-executor?/portable-atomic",
"async-task/portable-atomic",
]
[dependencies]
bevy_platform_support = { path = "../bevy_platform_support", version = "0.16.0-dev", default-features = false, features = [
@ -54,6 +49,14 @@ wasm-bindgen-futures = "0.4"
pin-project = "1"
futures-channel = "0.3"
[target.'cfg(not(all(target_has_atomic = "8", target_has_atomic = "16", target_has_atomic = "32", target_has_atomic = "64", target_has_atomic = "ptr")))'.dependencies]
async-task = { version = "4.4.0", default-features = false, features = [
"portable-atomic",
] }
edge-executor = { version = "0.4.1", default-features = false, optional = true, features = [
"portable-atomic",
] }
[dev-dependencies]
web-time = { version = "1.1" }

View File

@ -74,16 +74,20 @@ impl TaskPoolBuilder {
/// This is called on the thread itself and has access to all thread-local storage.
/// This will block running async tasks on the thread until the callback completes.
pub fn on_thread_spawn(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
#[cfg(feature = "portable-atomic")]
let arc = {
let boxed = Box::new(f);
let boxed: Box<dyn Fn() + Send + Sync + 'static> = boxed;
Arc::from(boxed)
};
#[cfg(not(feature = "portable-atomic"))]
let arc = Arc::new(f);
#[cfg(not(target_has_atomic = "ptr"))]
#[expect(
unsafe_code,
reason = "unsized coercion is an unstable feature for non-std types"
)]
// SAFETY:
// - Coercion from `impl Fn` to `dyn Fn` is valid
// - `Arc::from_raw` receives a valid pointer from a previous call to `Arc::into_raw`
let arc = unsafe {
Arc::from_raw(Arc::into_raw(arc) as *const (dyn Fn() + Send + Sync + 'static))
};
self.on_thread_spawn = Some(arc);
self
}
@ -93,16 +97,20 @@ impl TaskPoolBuilder {
/// This is called on the thread itself and has access to all thread-local storage.
/// This will block thread termination until the callback completes.
pub fn on_thread_destroy(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
#[cfg(feature = "portable-atomic")]
let arc = {
let boxed = Box::new(f);
let boxed: Box<dyn Fn() + Send + Sync + 'static> = boxed;
Arc::from(boxed)
};
#[cfg(not(feature = "portable-atomic"))]
let arc = Arc::new(f);
#[cfg(not(target_has_atomic = "ptr"))]
#[expect(
unsafe_code,
reason = "unsized coercion is an unstable feature for non-std types"
)]
// SAFETY:
// - Coercion from `impl Fn` to `dyn Fn` is valid
// - `Arc::from_raw` receives a valid pointer from a previous call to `Arc::into_raw`
let arc = unsafe {
Arc::from_raw(Arc::into_raw(arc) as *const (dyn Fn() + Send + Sync + 'static))
};
self.on_thread_destroy = Some(arc);
self
}

View File

@ -50,15 +50,6 @@ critical-section = [
"bevy_app/critical-section",
]
## `portable-atomic` provides additional platform support for atomic types and
## operations, even on targets without native support.
portable-atomic = [
"bevy_ecs/portable-atomic",
"bevy_platform_support/portable-atomic",
"bevy_reflect?/portable-atomic",
"bevy_app/portable-atomic",
]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false }

View File

@ -30,10 +30,6 @@ alloc = ["bevy_platform_support/alloc"]
## on all platforms, including `no_std`.
critical-section = ["bevy_platform_support/critical-section"]
## `portable-atomic` provides additional platform support for atomic types and
## operations, even on targets without native support.
portable-atomic = ["bevy_platform_support/portable-atomic"]
[dependencies]
bevy_platform_support = { path = "../bevy_platform_support", version = "0.16.0-dev", default-features = false }