-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Introduce methods on QueryState to obtain a Query #15858
Conversation
…ry to consume Self and return the original lifetime.
I agree that there's a lot of duplication between QueryState's and Query's API and it's good that this is lessened, but just out of curiosity (just to offer a potentially dumb idea), would it perhaps be better if QueryState and Query were just one single struct instead of two? |
I think the idea is that you need access to the world to actually do querying, but that we want to cache some state that lives longer than that borrow. So a I don't see a clean way to get rid of either. If you don't have |
I realized I could split this into two independent PRs: One for the methods on |
I say go big! Go ahead and remove the to-be-deprecated methods on QueryState! 😆 You can split it up if you think it would help, but I don't mind bigger PRs. |
@@ -1542,24 +1542,6 @@ mod tests { | |||
}); | |||
} | |||
|
|||
#[test] | |||
#[should_panic = "Encountered a mismatched World."] | |||
fn query_validates_world_id() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why remove this test? Is it no longer applicable?
#[inline] | ||
pub(crate) unsafe fn new( | ||
world: UnsafeWorldCell<'w>, | ||
state: &'s QueryState<D, F>, | ||
last_run: Tick, | ||
this_run: Tick, | ||
) -> Self { | ||
state.validate_world(world.id()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why remove this check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, right, that was subtle and I should have pointed it out!
The reason is that I changed all of the &self
and &mut self
methods on Query
to call self.as_readonly().actual_method()
and self.reborrow().actual_method()
, and as_readonly()
and reborrow()
are implemented in terms of Query::new()
. I wanted to make sure that change was zero-cost, but checking the assembly with cargo-show-asm
showed that it was adding a call to validate_world
to every existing method on Query
! Since we always had a world that was already validated, I changed it to be a safety requirement, and now the assembly appears to be unchanged.
(Then I removed the test you asked about above because it's no longer true that it panics.)
I think it would be good to add more examples/tests that might help miri find |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me, mostly just nits although I do have one question about that IntoIterator
for Query
?
@@ -381,6 +381,14 @@ pub struct Query<'world, 'state, D: QueryData, F: QueryFilter = ()> { | |||
this_run: Tick, | |||
} | |||
|
|||
impl<D: ReadOnlyQueryData, F: QueryFilter> Clone for Query<'_, '_, D, F> { | |||
fn clone(&self) -> Self { | |||
*self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, noob question: how does this work?
The &QueryState
inside the Query is simply copied? i.e. we create a new Query that points to the same QueryState?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup! Everything in Query
is Copy
, which means it can be copied by memcpy
. That includes references like &QueryState
, which just makes a new reference to the same underlying QueryState
. So we can make Query
be Copy
, and then implement Clone
in terms of it.
/// Creates a [`Query`] from the given [`QueryState`] and [`World`]. | ||
/// | ||
/// This will create read-only queries, see [`Self::query_mut`] for mutable queries. | ||
pub fn query<'w, 's>(&'s mut self, world: &'w World) -> Query<'w, 's, D::ReadOnly, F> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh my gosh finally.
self.state | ||
.iter_unchecked_manual(self.world, self.last_run, self.this_run) | ||
} | ||
self.reborrow().into_iter() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry noob question again could you please explain again why we need to re-borrow?
self.reborrow().into_iter()
creates a new Query with reborrow()
and then borrows from that with into_iter()
self.into_iter()
would simply create a QueryIter that borrows from the initital &'a mut Query
so the lifetime is constrained by `a; is that the issue?
The lifetimes are elided on reborrow
so I don't get exactly what is happening
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reborrow()
goes from &'a mut Query<'w>
to Query<'a>
, turning a borrowed Query
into an owned one with a shorter lifetime.
into_iter()
takes self
instead of &mut self
, so it actually consumes the Query
.
We have an &mut Query
, so we first reborrow()
to get an owned Query
with a shorter lifetime, and then use into_iter()
to consume the owned query and create a QueryIter
- still with the shorter lifetime.
It's more complex when looking at one method, but across the whole type it means we can re-use the safety proofs in reborrow()
and as_readonly()
. That lets us implement this method without unsafe
code, and means the compiler would catch it if we tried to return QueryIter<'w, ...>
instead of QueryIter<'_, ...>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry to block this (the merge queue got there first), but I don't think Query
being Copy
is sound?
IIUC you can go from a mutable Query
to a read-only QueryLens
, make a Query
out of that lens again, then copy that Query
, transmute_lens
it again, and get a Query
from it for a last time. Then you'd have a read-only query that is no longer bound to the &mut self
of the original mutable Query
.
Did I overlook something?
Discuss in #17693. |
Something else I'd like to note is that this PR implements |
Yup! That's the generalization of I'm hoping it will be a nicer experience for users: You can write the obvious And because read-only queries also |
And while I'd been focusing on For example: let mut lens: QueryLens<_> = ...;
// Fails with "error[E0716]: temporary value dropped while borrowed"
let first = lens.query().iter_mut().next();
assert!(first.is_some());
// Works!
let first = lens.query().into_iter().next();
assert!(first.is_some()); |
# Objective Restore the behavior of `Query::get_many` prior to #15858. When passed duplicate `Entity`s, `get_many` is supposed to return results for all of them, since read-only queries don't alias. However, #15858 merged the implementation with `get_many_mut` and caused it to return `QueryEntityError::AliasedMutability`. ## Solution Introduce a new `Query::get_many_readonly` method that consumes the `Query` like `get_many_inner`, but that is constrained to `D: ReadOnlyQueryData` so that it can skip the aliasing check. Implement `Query::get_many` in terms of that new method. Add a test, and a comment explaining why it doesn't match the pattern of the other `&self` methods.
Objective
Simplify and expand the API for
QueryState
.QueryState
has a lot of methods that mirror those onQuery
. These are then multiplied by variants that take&World
,&mut World
, andUnsafeWorldCell
. In addition, many of them have_manual
variants that take&QueryState
and avoid callingupdate_archetypes()
. Not all of the combinations exist, however, so some operations are not possible.Solution
Introduce methods to get a
Query
from aQueryState
. That will reduce duplication between the types, and ensure that the fullQuery
API is always available forQueryState
.Introduce methods on
Query
that consume the query to return types with the full'w
lifetime. This avoids issues with borrowing where things likequery_state.query(&world).get(entity)
don't work because they borrow from the temporaryQuery
.Finally, implement
Copy
for read-onlyQuery
s.get_inner
anditer_inner
currently take&self
, so changing them to consumeself
would be a breaking change. By makingQuery: Copy
, they can consume a copy ofself
and continue to work.The consuming methods also let us simplify the implementation of methods on
Query
, by doingfn foo(&self) { self.as_readonly().foo_inner() }
andfn foo_mut(&mut self) { self.reborrow().foo_inner() }
. That structure makes it more difficult to accidentally extend lifetimes, since the safeas_readonly()
andreborrow()
methods shrink them appropriately. The optimizer is able to see that they are both identity functions and inline them, so there should be no performance cost.Note that this change would conflict with #15848. If
QueryState
is stored as aCow
, then the consuming methods cannot be implemented, andCopy
cannot be implemented.Future Work
The next step is to mark the methods on
QueryState
as#[deprecated]
, and move the implementations intoQuery
.Migration Guide
Query::to_readonly
has been renamed toQuery::as_readonly
.