Skip to content
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

Encapsulate cfg(feature = "track_location") in a type. #17602

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

chescock
Copy link
Contributor

@chescock chescock commented Jan 29, 2025

Objective

Eliminate the need to write cfg(feature = "track_location") every time one uses an API that may use location tracking. It's verbose, and a little intimidating. And it requires code outside of bevy_ecs that wants to use location tracking needs to either unconditionally enable the feature, or include conditional compilation of its own. It would be good for users to be able to log locations when they are available without needing to add feature flags to their own crates.

Reduce the number of cases where code compiles with the track_location feature enabled, but not with it disabled, or vice versa. It can be hard to remember to test it both ways!

Remove the need to store a None in HookContext when the track_location feature is disabled.

Solution

Create an MaybeLocation<T> type that contains a T if the track_location feature is enabled, and is a ZST if it is not. The overall API is similar to Option, but whether the value is Some or None is set at compile time and is the same for all values.

Default T to &'static Location<'static>, since that is the most common case.

Remove all cfg(feature = "track_location") blocks outside of the implementation of that type, and instead call methods on it.

When track_location is disabled, MaybeLocation is a ZST and all methods are #[inline] and empty, so they should be entirely removed by the compiler. But the code will still be visible to the compiler and checked, so if it compiles with the feature disabled then it should also compile with it enabled, and vice versa.

Open Questions

Where should these types live? I put them in change_detection because that's where the existing MaybeLocation types were, but we now use these outside of change detection.

While I believe that the compiler should be able to remove all of these calls, I have not actually tested anything. If we want to take this approach, what testing is required to ensure it doesn't impact performance?

Migration Guide

Methods like Ref::changed_by() that return a &'static Location<'static> will now be available even when the track_location feature is disabled, but they will return a new MaybeLocation type. MaybeLocation wraps a &'static Location<'static> when the feature is enabled, and is a ZST when the feature is disabled.

Existing code that needs a &Location can call into_option().unwrap() to recover it. Many trait impls are forwarded, so if you only need Display then no changes will be necessary.

If that code was conditionally compiled, you may instead want to use the methods on MaybeLocation to remove the need for conditional compilation.

Code that constructs a Ref, Mut, Res, or ResMut will now need to provide location information unconditionally. If you are creating them from existing Bevy types, you can obtain a MaybeLocation from methods like Table::get_changed_by_slice_for() or ComponentSparseSet::get_with_ticks. Otherwise, you will need to store a MaybeLocation next to your data and use methods like as_ref() or as_mut() to obtain wrapped references.

This allows code that depends on the feature to be checked by the compiler even when the feature is disabled,
while still being removed during compilation.
@chescock chescock added A-ECS Entities, components, systems, and events C-Code-Quality A section of code that is hard to understand or change M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide X-Controversial There is active debate or serious implications around merging this PR S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 29, 2025
@SpecificProtagonist
Copy link
Contributor

SpecificProtagonist commented Jan 29, 2025

Nice, we do want something like this.

I'll take a proper look later, but from a quick glance I can't see why we need TrackLocationOption<T>:
Shouldn't defining MaybeLocation as struct MaybeLocation { #[cfg(feature = "track_location")] caller: &'static Location<'static>} and using it in all the places where we previously had a cfg'd Location be enough? E.g. &'w mut MaybeLocation instead of TrackLocationOption<&'w mut &'static Location<'static>>.

Remove the need to store a None in HookContext when the track_location feature is disabled.

I was surprised that this didn't happen in the PR that introduced it (didn't get a chance to look at it before it got merged).

@chescock
Copy link
Contributor Author

I'll take a proper look later, but from a quick glance I can't see why we need TrackLocationOption<T>:
Shouldn't defining MaybeLocation as struct MaybeLocation { #[cfg(feature = "track_location")] caller: &'static Location<'static>} and using it in all the places where we previously had a cfg'd Location be enough? E.g. &'w mut MaybeLocation instead of TrackLocationOption<&'w mut &'static Location<'static>>.

That would be simpler! I was trying to make sure that there was never any cost when the feature was disabled, though, and &mut ZST is pointer-sized instead of zero-sized. It would also mean things like set_spawned_or_despawned_by would have to actually do array indexing before assigning the ZST. I'm already a little worried that this PR will get rejected because we can't be certain that it's zero-cost :).

@Seldom-SE
Copy link

This would help avoid this pitfall I've seen a couple times cBournhonesque/lightyear#745 joseph-gio/bevy-trait-query#77

Copy link
Contributor

@eckz eckz left a comment

Choose a reason for hiding this comment

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

Just to avoid repetition in names like TrackLocationOption<Option<T>>, what about renaming TrackLocationOption to

pub struct MaybeLocation<T: ?Sized = &'static Location<'static>> { ... }

and removing the type alias

crates/bevy_ecs/src/change_detection.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/change_detection.rs Outdated Show resolved Hide resolved
Remove `into_inner()` in favor of `into_option().unwrap()`.
@chescock
Copy link
Contributor Author

chescock commented Feb 3, 2025

Just to avoid repetition in names like TrackLocationOption<Option<T>>, what about renaming TrackLocationOption to

pub struct MaybeLocation<T: ?Sized = &'static Location<'static>> { ... }

and removing the type alias

Hmm, that's tempting. I'm concerned that the default type there is a little complex, though; usually default types are simple things like () or Global. And the repetition isn't exactly wrong, as calling into_option() gives you an Option<Option<T>>.

I think I'll leave it alone for now, but if someone else chimes in with a preference to change it to that then I'm happy to do so!

Copy link
Contributor

@JaySpruce JaySpruce left a comment

Choose a reason for hiding this comment

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

LGTM, but I do like eckz's suggestion. More complex than most default types maybe, but I think it'd be fine

Use a defaulted type parameter to make plain `MaybeLocation` work.
@chescock
Copy link
Contributor Author

chescock commented Feb 9, 2025

This would help avoid this pitfall I've seen a couple times cBournhonesque/lightyear#745 joseph-gio/bevy-trait-query#77

Oh, I should add a note about Ref::new to the migration guide! I hadn't thought of that!

I also see that bevy-trait-query is using ? when creating Ref, which would be awkward to do with TrackLocationOption<Option<T>>. I'll add a transpose() function similar to Result::transpose() that will let you convert those to Option<TrackLocation<T>> to use with ?.

@chescock
Copy link
Contributor Author

chescock commented Feb 9, 2025

I do like eckz's suggestion. More complex than most default types maybe, but I think it'd be fine

Alright, two votes in favor sounds good to me! I'll make the change!

Link to `Option`.
Remove extra spaces.
@alice-i-cecile alice-i-cecile 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 Feb 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Code-Quality A section of code that is hard to understand or change M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants