Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
eb9ea33
Introduce Command::apply_raw
james7132 Aug 15, 2025
b6e3bfd
Fix CI
james7132 Aug 15, 2025
b65945c
Typo fix
james7132 Aug 15, 2025
4aab60f
Attempt to pipe this through DynamicBundle
james7132 Aug 15, 2025
14c0669
Use &raw instead.
james7132 Aug 16, 2025
347b3e3
Enable unalignned Bundle inserts
james7132 Aug 17, 2025
ed19829
Fix most of the miri identified UB
james7132 Aug 26, 2025
949ffcf
CI Fixes
james7132 Aug 26, 2025
a31ff32
Dead code
james7132 Aug 26, 2025
ef6a097
Split effect application
james7132 Aug 27, 2025
86d0d12
Clippy fixes
james7132 Aug 27, 2025
f797072
Fix UB in relationship spawns
james7132 Aug 27, 2025
a6b342f
Typos
james7132 Aug 27, 2025
051c256
Revert "Introduce Command::apply_raw"
james7132 Aug 27, 2025
c453b08
Revert the Command::apply_raw changes
james7132 Aug 27, 2025
2f4a5d5
Revert more Command::apply_raw changes
james7132 Aug 27, 2025
a2aba3e
Use const pointers
james7132 Aug 27, 2025
97b0188
Introduce RemoteDropPtr
james7132 Aug 27, 2025
a9d2f3a
Use ManuallyDrop instead of core::mem::forget
james7132 Aug 27, 2025
ea489e5
Merge branch 'main' into stack-overflow-fix
james7132 Aug 27, 2025
13688aa
Safety comments
james7132 Aug 27, 2025
3bcc2fb
Update safety invariants for DynamicBundle
james7132 Aug 27, 2025
2d38772
Update safety comments about `Effect: NoBundleEffect`
james7132 Aug 27, 2025
46badec
CI fix
james7132 Aug 27, 2025
83bd955
Canonicalize that Clone impl
james7132 Aug 27, 2025
63fa898
Miri: Unique access
james7132 Aug 27, 2025
bc74acf
Clippy fixes
james7132 Aug 27, 2025
207ca9a
Typos
james7132 Aug 27, 2025
cc16b6d
take advantage of ptr methods on dynamicbundle for children
janis-bhm Aug 27, 2025
57d7fd3
since bundles can be inserted with unaligned pointers, these must als…
janis-bhm Aug 27, 2025
1ac8358
impl spawn_raw for SpawnableList variadic tuples
janis-bhm Aug 27, 2025
82894f1
safety comment for <Spawn<B> as SpawnableList>::spawn
janis-bhm Aug 27, 2025
ed3ca7e
implementations of *_raw functions correctly require well-aligned poi…
janis-bhm Aug 27, 2025
959fe01
Apply suggestions from code review
james7132 Aug 27, 2025
e84cce2
Apply suggestions from code review
james7132 Aug 28, 2025
a452621
Minimize further stack copies and properly document insert_raw_by_id
james7132 Aug 28, 2025
9043bf6
Better safety comments for insert and spawn
james7132 Aug 28, 2025
ed9eb64
More safety comments
james7132 Aug 28, 2025
c5e4696
Update alignment based safety comments
james7132 Aug 28, 2025
0f85d1e
Formatting
james7132 Aug 28, 2025
294a9d0
Use MaybeUninit for apply_effect
james7132 Aug 28, 2025
606794e
Formatting and typos
james7132 Aug 28, 2025
f659939
Merge branch 'main' into stack-overflow-fix
james7132 Sep 7, 2025
23b54f2
Remove RemoteDropPtr in favor of MovingPtr
james7132 Sep 7, 2025
607ba15
Move to use MovingPtr instead of raw pointers
james7132 Sep 8, 2025
77d8ce3
Cleanup
james7132 Sep 8, 2025
a6400c4
Fix doctest
james7132 Sep 8, 2025
00cc71e
Move MovingPtr initialization into its own unsafe block
james7132 Sep 8, 2025
2e4670f
Clean up unnecessary unsafe invariants
james7132 Sep 8, 2025
ab12802
Cleanup more unneccessary safety invariants
james7132 Sep 8, 2025
0fbc2e3
Remove now unused BundleEffect trait
james7132 Sep 8, 2025
18dca82
CI fixes
james7132 Sep 8, 2025
205f19b
Add move_as_ptr to replace MovingPtr::make
james7132 Sep 8, 2025
3da7fb5
Clean up MaybeUninit mentions in safety comments
james7132 Sep 8, 2025
5c4afb5
Add missing safety comment for partial move
james7132 Sep 8, 2025
5e1a16e
Fix UB in Spawn<B>
james7132 Sep 8, 2025
d7ffbd7
Typo fix
james7132 Sep 8, 2025
f873b74
Return to insert_with_caller to being a safe function
james7132 Sep 8, 2025
5e4034f
Add more information about why the static assertion exists
james7132 Sep 8, 2025
5582bf5
Make partial_move and spawn_with_caller safe
james7132 Sep 8, 2025
72f4001
Replace spawn with spawn_raw's implementations
james7132 Sep 9, 2025
e9f9356
Missing unsafe
james7132 Sep 9, 2025
afd4b86
Ensure derive(Bundle): Drop types fail to compile
james7132 Sep 9, 2025
95d81a6
Update failure reason
james7132 Sep 9, 2025
7f0a0d9
Make move_field to use &raw mut via closures
james7132 Sep 9, 2025
31fea98
Formatting
james7132 Sep 9, 2025
df5a255
Merge branch 'main' into stack-overflow-fix
james7132 Sep 10, 2025
742f70b
Scope deconstruct_moving_ptr to just destructuring
james7132 Sep 10, 2025
bf1542a
Fix CI
james7132 Sep 10, 2025
3514457
Apply suggestions from code review
james7132 Sep 11, 2025
bc02b40
Fix up the safety comment in spawn_with_caller
james7132 Sep 11, 2025
bf882d3
Better docs for DynamicBundle functions
james7132 Sep 11, 2025
b051573
Update safety comments
james7132 Sep 11, 2025
eb12baf
CI fixes
james7132 Sep 11, 2025
96b15b7
Formatting
james7132 Sep 11, 2025
fd06151
Add non-doc comments for justifying the use of MovingPtr
james7132 Sep 12, 2025
4b5789b
Ytop
james7132 Sep 12, 2025
877b2d2
Apply suggestions from code review
james7132 Sep 12, 2025
32b38d0
Code review fixes
james7132 Sep 12, 2025
1e2c514
Typo
james7132 Sep 12, 2025
afb4a29
No fat-finger typo
james7132 Sep 12, 2025
d618a30
Remove the field type safety invariant
james7132 Sep 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions crates/bevy_ecs/compile_fail/tests/ui/bundle_on_drop_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use bevy_ecs::prelude::*;

#[derive(Component, Debug)]
pub struct A(usize);

// this should fail since destructuring T: Drop cannot be split.
#[derive(Bundle, Debug)]
//~^ E0509
pub struct DropBundle {
component_a: A,
}

impl Drop for DropBundle {
fn drop(&mut self) {
// Just need the impl
}
}
41 changes: 37 additions & 4 deletions crates/bevy_ecs/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,23 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {

let mut active_field_types = Vec::new();
let mut active_field_tokens = Vec::new();
let mut active_field_alias: Vec<proc_macro2::TokenStream> = Vec::new();
let mut inactive_field_tokens = Vec::new();
for (((i, field_type), field_kind), field) in field_type
.iter()
.enumerate()
.zip(field_kind.iter())
.zip(field.iter())
{
let field_alias = format_ident!("field_{}", i).to_token_stream();
let field_tokens = match field {
Some(field) => field.to_token_stream(),
None => Index::from(i).to_token_stream(),
};
match field_kind {
BundleFieldKind::Component => {
active_field_types.push(field_type);
active_field_alias.push(field_alias);
active_field_tokens.push(field_tokens);
}

Expand Down Expand Up @@ -163,16 +166,46 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
};

let dynamic_bundle_impl = quote! {
#[allow(deprecated)]
#[doc(hidden)]
#[expect(dead_code, reason = "This is a static assertion.")]
impl #impl_generics #struct_name #ty_generics #where_clause {
// Types that implement `Drop` cannot have their fields moved out. The implementation in
// `get_componments` avoids this with pointers, so there needs to be a static assertion
// that this is a sound thing to do. See https://doc.rust-lang.org/error_codes/E0509.html
// for more information.
fn check_no_bundle_drop(self) {
// This has no effect, but we need to make sure the compiler doesn't optimize it out
// black_box is used to do this
#( core::hint::black_box(self.#active_field_tokens); )*
Comment thread
james7132 marked this conversation as resolved.
}
}

impl #impl_generics #ecs_path::bundle::DynamicBundle for #struct_name #ty_generics #where_clause {
type Effect = ();
#[allow(unused_variables)]
#[inline]
fn get_components(
self,
unsafe fn get_components(
ptr: #ecs_path::ptr::MovingPtr<'_, Self>,
func: &mut impl FnMut(#ecs_path::component::StorageType, #ecs_path::ptr::OwningPtr<'_>)
) {
#(<#active_field_types as #ecs_path::bundle::DynamicBundle>::get_components(self.#active_field_tokens, &mut *func);)*
use #ecs_path::__macro_exports::DebugCheckedUnwrap;

#( let #active_field_alias = ptr.move_field(|ptr| &raw mut (*ptr).#active_field_tokens); )*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this doing the same thing as deconstruct_moving_ptr!? Should we use that here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The macro isn't quite flexible enough to support both normal structts and tuple structs, hence why there's the usage of active_field_alias. I'd love to use it here, but lack the flexibility right now with that macro.

core::mem::forget(ptr);
#(
<#active_field_types as #ecs_path::bundle::DynamicBundle>::get_components(
#active_field_alias.try_into().debug_checked_unwrap(),
func
);
)*
}

#[allow(unused_variables)]
#[inline]
unsafe fn apply_effect(
ptr: #ecs_path::ptr::MovingPtr<'_, core::mem::MaybeUninit<Self>>,
func: &mut #ecs_path::world::EntityWorldMut<'_>,
) {
}
}
};
Expand Down
95 changes: 48 additions & 47 deletions crates/bevy_ecs/src/bundle/impls.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use core::any::TypeId;

use bevy_ptr::OwningPtr;
use variadics_please::all_tuples;
use bevy_ptr::{MovingPtr, OwningPtr};
use core::mem::MaybeUninit;
use variadics_please::all_tuples_enumerated;

use crate::{
bundle::{Bundle, BundleEffect, BundleFromComponents, DynamicBundle, NoBundleEffect},
bundle::{Bundle, BundleFromComponents, DynamicBundle, NoBundleEffect},
component::{Component, ComponentId, Components, ComponentsRegistrator, StorageType},
query::DebugCheckedUnwrap,
world::EntityWorldMut,
};

Expand Down Expand Up @@ -40,13 +42,19 @@ unsafe impl<C: Component> BundleFromComponents for C {
impl<C: Component> DynamicBundle for C {
type Effect = ();
#[inline]
fn get_components(self, func: &mut impl FnMut(StorageType, OwningPtr<'_>)) -> Self::Effect {
OwningPtr::make(self, |ptr| func(C::STORAGE_TYPE, ptr));
unsafe fn get_components(
ptr: MovingPtr<'_, Self>,
func: &mut impl FnMut(StorageType, OwningPtr<'_>),
) -> Self::Effect {
func(C::STORAGE_TYPE, OwningPtr::from(ptr));
}

#[inline]
unsafe fn apply_effect(_ptr: MovingPtr<'_, MaybeUninit<Self>>, _entity: &mut EntityWorldMut) {}
}

macro_rules! tuple_impl {
($(#[$meta:meta])* $($name: ident),*) => {
($(#[$meta:meta])* $(($index:tt, $name: ident, $alias: ident)),*) => {
#[expect(
clippy::allow_attributes,
reason = "This is a tuple-related macro; as such, the lints below may not always apply."
Expand Down Expand Up @@ -125,59 +133,52 @@ macro_rules! tuple_impl {
reason = "Zero-length tuples will generate a function body equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case."
)]
#[inline(always)]
fn get_components(self, func: &mut impl FnMut(StorageType, OwningPtr<'_>)) -> Self::Effect {
#[allow(
non_snake_case,
reason = "The names of these variables are provided by the caller, not by us."
)]
let ($(mut $name,)*) = self;
($(
$name.get_components(&mut *func),
)*)
unsafe fn get_components(ptr: MovingPtr<'_, Self>, func: &mut impl FnMut(StorageType, OwningPtr<'_>)) {
// SAFETY:
// - All of the `move_field` calls all fetch distinct and valid fields within `Self`.
// - If a field is `NoBundleEffect`, it's `apply_effect` is a no-op
// and cannot move any value out of an invalid instance after this call.
// - If a field is `!NoBundleEffect`, it must be valid since a safe
// implementation of `DynamicBundle` only moves the value out only
// once between `get_components` and `apply_effect`.
bevy_ptr::deconstruct_moving_ptr!(ptr => ($($index => $alias,)*));
// SAFETY:
// - If `ptr` is aligned, then field_ptr is aligned properly. Rust tuples cannot be `repr(packed)`.
$( $name::get_components($alias.try_into().debug_checked_unwrap(), func); )*
}
}
}
}

all_tuples!(
#[doc(fake_variadic)]
tuple_impl,
0,
15,
B
);

macro_rules! after_effect_impl {
($(#[$meta:meta])* $($after_effect: ident),*) => {
#[expect(
clippy::allow_attributes,
reason = "This is a tuple-related macro; as such, the lints below may not always apply."
)]
$(#[$meta])*
impl<$($after_effect: BundleEffect),*> BundleEffect for ($($after_effect,)*) {
#[allow(
clippy::unused_unit,
reason = "Zero-length tuples will generate a function body equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case.")
]
fn apply(self, _entity: &mut EntityWorldMut) {
#[allow(
non_snake_case,
reason = "The names of these variables are provided by the caller, not by us."
)]
let ($($after_effect,)*) = self;
$($after_effect.apply(_entity);)*
reason = "Zero-length tuples will generate a function body equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case."
)]
#[inline(always)]
unsafe fn apply_effect(ptr: MovingPtr<'_, MaybeUninit<Self>>, entity: &mut EntityWorldMut) {
// SAFETY:
// - All of the `move_field` calls all fetch distinct and valid fields within `Self`.
// - If a field is `NoBundleEffect`, it's `apply_effect` is a no-op
// and cannot move any value out of an invalid instance.
// - If a field is `!NoBundleEffect`, it must be valid since a safe
// implementation of `DynamicBundle` only moves the value out only
// once between `get_components` and `apply_effect`.
bevy_ptr::deconstruct_moving_ptr!(ptr: MaybeUninit => (
$($index => $alias,)*
));
// SAFETY:
// - If `ptr` is aligned, then field_ptr is aligned properly. Rust tuples cannot be `repr(packed)`.
$( $name::apply_effect($alias.try_into().debug_checked_unwrap(), entity); )*
}
}

$(#[$meta])*
impl<$($after_effect: NoBundleEffect),*> NoBundleEffect for ($($after_effect,)*) { }
impl<$($name: NoBundleEffect),*> NoBundleEffect for ($($name,)*) {}
}
}

all_tuples!(
all_tuples_enumerated!(
#[doc(fake_variadic)]
after_effect_impl,
tuple_impl,
0,
15,
P
B,
field_
);
16 changes: 10 additions & 6 deletions crates/bevy_ecs/src/bundle/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use bevy_platform::{
collections::{HashMap, HashSet},
hash::FixedHasher,
};
use bevy_ptr::OwningPtr;
use bevy_ptr::{MovingPtr, OwningPtr};
use bevy_utils::TypeIdMap;
use core::{any::TypeId, ptr::NonNull};
use indexmap::{IndexMap, IndexSet};
Expand Down Expand Up @@ -228,8 +228,14 @@ impl BundleInfo {
/// which removes the need to look up the [`ArchetypeAfterBundleInsert`](crate::archetype::ArchetypeAfterBundleInsert)
/// in the archetype graph, which requires ownership of the entity's current archetype.
///
/// Regardless of how this is used, [`apply_effect`] must be called at most once on `bundle` after this function is
/// called if `T::Effect: !NoBundleEffect` before returning to user-space safe code before returning to user-space safe code.
/// This is currently only doable via use of [`MovingPtr::partial_move`].
///
/// `table` must be the "new" table for `entity`. `table_row` must have space allocated for the
/// `entity`, `bundle` must match this [`BundleInfo`]'s type
///
/// [`apply_effect`]: crate::bundle::DynamicBundle::apply_effect
#[inline]
pub(super) unsafe fn write_components<'a, T: DynamicBundle, S: BundleComponentStatus>(
&self,
Expand All @@ -240,14 +246,14 @@ impl BundleInfo {
entity: Entity,
table_row: TableRow,
change_tick: Tick,
bundle: T,
bundle: MovingPtr<'_, T>,
insert_mode: InsertMode,
caller: MaybeLocation,
) -> T::Effect {
) {
// NOTE: get_components calls this closure on each component in "bundle order".
// bundle_info.component_ids are also in "bundle order"
let mut bundle_component = 0;
let after_effect = bundle.get_components(&mut |storage_type, component_ptr| {
T::get_components(bundle, &mut |storage_type, component_ptr| {
let component_id = *self
.contributed_component_ids
.get_unchecked(bundle_component);
Expand Down Expand Up @@ -303,8 +309,6 @@ impl BundleInfo {
caller,
);
}

after_effect
}

/// Internal method to initialize a required component from an [`OwningPtr`]. This should ultimately be called
Expand Down
33 changes: 20 additions & 13 deletions crates/bevy_ecs/src/bundle/insert.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use alloc::vec::Vec;
use bevy_ptr::ConstNonNull;
use bevy_ptr::{ConstNonNull, MovingPtr};
use core::ptr::NonNull;

use crate::{
Expand Down Expand Up @@ -138,18 +138,25 @@ impl<'w> BundleInserter<'w> {
}

/// # Safety
/// `entity` must currently exist in the source archetype for this inserter. `location`
/// must be `entity`'s location in the archetype. `T` must match this [`BundleInfo`]'s type
/// - `entity` must currently exist in the source archetype for this inserter.
/// - `location` must be `entity`'s location in the archetype.
/// - `T` must match this [`BundleInserter`] type used to create
/// - If `T::Effect: !NoBundleEffect.`, then [`apply_effect`] must be called at most once on
/// `bundle` after this function before returning to user-space safe code.
/// - The value pointed to by `bundle` must not be accessed for anything other than [`apply_effect`]
/// or dropped.
///
/// [`apply_effect`]: crate::bundle::DynamicBundle::apply_effect
#[inline]
pub(crate) unsafe fn insert<T: DynamicBundle>(
&mut self,
entity: Entity,
location: EntityLocation,
bundle: T,
bundle: MovingPtr<'_, T>,
insert_mode: InsertMode,
caller: MaybeLocation,
relationship_hook_mode: RelationshipHookMode,
) -> (EntityLocation, T::Effect) {
) -> EntityLocation {
let bundle_info = self.bundle_info.as_ref();
let archetype_after_insert = self.archetype_after_insert.as_ref();
let archetype = self.archetype.as_ref();
Expand Down Expand Up @@ -188,15 +195,15 @@ impl<'w> BundleInserter<'w> {
// so this reference can only be promoted from shared to &mut down here, after they have been ran
let archetype = self.archetype.as_mut();

let (new_archetype, new_location, after_effect) = match &mut self.archetype_move_type {
let (new_archetype, new_location) = match &mut self.archetype_move_type {
ArchetypeMoveType::SameArchetype => {
// SAFETY: Mutable references do not alias and will be dropped after this block
let sparse_sets = {
let world = self.world.world_mut();
&mut world.storages.sparse_sets
};

let after_effect = bundle_info.write_components(
bundle_info.write_components(
table,
sparse_sets,
archetype_after_insert,
Expand All @@ -209,7 +216,7 @@ impl<'w> BundleInserter<'w> {
caller,
);

(archetype, location, after_effect)
(archetype, location)
}
ArchetypeMoveType::NewArchetypeSameTable { new_archetype } => {
let new_archetype = new_archetype.as_mut();
Expand Down Expand Up @@ -237,7 +244,7 @@ impl<'w> BundleInserter<'w> {
}
let new_location = new_archetype.allocate(entity, result.table_row);
entities.set(entity.index(), Some(new_location));
let after_effect = bundle_info.write_components(
bundle_info.write_components(
table,
sparse_sets,
archetype_after_insert,
Expand All @@ -250,7 +257,7 @@ impl<'w> BundleInserter<'w> {
caller,
);

(new_archetype, new_location, after_effect)
(new_archetype, new_location)
}
ArchetypeMoveType::NewArchetypeNewTable {
new_archetype,
Expand Down Expand Up @@ -319,7 +326,7 @@ impl<'w> BundleInserter<'w> {
}
}

let after_effect = bundle_info.write_components(
bundle_info.write_components(
new_table,
sparse_sets,
archetype_after_insert,
Expand All @@ -332,7 +339,7 @@ impl<'w> BundleInserter<'w> {
caller,
);

(new_archetype, new_location, after_effect)
(new_archetype, new_location)
}
};

Expand Down Expand Up @@ -411,7 +418,7 @@ impl<'w> BundleInserter<'w> {
}
}

(new_location, after_effect)
new_location
}

#[inline]
Expand Down
Loading
Loading