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

Conversation

bushrat011899
Copy link
Contributor

@bushrat011899 bushrat011899 commented Apr 12, 2025

Objective

Solution

  • Created a new cfg module in bevy_platform which contains two macros to aid in working with features like web, std, and alloc.
    • switch is a stable implementation of cfg_match, which itself is a core alternative to cfg_if.
    • define_alias is a build.rs-free alternative to cfg_aliases with the ability to share feature information between crates.
  • Switched to these macros within bevy_platform to demonstrate usage.

Testing

  • CI

Showcase

Consider the typical std feature as an example of a "virality". With just bevy_platform, bevy_utils, and bevy_ecs, we have 3 crates in a chain where activating std in any of them should really activate it everywhere. The status-quo for this is for each crate to define its own std feature, and ensure it includes the std feature of every dependency in that feature. For crates which don't even interact with std directly, this can be quite cumbersome. Especially considering that Bevy has a fundamental crate, bevy_platform, which is a dependency for effectively every crate.

Instead, we can use define_alias to create a macro which will conditionally compile code if and only if the specified configuration condition is met in the defining crate.

// In `bevy_platform`

define_alias! {
    #[cfg(feature = "std")] => {
        /// Indicates the `std` crate is available and can be used.
        std
    }
    #[cfg(all(target_arch = "wasm32", feature = "web"))] => {
        /// Indicates that this target has access to browser APIs.
        web
    }
}

The above web and std macros will either no-op the provided code if the conditions are not met, or pass it unmodified if it is met. Since it is evaluated in the context of the defining crate, bevy_platform/std can be used to conditionally compile code in bevy_utils and bevy_ecs without those crates including their own std features.

// In `bevy_utils`
use bevy_platform::cfg;

// If `bevy_platform` has `std`, then we can too!
cfg::std! {
    extern crate std;
}

To aid in more complex configurations, switch is provided to provide a cfg_if alternative that is compatible with define_alias:

use bevy_platform::cfg;

cfg::switch! {
    #[cfg(feature = "foo")] => { /* use the foo API */ }
    cfg::web => { /* use browser API */ }
    cfg::std => { /* use std */ }
    _ => { /* use a fallback implementation */ }
}

This paradigm would allow Bevy's sub-crates to avoid re-exporting viral features, and also enable functionality in response to availability in their dependencies, rather than from explicit features (bottom-up instead of top-down). I imagine that a "full rollout" of this paradigm would remove most viral features from Bevy's crates, leaving only bevy_platform, bevy_internal, and bevy (since bevy/_internal are explicitly re-exports of all of Bevy's crates).

This bottom-up approach may be useful in other areas of Bevy's features too. For example, bevy_core_pipeline/tonemapping_luts requires:

  • bevy_render/ktx2
  • bevy_image/ktx2
  • bevy_image/zstd

If define_alias was used in bevy_image, bevy_render would not need to re-export the ktx2 feature, and bevy_core_pipeline could directly probe bevy_image for the status of ktx2 and zstd features to determine if it should compile the tonemapping_luts functionality, rather than having an explicitly feature. Of course, an explicit feature is still important for features, so this may not be the best example, but it highlights that with this paradigm crates can reactively provide functionality, rather than needing to proactively declare feature combinations up-front and hope the user enables them.

@bushrat011899 bushrat011899 added C-Feature A new feature, making something new possible A-Utils Utility functions and types X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Apr 12, 2025
@bushrat011899
Copy link
Contributor Author

Marking as X-Blessed based on Cart's approval on Discord. Please remove if that's not the case!

/// }
/// }
/// ```
#[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.


#[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.

Comment on lines 153 to 167
#[doc(hidden)]
#[macro_export]
macro_rules! noop {
() => { false };
(if { $($p:tt)* } else { $($n:tt)* }) => { $($n)* };
($($p:tt)*) => {};
}

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

Choose a reason for hiding this comment

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

We define this macros and then re-export them inside define_alias because it is not possible to define a macro from within a macro if the inner macro uses repetition in its pattern (since the outer macro sees that as a repetition for itself). The usage of doc(inline) obfuscates this issue and presents documentation on docs.rs as-if the macro is defined every time.

Comment on lines 196 to 225
define_alias! {
feature = "alloc" => {
/// Indicates the `alloc` crate is available and can be used.
alloc
}
feature = "std" => {
/// Indicates the `std` crate is available and can be used.
std
}
panic = "unwind" => {
/// Indicates that a [`panic`] will be unwound, and can be potentially caught.
panic_unwind
}
panic = "abort" => {
/// Indicates that a [`panic`] will lead to an abort, and cannot be caught.
panic_abort
}
all(target_arch = "wasm32", feature = "web") => {
/// Indicates that this target has access to browser APIs.
web
}
all(feature = "alloc", target_has_atomic = "ptr") => {
/// Indicates that this target has access to a native implementation of `Arc`.
arc
}
feature = "critical-section" => {
/// Indicates `critical-section` is available.
critical_section
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are mostly examples right now. How we actually use aliases will depend on the rest of Bevy's crates. For example, I believe Cart was interested in moving some critical platform-dependent dependencies to bevy_platform so that features here could be inherited elsewhere.

@@ -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.

Comment on lines -36 to +42
#[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,
};
}
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! :)

