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

Add basic debug checks for read-only UnsafeWorldCell #17393

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

Conversation

JoJoJet
Copy link
Member

@JoJoJet JoJoJet commented Jan 16, 2025

Objective

The method World::as_unsafe_world_cell_readonly is used to create an UnsafeWorldCell which is only allowed to access world data immutably. This can be tricky to use, as the data that an UnsafeWorldCell is allowed to access exists only in documentation (you could think of it as a "doc-time abstraction" rather than a "compile-time" abstraction). It's quite easy to forget where a particular instance came from and attempt to use it for mutable access, leading to instant, silent undefined behavior.

Solution

Add a debug-mode only flag to UnsafeWorldCell which tracks whether or not the instance can be used to access world data mutably. This should catch basic improper usages of as_unsafe_world_cell_readonly.

Future work

There are a few ways that you can bypass the runtime checks introduced by this PR:

  • Any world accesses done via UnsafeWorldCell::storages are completely invisible to these runtime checks. Unfortunately, storages constitutes most of the world accesses used in the engine itself, so this PR will mostly benefit downstream users of bevy.
  • It's possible to call get_resource_by_id, and then convert the returned Ptr to a PtrMut by calling assert_unique. In the future we'll probably want to add a debug-mode only flag to Ptr which tracks whether or not it can be upgraded to a PtrMut. I didn't include this change in this PR as those types are currently defined using macros which makes it a bit tricky to modify their definitions.
  • Any data accesses done through a mutable UnsafeWorldCell are completely unchecked, meaning it's possible to unsoundly create multiple mutable references to a single component, for example. In the future we may want to store an Access<> set inside of the world's Storages to add granular debug-mode runtime checks.

That said, I'd consider this PR to be a good first step towards adding full runtime checks to UnsafeWorldCell.

Testing

Added a few tests that basic invalid mutable world access result in a panic.

@JoJoJet JoJoJet added A-ECS Entities, components, systems, and events C-Testing A change that impacts how we test Bevy or how users test their apps D-Unsafe Touches with unsafe code in some way labels Jan 16, 2025
@alice-i-cecile alice-i-cecile added D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 16, 2025
Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Good compromise. I like this direction.

Copy link
Contributor

@chescock chescock left a comment

Choose a reason for hiding this comment

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

This is a good idea!

I bet we could expand on it in the future by adding checks in SystemState::get_unchecked_manual() and the various QueryState::something_unchecked() methods.

}

#[cfg_attr(debug_assertions, inline(never), track_caller)]
#[cfg_attr(not(debug_assertions), inline(always))]
Copy link
Contributor

Choose a reason for hiding this comment

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

inline(always) and inline(never) are pretty strong. Are you sure we can't just leave them out and trust the optimizer? For the debug version, I'd expect it to inline the self.allows_mutable_access check to avoid the call overhead but leave the panic branch in a cold function.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't have a very strong opinion on what we do here, but I feel like in cases like this where a given inlining strategy is "obviously right" there's not much reason not to include the inline attributes. In debug mode there's pretty much never a reason to inline a panicking function, while in release mode this function is empty, so inline(always) is just saving the compiler from having to use a heuristic here to eliminate the dead code.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yup, it's not a big deal either way! I have just been wrong often enough when I thought something was "obviously right" that I prefer to trust the compiler instead of myself :).

The reason I would want to inline the function in debug mode is to avoid the function call overhead for the if check.
That is, I want it to wind up with

pub unsafe fn world_mut(self) -> &'w mut World {
    if !self.allows_mutable_access {
        call_some_cold_function_to_panic();
    }

and I think that requires inlining assert_allows_mutable_access.

... And I checked in godbolt and it does not do that. It inlines the whole thing, including the panic. So I have no idea which way is better!

Copy link
Member Author

@JoJoJet JoJoJet Jan 17, 2025

Choose a reason for hiding this comment

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

In my view, the release-mode behavior does seem obviously correct here, while the performance in debug mode isn't really worth worrying about unless it majorly impacts dev experience, but that seems unlikely for something this small.

@@ -457,6 +482,7 @@ impl<'w> UnsafeWorldCell<'w> {
/// - no other references to the resource exist at the same time
#[inline]
pub unsafe fn get_resource_mut<R: Resource>(self) -> Option<Mut<'w, R>> {
self.assert_allows_mutable_access();
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't technically need this one (or the one in get_non_send_resource_mut), since it calls get_resource_mut_by_id which already does the check.

Or, if you want to do it consistently this way, you're missing a few like UnsafeEntityCell::get_mut.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah good point, this call is unnecessary

@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 Jan 17, 2025
@JoJoJet
Copy link
Member Author

JoJoJet commented Jan 17, 2025

I bet we could expand on it in the future by adding checks in SystemState::get_unchecked_manual() and the various QueryState::something_unchecked() methods.

In the future I'm hoping to add checks into Storages, which would provide runtime checks to Query and friends for 'free'

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Jan 18, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Jan 18, 2025
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Jan 20, 2025
@alice-i-cecile alice-i-cecile removed this pull request from the merge queue due to a manual request Jan 20, 2025
@alice-i-cecile
Copy link
Member

@JoJoJet it looks like you missed some feature-flagged code on web. Ping me when that's fixed and I'll try merging this in again.

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-Testing A change that impacts how we test Bevy or how users test their apps D-Straightforward Simple bug fixes and API improvements, docs, test and examples D-Unsafe Touches with unsafe code in some way S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants