Allow not emitting BundleFromComponents with Bundle derive macro (#19249)

# Objective

Fixes #19136

## Solution

- Add a new container attribute which when set does not emit
`BundleFromComponents`

## Testing

- Did you test these changes?

Yes, a new test was added.

- Are there any parts that need more testing?

Since `BundleFromComponents` is unsafe I made extra sure that I did not
misunderstand its purpose. As far as I can tell, _not_ implementing it
is ok.

- How can other people (reviewers) test your changes? Is there anything
specific they need to know?

Nope

- If relevant, what platforms did you test these changes on, and are
there any important ones you can't test?

I don't think the platform is relevant

---



One thing I am not sure about is how to document this? I'll gladly add
it

---------

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2025-06-09 22:15:42 +02:00 committed by GitHub
parent b6e4d171b5
commit 2768af5d2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 128 additions and 14 deletions

View File

@ -29,6 +29,20 @@ enum BundleFieldKind {
const BUNDLE_ATTRIBUTE_NAME: &str = "bundle";
const BUNDLE_ATTRIBUTE_IGNORE_NAME: &str = "ignore";
const BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS: &str = "ignore_from_components";
#[derive(Debug)]
struct BundleAttributes {
impl_from_components: bool,
}
impl Default for BundleAttributes {
fn default() -> Self {
Self {
impl_from_components: true,
}
}
}
/// Implement the `Bundle` trait.
#[proc_macro_derive(Bundle, attributes(bundle))]
@ -36,6 +50,27 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let ecs_path = bevy_ecs_path();
let mut errors = vec![];
let mut attributes = BundleAttributes::default();
for attr in &ast.attrs {
if attr.path().is_ident(BUNDLE_ATTRIBUTE_NAME) {
let parsing = attr.parse_nested_meta(|meta| {
if meta.path.is_ident(BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS) {
attributes.impl_from_components = false;
return Ok(());
}
Err(meta.error(format!("Invalid bundle container attribute. Allowed attributes: `{BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS}`")))
});
if let Err(error) = parsing {
errors.push(error.into_compile_error());
}
}
}
let named_fields = match get_struct_fields(&ast.data, "derive(Bundle)") {
Ok(fields) => fields,
Err(e) => return e.into_compile_error().into(),
@ -130,7 +165,28 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let struct_name = &ast.ident;
let from_components = attributes.impl_from_components.then(|| quote! {
// SAFETY:
// - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order
#[allow(deprecated)]
unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause {
#[allow(unused_variables, non_snake_case)]
unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self
where
__F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_>
{
Self{
#(#field_from_components)*
}
}
}
});
let attribute_errors = &errors;
TokenStream::from(quote! {
#(#attribute_errors)*
// SAFETY:
// - ComponentId is returned in field-definition-order. [get_components] uses field-definition-order
// - `Bundle::get_components` is exactly once for each member. Rely's on the Component -> Bundle implementation to properly pass
@ -159,20 +215,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
}
}
// SAFETY:
// - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order
#[allow(deprecated)]
unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause {
#[allow(unused_variables, non_snake_case)]
unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self
where
__F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_>
{
Self{
#(#field_from_components)*
}
}
}
#from_components
#[allow(deprecated)]
impl #impl_generics #ecs_path::bundle::DynamicBundle for #struct_name #ty_generics #where_clause {

View File

@ -2,6 +2,57 @@
//!
//! This module contains the [`Bundle`] trait and some other helper types.
/// Derive the [`Bundle`] trait
///
/// You can apply this derive macro to structs that are
/// composed of [`Component`]s or
/// other [`Bundle`]s.
///
/// ## Attributes
///
/// Sometimes parts of the Bundle should not be inserted.
/// Those can be marked with `#[bundle(ignore)]`, and they will be skipped.
/// In that case, the field needs to implement [`Default`] unless you also ignore
/// the [`BundleFromComponents`] implementation.
///
/// ```rust
/// # use bevy_ecs::prelude::{Component, Bundle};
/// # #[derive(Component)]
/// # struct Hitpoint;
/// #
/// #[derive(Bundle)]
/// struct HitpointMarker {
/// hitpoints: Hitpoint,
///
/// #[bundle(ignore)]
/// creator: Option<String>
/// }
/// ```
///
/// Some fields may be bundles that do not implement
/// [`BundleFromComponents`]. This happens for bundles that cannot be extracted.
/// For example with [`SpawnRelatedBundle`](bevy_ecs::spawn::SpawnRelatedBundle), see below for an
/// example usage.
/// In those cases you can either ignore it as above,
/// or you can opt out the whole Struct by marking it as ignored with
/// `#[bundle(ignore_from_components)]`.
///
/// ```rust
/// # use bevy_ecs::prelude::{Component, Bundle, ChildOf, Spawn};
/// # #[derive(Component)]
/// # struct Hitpoint;
/// # #[derive(Component)]
/// # struct Marker;
/// #
/// use bevy_ecs::spawn::SpawnRelatedBundle;
///
/// #[derive(Bundle)]
/// #[bundle(ignore_from_components)]
/// struct HitpointMarker {
/// hitpoints: Hitpoint,
/// related_spawner: SpawnRelatedBundle<ChildOf, Spawn<Marker>>,
/// }
/// ```
pub use bevy_ecs_macros::Bundle;
use crate::{
@ -2122,6 +2173,26 @@ mod tests {
}
}
#[derive(Bundle)]
#[bundle(ignore_from_components)]
struct BundleNoExtract {
b: B,
no_from_comp: crate::spawn::SpawnRelatedBundle<ChildOf, Spawn<C>>,
}
#[test]
fn can_spawn_bundle_without_extract() {
let mut world = World::new();
let id = world
.spawn(BundleNoExtract {
b: B,
no_from_comp: Children::spawn(Spawn(C)),
})
.id();
assert!(world.entity(id).get::<Children>().is_some());
}
#[test]
fn component_hook_order_spawn_despawn() {
let mut world = World::new();