@hukasu
Copy link
Contributor

hukasu commented Apr 12, 2025

This define_alias! is a definitive reason to close #18170, or redo it based on this

Copy link

@heydocode heydocode left a comment

Choose a reason for hiding this comment

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

The more I think about it, the greater I find it, especially in crates/bevy_platform/src/sync/mod.rs: I find the refactor (alloc! followed by nested conditional arc!) really neat and much more comprehensive!

Copy link
Member

@BD103 BD103 left a comment

Choose a reason for hiding this comment

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

I love the ergonomic improvements this brings along! I have some notes, but it's generally only good things :)

Comment on lines -36 to +42
#[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,
};
}
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! :)

Copy link
Member

@cart cart left a comment

Choose a reason for hiding this comment

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

I think this is the right general path forward. I'm not sure we'll be able to use it to completely remove viral features (I suspect removing "std" completely will be a challenge), but my branch doing a similar thing here proves that it is viable for "web" (which is one of the bigger causes of viral nastiness).

The macros also just generally improve maintainability and legibility, which I like.

The big "risk" of using this approach is that bevy_platform will become a dumping ground for types and re-exported crates, creating a bottleneck in our compiles (ex: see the web module on my branch), as dependent crates can no longer conditionally pull in new dependencies based on these features. Every time we move a thing into bevy_platform, we'll need to ask ourselves "is this worth it?".

In the case of "web", I think the answer is probably "yes it is worth it", as the cost to downstream developers is very high. We should measure the full bevy compile time cost after that consolidation though.

@cart cart added this to the 0.17 milestone Apr 13, 2025
Co-Authored-By: BD103 <59022059+BD103@users.noreply.github.com>
@bushrat011899
Copy link
Contributor Author

For other reviewers, I played around with this PR on this branch to see how far we could go with a bottom-up approach to features. There's definitely some design considerations to be had, but with a handful of controversial choices, we can definitely eliminate the std, critical-section, and alloc features entirely. I can also see a much better version of multi_threaded (which actually marks itself as disabled on wasm), and the removal of reflect_functions too. With bevy_ecs doing some tactical re-exports of things like bevy_reflect, I think we could remove that feature from most crates too.

Lots of interesting designs to consider!

@cart
Copy link
Member

cart commented Apr 14, 2025

Did a quick pass over that branch. The clarity / terseness wins there are huge! Very exciting.

@bushrat011899 bushrat011899 added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Apr 26, 2025
@alice-i-cecile
Copy link
Member

For other reviewers, I played around with this PR on this branch to see how far we could go with a bottom-up approach to features. There's definitely some design considerations to be had, but with a handful of controversial choices, we can definitely eliminate the std, critical-section, and alloc features entirely. I can also see a much better version of multi_threaded (which actually marks itself as disabled on wasm), and the removal of reflect_functions too. With bevy_ecs doing some tactical re-exports of things like bevy_reflect, I think we could remove that feature from most crates too.

Exciting! I look forward to further work here!

@alice-i-cecile alice-i-cecile added this pull request to the merge queue May 6, 2025
Merged via the queue into bevyengine:main with commit aadd3a3 May 6, 2025
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Utils Utility functions and types C-Feature A new feature, making something new possible D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Do We Want to Use CFG Aliases?
6 participants