Skip to content

Create bevy_platform::cfg for viral feature management #18822

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion crates/bevy_platform/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ critical-section = ["dep:critical-section", "portable-atomic/critical-section"]
web = ["dep:web-time", "dep:getrandom"]

[dependencies]
cfg-if = "1.0.0"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe bevy_platform::cfg::switch is superior to cfg-if so no reason to keep it around IMO.

critical-section = { version = "1.2.0", default-features = false, optional = true }
spin = { version = "0.9.8", default-features = false, features = [
"mutex",
Expand Down
264 changes: 264 additions & 0 deletions crates/bevy_platform/src/cfg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
//! Provides helpful configuration macros, allowing detection of platform features such as
//! [`alloc`](crate::cfg::alloc) or [`std`](crate::cfg::std) without explicit features.

/// Provides a `match`-like expression similar to [`cfg_if`] and based on the experimental
/// [`cfg_match`].
/// The name `switch` is used to avoid conflict with the `match` keyword.
/// Arms are evaluated top to bottom, and an optional wildcard arm can be provided if no match
/// can be made.
///
/// An arm can either be:
/// - a `cfg(...)` pattern (e.g., `feature = "foo"`)
/// - a wildcard `_`
/// - an alias defined using [`define_alias`]
///
/// Common aliases are provided by [`cfg`](crate::cfg).
/// Note that aliases are evaluated from the context of the defining crate, not the consumer.
///
/// # Examples
///
/// ```
/// # use bevy_platform::cfg;
/// # fn log(_: &str) {}
/// # fn foo(_: &str) {}
/// #
/// cfg::switch! {
/// #[cfg(feature = "foo")] => {
/// foo("We have the `foo` feature!")
/// }
/// cfg::std => {
/// extern crate std;
/// std::println!("No `foo`, but we have `std`!");
/// }
/// _ => {
/// log("Don't have `std` or `foo`");
/// }
/// }
/// ```
///
/// [`cfg_if`]: https://crates.io/crates/cfg-if
/// [`cfg_match`]: https://github.com/rust-lang/rust/issues/115585
#[doc(inline)]
pub use crate::switch;

/// Defines an alias for a particular configuration.
/// This has two advantages over directly using `#[cfg(...)]`:
///
/// 1. Complex configurations can be abbreviated to more meaningful shorthand.
/// 2. Features are evaluated in the context of the _defining_ crate, not the consuming.
///
/// The second advantage is a particularly powerful tool, as it allows consuming crates to use
/// functionality in a defining crate regardless of what crate in the dependency graph enabled the
/// relevant feature.
///
/// For example, consider a crate `foo` that depends on another crate `bar`.
/// `bar` has a feature "`faster_algorithms`".
/// If `bar` defines a "`faster_algorithms`" alias:
///
/// ```ignore
/// define_alias! {
/// #[cfg(feature = "faster_algorithms")] => { faster_algorithms }
/// }
/// ```
///
/// Now, `foo` can gate its usage of those faster algorithms on the alias, avoiding the need to
/// expose its own "`faster_algorithms`" feature.
/// This also avoids the unfortunate situation where one crate activates "`faster_algorithms`" on
/// `bar` without activating that same feature on `foo`.
///
/// Once an alias is defined, there are 4 ways you can use it:
///
/// 1. Evaluate with no contents to return a `bool` indicating if the alias is active.
/// ```
/// # use bevy_platform::cfg;
/// if cfg::std!() {
/// // Have `std`!
/// } else {
/// // No `std`...
/// }
/// ```
/// 2. Pass a single code block which will only be compiled if the alias is active.
/// ```
/// # use bevy_platform::cfg;
/// cfg::std! {
/// // Have `std`!
/// # ()
/// }
/// ```
/// 3. Pass a single `if { ... } else { ... }` expression to conditionally compile either the first
/// or the second code block.
/// ```
/// # use bevy_platform::cfg;
/// cfg::std! {
/// if {
/// // Have `std`!
/// } else {
/// // No `std`...
/// }
/// }
/// ```
/// 4. Use in a [`switch`] arm for more complex conditional compilation.
/// ```
/// # use bevy_platform::cfg;
/// cfg::switch! {
/// cfg::std => {
/// // Have `std`!
/// }
/// cfg::alloc => {
/// // No `std`, but do have `alloc`!
/// }
/// _ => {
/// // No `std` or `alloc`...
/// }
/// }
/// ```
#[doc(inline)]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using doc(inline) on this pub use statement, but doc(hidden) on the declaration gives us full documentation on docs.rs, and keeps the macro contained to this module, rather than immediately going to the crate root. This lets us use shorter named like switch and std, since they're already scoped to the cfg namespace, avoiding the cfg_if::cfg_if ugliness.

pub use crate::define_alias;

/// Macro which represents an enabled compilation condition.
#[doc(inline)]
pub use crate::enabled;

/// Macro which represents a disabled compilation condition.
#[doc(inline)]
pub use crate::disabled;

#[doc(hidden)]
#[macro_export]
macro_rules! switch {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is heavily based on cfg_match from the nightly standard library. It includes an extra case to integration with macros created with define_alias though.

({ $($tt:tt)* }) => {{
$crate::switch! { $($tt)* }
}};
(_ => { $($output:tt)* }) => {
$($output)*
};
(
$cond:path => $output:tt
$($( $rest:tt )+)?
) => {
$cond! {
if {
$crate::switch! { _ => $output }
} else {
$(
$crate::switch! { $($rest)+ }
)?
}
}
};
(
#[cfg($cfg:meta)] => $output:tt
$($( $rest:tt )+)?
) => {
#[cfg($cfg)]
$crate::switch! { _ => $output }
$(
#[cfg(not($cfg))]
$crate::switch! { $($rest)+ }
)?
};
}

#[doc(hidden)]
#[macro_export]
macro_rules! disabled {
() => { false };
(if { $($p:tt)* } else { $($n:tt)* }) => { $($n)* };
($($p:tt)*) => {};
}

#[doc(hidden)]
#[macro_export]
macro_rules! enabled {
() => { true };
(if { $($p:tt)* } else { $($n:tt)* }) => { $($p)* };
($($p:tt)*) => { $($p)* };
}

#[doc(hidden)]
#[macro_export]
macro_rules! define_alias {
(
#[cfg($meta:meta)] => $p:ident
$(, $( $rest:tt )+)?
) => {
$crate::define_alias! {
#[cfg($meta)] => { $p }
$(
$($rest)+
)?
}
};
(
#[cfg($meta:meta)] => $p:ident,
$($( $rest:tt )+)?
) => {
$crate::define_alias! {
#[cfg($meta)] => { $p }
$(
$($rest)+
)?
}
};
(
#[cfg($meta:meta)] => {
$(#[$p_meta:meta])*
$p:ident
}
$($( $rest:tt )+)?
) => {
$crate::switch! {
#[cfg($meta)] => {
$(#[$p_meta])*
#[doc(inline)]
///
#[doc = concat!("This macro passes the provided code because `#[cfg(", stringify!($meta), ")]` is currently active.")]
pub use $crate::enabled as $p;
}
_ => {
$(#[$p_meta])*
#[doc(inline)]
///
#[doc = concat!("This macro suppresses the provided code because `#[cfg(", stringify!($meta), ")]` is _not_ currently active.")]
pub use $crate::disabled as $p;
}
}

$(
$crate::define_alias! {
$($rest)+
}
)?
}
}

define_alias! {
#[cfg(feature = "alloc")] => {
/// Indicates the `alloc` crate is available and can be used.
alloc
}
#[cfg(feature = "std")] => {
/// Indicates the `std` crate is available and can be used.
std
}
#[cfg(panic = "unwind")] => {
/// Indicates that a [`panic`] will be unwound, and can be potentially caught.
panic_unwind
}
#[cfg(panic = "abort")] => {
/// Indicates that a [`panic`] will lead to an abort, and cannot be caught.
panic_abort
}
#[cfg(all(target_arch = "wasm32", feature = "web"))] => {
/// Indicates that this target has access to browser APIs.
web
}
#[cfg(all(feature = "alloc", target_has_atomic = "ptr"))] => {
/// Indicates that this target has access to a native implementation of `Arc`.
arc
}
#[cfg(feature = "critical-section")] => {
/// Indicates `critical-section` is available.
critical_section
}
}
25 changes: 14 additions & 11 deletions crates/bevy_platform/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,22 @@
//!
//! [Bevy]: https://bevyengine.org/

#[cfg(feature = "std")]
extern crate std;
cfg::std! {
extern crate std;
}

cfg::alloc! {
extern crate alloc;

#[cfg(feature = "alloc")]
extern crate alloc;
pub mod collections;
}

pub mod cfg;
pub mod hash;
pub mod sync;
pub mod thread;
pub mod time;

#[cfg(feature = "alloc")]
pub mod collections;

/// 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
Expand All @@ -33,10 +35,11 @@ pub mod collections;
/// 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,
};
crate::cfg::alloc! {
pub use alloc::{
borrow::ToOwned, boxed::Box, format, string::String, string::ToString, vec, vec::Vec,
};
}
Comment on lines -36 to +42
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The one downside of these macros is it's common to go one indentation level higher. Not impossible to work with, but annoying.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally don't mind this, as it's easier to tell if a large function is feature-gated without having to scroll all the way up! :)


// Items from `std::prelude` that are missing in this module:
// * dbg
Expand Down
19 changes: 11 additions & 8 deletions crates/bevy_platform/src/sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,17 @@ 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};
crate::cfg::alloc! {
pub use arc::{Arc, Weak};

crate::cfg::arc! {
if {
use alloc::sync as arc;
} else {
use portable_atomic_util as arc;
}
}
}

pub mod atomic;

Expand All @@ -25,9 +34,3 @@ mod mutex;
mod once;
mod poison;
mod rwlock;

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

#[cfg(all(feature = "alloc", target_has_atomic = "ptr"))]
use alloc::sync as arc;
8 changes: 5 additions & 3 deletions crates/bevy_platform/src/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

pub use thread::sleep;

cfg_if::cfg_if! {
crate::cfg::switch! {
// TODO: use browser timeouts based on ScheduleRunnerPlugin::build
if #[cfg(feature = "std")] {
// crate::cfg::web => { ... }
crate::cfg::std => {
use std::thread;
} else {
}
_ => {
mod fallback {
use core::{hint::spin_loop, time::Duration};

Expand Down
Loading