Skip to content

Commit 73c78c3

Browse files
Use lifetimed, type erased pointers in bevy_ecs (#3001)
# Objective `bevy_ecs` has large amounts of unsafe code which is hard to get right and makes it difficult to audit for soundness. ## Solution Introduce lifetimed, type-erased pointers: `Ptr<'a>` `PtrMut<'a>` `OwningPtr<'a>'` and `ThinSlicePtr<'a, T>` which are newtypes around a raw pointer with a lifetime and conceptually representing strong invariants about the pointee and validity of the pointer. The process of converting bevy_ecs to use these has already caught multiple cases of unsound behavior. ## Changelog TL;DR for release notes: `bevy_ecs` now uses lifetimed, type-erased pointers internally, significantly improving safety and legibility without sacrificing performance. This should have approximately no end user impact, unless you were meddling with the (unfortunately public) internals of `bevy_ecs`. - `Fetch`, `FilterFetch` and `ReadOnlyFetch` trait no longer have a `'state` lifetime - this was unneeded - `ReadOnly/Fetch` associated types on `WorldQuery` are now on a new `WorldQueryGats<'world>` trait - was required to work around lack of Generic Associated Types (we wish to express `type Fetch<'a>: Fetch<'a>`) - `derive(WorldQuery)` no longer requires `'w` lifetime on struct - this was unneeded, and improves the end user experience - `EntityMut::get_unchecked_mut` returns `&'_ mut T` not `&'w mut T` - allows easier use of unsafe API with less footguns, and can be worked around via lifetime transmutery as a user - `Bundle::from_components` now takes a `ctx` parameter to pass to the `FnMut` closure - required because closure return types can't borrow from captures - `Fetch::init` takes `&'world World`, `Fetch::set_archetype` takes `&'world Archetype` and `&'world Tables`, `Fetch::set_table` takes `&'world Table` - allows types implementing `Fetch` to store borrows into world - `WorldQuery` trait now has a `shrink` fn to shorten the lifetime in `Fetch::<'a>::Item` - this works around lack of subtyping of assoc types, rust doesnt allow you to turn `<T as Fetch<'static>>::Item'` into `<T as Fetch<'a>>::Item'` - `QueryCombinationsIter` requires this - Most types implementing `Fetch` now have a lifetime `'w` - allows the fetches to store borrows of world data instead of using raw pointers ## Migration guide - `EntityMut::get_unchecked_mut` returns a more restricted lifetime, there is no general way to migrate this as it depends on your code - `Bundle::from_components` implementations must pass the `ctx` arg to `func` - `Bundle::from_components` callers have to use a fn arg instead of closure captures for borrowing from world - Remove lifetime args on `derive(WorldQuery)` structs as it is nonsensical - `<Q as WorldQuery>::ReadOnly/Fetch` should be changed to either `RO/QueryFetch<'world>` or `<Q as WorldQueryGats<'world>>::ReadOnly/Fetch` - `<F as Fetch<'w, 's>>` should be changed to `<F as Fetch<'w>>` - Change the fn sigs of `Fetch::init/set_archetype/set_table` to match respective trait fn sigs - Implement the required `fn shrink` on any `WorldQuery` implementations - Move assoc types `Fetch` and `ReadOnlyFetch` on `WorldQuery` impls to `WorldQueryGats` impls - Pass an appropriate `'world` lifetime to whatever fetch struct you are for some reason using ### Type inference regression in some cases rustc may give spurrious errors when attempting to infer the `F` parameter on a query/querystate this can be fixed by manually specifying the type, i.e. `QueryState::new::<_, ()>(world)`. The error is rather confusing: ```rust= error[E0271]: type mismatch resolving `<() as Fetch<'_>>::Item == bool` --> crates/bevy_pbr/src/render/light.rs:1413:30 | 1413 | main_view_query: QueryState::new(world), | ^^^^^^^^^^^^^^^ expected `bool`, found `()` | = note: required because of the requirements on the impl of `for<'x> FilterFetch<'x>` for `<() as WorldQueryGats<'x>>::Fetch` note: required by a bound in `bevy_ecs::query::QueryState::<Q, F>::new` --> crates/bevy_ecs/src/query/state.rs:49:32 | 49 | for<'x> QueryFetch<'x, F>: FilterFetch<'x>, | ^^^^^^^^^^^^^^^ required by this bound in `bevy_ecs::query::QueryState::<Q, F>::new` ``` --- Made with help from @BoxyUwU and @alice-i-cecile Co-authored-by: Boxy <[email protected]>
1 parent ddce22b commit 73c78c3

File tree

21 files changed

+1516
-1274
lines changed

21 files changed

+1516
-1274
lines changed

crates/bevy_ecs/macros/src/fetch.rs

Lines changed: 168 additions & 333 deletions
Large diffs are not rendered by default.

crates/bevy_ecs/macros/src/lib.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,18 +124,17 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
124124
self.#field.get_components(&mut func);
125125
});
126126
field_from_components.push(quote! {
127-
#field: <#field_type as #ecs_path::bundle::Bundle>::from_components(&mut func),
127+
#field: <#field_type as #ecs_path::bundle::Bundle>::from_components(ctx, &mut func),
128128
});
129129
} else {
130130
field_component_ids.push(quote! {
131131
component_ids.push(components.init_component::<#field_type>(storages));
132132
});
133133
field_get_components.push(quote! {
134-
func((&mut self.#field as *mut #field_type).cast::<u8>());
135-
std::mem::forget(self.#field);
134+
#ecs_path::ptr::OwningPtr::make(self.#field, &mut func);
136135
});
137136
field_from_components.push(quote! {
138-
#field: func().cast::<#field_type>().read(),
137+
#field: func(ctx).inner().as_ptr().cast::<#field_type>().read(),
139138
});
140139
}
141140
}
@@ -157,14 +156,17 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
157156
}
158157

159158
#[allow(unused_variables, unused_mut, non_snake_case)]
160-
unsafe fn from_components(mut func: impl FnMut() -> *mut u8) -> Self {
159+
unsafe fn from_components<T, F>(ctx: &mut T, mut func: F) -> Self
160+
where
161+
F: FnMut(&mut T) -> #ecs_path::ptr::OwningPtr<'_>
162+
{
161163
Self {
162164
#(#field_from_components)*
163165
}
164166
}
165167

166168
#[allow(unused_variables, unused_mut, forget_copy, forget_ref)]
167-
fn get_components(mut self, mut func: impl FnMut(*mut u8)) {
169+
fn get_components(self, mut func: impl FnMut(#ecs_path::ptr::OwningPtr<'_>)) {
168170
#(#field_get_components)*
169171
}
170172
}

crates/bevy_ecs/src/bundle.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::{
88
archetype::{AddBundle, Archetype, ArchetypeId, Archetypes, ComponentStatus},
99
component::{Component, ComponentId, ComponentTicks, Components, StorageType},
1010
entity::{Entities, Entity, EntityLocation},
11+
ptr::OwningPtr,
1112
storage::{SparseSetIndex, SparseSets, Storages, Table},
1213
};
1314
use bevy_ecs_macros::all_tuples;
@@ -85,14 +86,15 @@ pub unsafe trait Bundle: Send + Sync + 'static {
8586
/// # Safety
8687
/// Caller must return data for each component in the bundle, in the order of this bundle's
8788
/// [`Component`]s
88-
unsafe fn from_components(func: impl FnMut() -> *mut u8) -> Self
89+
unsafe fn from_components<T, F>(ctx: &mut T, func: F) -> Self
8990
where
91+
F: FnMut(&mut T) -> OwningPtr<'_>,
9092
Self: Sized;
9193

9294
/// Calls `func` on each value, in the order of this bundle's [`Component`]s. This will
9395
/// [`std::mem::forget`] the bundle fields, so callers are responsible for dropping the fields
9496
/// if that is desirable.
95-
fn get_components(self, func: impl FnMut(*mut u8));
97+
fn get_components(self, func: impl FnMut(OwningPtr<'_>));
9698
}
9799

98100
macro_rules! tuple_impl {
@@ -105,21 +107,23 @@ macro_rules! tuple_impl {
105107

106108
#[allow(unused_variables, unused_mut)]
107109
#[allow(clippy::unused_unit)]
108-
unsafe fn from_components(mut func: impl FnMut() -> *mut u8) -> Self {
110+
unsafe fn from_components<T, F>(ctx: &mut T, mut func: F) -> Self
111+
where
112+
F: FnMut(&mut T) -> OwningPtr<'_>
113+
{
109114
#[allow(non_snake_case)]
110115
let ($(mut $name,)*) = (
111-
$(func().cast::<$name>(),)*
116+
$(func(ctx).inner().cast::<$name>(),)*
112117
);
113-
($($name.read(),)*)
118+
($($name.as_ptr().read(),)*)
114119
}
115120

116121
#[allow(unused_variables, unused_mut)]
117-
fn get_components(self, mut func: impl FnMut(*mut u8)) {
122+
fn get_components(self, mut func: impl FnMut(OwningPtr<'_>)) {
118123
#[allow(non_snake_case)]
119124
let ($(mut $name,)*) = self;
120125
$(
121-
func((&mut $name as *mut $name).cast::<u8>());
122-
std::mem::forget($name);
126+
OwningPtr::make($name, &mut func);
123127
)*
124128
}
125129
}

crates/bevy_ecs/src/component.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Types for declaring and storing [`Component`]s.
22
33
use crate::{
4+
ptr::OwningPtr,
45
storage::{SparseSetIndex, Storages},
56
system::Resource,
67
};
@@ -109,7 +110,7 @@ impl ComponentInfo {
109110
}
110111

111112
#[inline]
112-
pub fn drop(&self) -> unsafe fn(*mut u8) {
113+
pub fn drop(&self) -> unsafe fn(OwningPtr<'_>) {
113114
self.descriptor.drop
114115
}
115116

@@ -154,7 +155,6 @@ impl SparseSetIndex for ComponentId {
154155
}
155156
}
156157

157-
#[derive(Debug)]
158158
pub struct ComponentDescriptor {
159159
name: String,
160160
// SAFETY: This must remain private. It must match the statically known StorageType of the
@@ -165,13 +165,28 @@ pub struct ComponentDescriptor {
165165
is_send_and_sync: bool,
166166
type_id: Option<TypeId>,
167167
layout: Layout,
168-
drop: unsafe fn(*mut u8),
168+
// SAFETY: this function must be safe to call with pointers pointing to items of the type
169+
// this descriptor describes.
170+
drop: for<'a> unsafe fn(OwningPtr<'a>),
171+
}
172+
173+
// We need to ignore the `drop` field in our `Debug` impl
174+
impl std::fmt::Debug for ComponentDescriptor {
175+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176+
f.debug_struct("ComponentDescriptor")
177+
.field("name", &self.name)
178+
.field("storage_type", &self.storage_type)
179+
.field("is_send_and_sync", &self.is_send_and_sync)
180+
.field("type_id", &self.type_id)
181+
.field("layout", &self.layout)
182+
.finish()
183+
}
169184
}
170185

171186
impl ComponentDescriptor {
172187
// SAFETY: The pointer points to a valid value of type `T` and it is safe to drop this value.
173-
unsafe fn drop_ptr<T>(x: *mut u8) {
174-
x.cast::<T>().drop_in_place();
188+
unsafe fn drop_ptr<T>(x: OwningPtr<'_>) {
189+
x.inner().cast::<T>().as_ptr().drop_in_place()
175190
}
176191

177192
pub fn new<T: Component>() -> Self {
@@ -330,7 +345,7 @@ impl Components {
330345
}
331346
}
332347

333-
#[derive(Clone, Debug)]
348+
#[derive(Copy, Clone, Debug)]
334349
pub struct ComponentTicks {
335350
pub(crate) added: u32,
336351
pub(crate) changed: u32,

crates/bevy_ecs/src/lib.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod change_detection;
66
pub mod component;
77
pub mod entity;
88
pub mod event;
9+
pub mod ptr;
910
pub mod query;
1011
#[cfg(feature = "bevy_reflect")]
1112
pub mod reflect;
@@ -46,14 +47,12 @@ pub use bevy_ecs_macros::all_tuples;
4647
#[cfg(test)]
4748
mod tests {
4849
use crate as bevy_ecs;
50+
use crate::prelude::Or;
4951
use crate::{
5052
bundle::Bundle,
5153
component::{Component, ComponentId},
5254
entity::Entity,
53-
query::{
54-
Added, ChangeTrackers, Changed, FilterFetch, FilteredAccess, Or, With, Without,
55-
WorldQuery,
56-
},
55+
query::{Added, ChangeTrackers, Changed, FilteredAccess, With, Without, WorldQuery},
5756
world::{Mut, World},
5857
};
5958
use bevy_tasks::TaskPool;
@@ -899,10 +898,7 @@ mod tests {
899898
}
900899
}
901900

902-
fn get_filtered<F: WorldQuery>(world: &mut World) -> Vec<Entity>
903-
where
904-
F::Fetch: FilterFetch,
905-
{
901+
fn get_filtered<F: WorldQuery>(world: &mut World) -> Vec<Entity> {
906902
world
907903
.query_filtered::<Entity, F>()
908904
.iter(world)

0 commit comments

Comments
 (0)