allow Call and Closure expressions in hook macro attributes (#18017)

# Objective

This PR adds:

- function call hook attributes `#[component(on_add = func(42))]`
  - main feature of this commit
- closure hook attributes `#[component(on_add = |w, ctx| { /* ... */
})]`
  - maybe too verbose
  - but was easy to add
  - was suggested on discord

This allows to reuse common functionality without replicating a lot of
boilerplate. A small example is a hook which just adds different default
sprites. The sprite loading code would be the same for every component.
Unfortunately we can't use the required components feature, since we
need at least an `AssetServer` or other `Resource`s or `Component`s to
load the sprite.

```rs
fn load_sprite(path: &str) -> impl Fn(DeferredWorld, HookContext) {
  |mut world, ctx| {
    // ... use world to load sprite
  }
}

#[derive(Component)]
#[component(on_add = load_sprite("knight.png"))]
struct Knight;

#[derive(Component)]
#[component(on_add = load_sprite("monster.png"))]
struct Monster;
```

---

The commit also reorders the logic of the derive macro a bit. It's
probably a bit less lazy now, but the functionality shouldn't be
performance critical and is executed at compile time anyways.

## Solution

- Introduce `HookKind` enum in the component proc macro module
- extend parsing to allow more cases of expressions

## Testing

I have some code laying around. I'm not sure where to put it yet though.
Also is there a way to check compilation failures? Anyways, here it is:

```rs
use bevy::prelude::*;

#[derive(Component)]
#[component(
    on_add = fooing_and_baring,
    on_insert = fooing_and_baring,
    on_replace = fooing_and_baring,
    on_despawn = fooing_and_baring,
    on_remove = fooing_and_baring
)]
pub struct FooPath;

fn fooing_and_baring(
    world: bevy::ecs::world::DeferredWorld,
    ctx: bevy::ecs::component::HookContext,
) {
}

#[derive(Component)]
#[component(
    on_add = baring_and_bazzing("foo"),
    on_insert = baring_and_bazzing("foo"),
    on_replace = baring_and_bazzing("foo"),
    on_despawn = baring_and_bazzing("foo"),
    on_remove = baring_and_bazzing("foo")
)]
pub struct FooCall;

fn baring_and_bazzing(
    path: &str,
) -> impl Fn(bevy::ecs::world::DeferredWorld, bevy::ecs::component::HookContext) {
    |world, ctx| {}
}

#[derive(Component)]
#[component(
    on_add = |w,ctx| {},
    on_insert = |w,ctx| {},
    on_replace = |w,ctx| {},
    on_despawn = |w,ctx| {},
    on_remove = |w,ctx| {}
)]
pub struct FooClosure;

#[derive(Component, Debug)]
#[relationship(relationship_target = FooTargets)]
#[component(
    on_add = baring_and_bazzing("foo"),
    // on_insert = baring_and_bazzing("foo"),
    // on_replace = baring_and_bazzing("foo"),
    on_despawn = baring_and_bazzing("foo"),
    on_remove = baring_and_bazzing("foo")
)]
pub struct FooTargetOf(Entity);

#[derive(Component, Debug)]
#[relationship_target(relationship = FooTargetOf)]
#[component(
    on_add = |w,ctx| {},
    on_insert = |w,ctx| {},
    // on_replace = |w,ctx| {},
    // on_despawn = |w,ctx| {},
    on_remove = |w,ctx| {}
)]
pub struct FooTargets(Vec<Entity>);

// MSG:  mismatched types  expected fn pointer `for<'w> fn(bevy::bevy_ecs::world::DeferredWorld<'w>, bevy::bevy_ecs::component::HookContext)`    found struct `Bar`
//
// pub struct Bar;
// #[derive(Component)]
// #[component(
//     on_add = Bar,
// )]
// pub struct FooWrongPath;

// MSG: this function takes 1 argument but 2 arguements were supplied
//
// #[derive(Component)]
// #[component(
//     on_add = wrong_bazzing("foo"),
// )]
// pub struct FooWrongCall;
//
// fn wrong_bazzing(path: &str) -> impl Fn(bevy::ecs::world::DeferredWorld) {
//     |world| {}
// }

// MSG: expected 1 argument, found 2
//
// #[derive(Component)]
// #[component(
//     on_add = |w| {},
// )]
// pub struct FooWrongCall;
```

---

## Showcase

I'll try to continue to work on this to have a small section in the
release notes.
This commit is contained in:
RobWalt 2025-03-06 16:39:11 +00:00 committed by GitHub
parent d6db78b5dd
commit a85a3a2a15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 333 additions and 37 deletions

View File

@ -1,14 +1,11 @@
error[E0277]: the trait bound `TupleStruct: Deref` is not satisfied
--> tests/deref_mut_derive/missing_deref_fail.rs:10:8
|
10 | struct TupleStruct(usize, #[deref] String);
| ^^^^^^^^^^^ the trait `Deref` is not implemented for `TupleStruct`
|
--> tests/deref_mut_derive/missing_deref_fail.rs:9:8
|
9 | struct TupleStruct(usize, #[deref] String);
| ^^^^^^^^^^^ the trait `Deref` is not implemented for `TupleStruct`
|
note: required by a bound in `DerefMut`
--> $RUSTUP_HOME/.rustup/toolchains/stable-x86_64-pc-windows-msvc/lib/rustlib/src/rust/library/core/src/ops/deref.rs:264:21
|
264 | pub trait DerefMut: Deref {
| ^^^^^ required by this bound in `DerefMut`
--> /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/ops/deref.rs:290:1
error[E0277]: the trait bound `TupleStruct: Deref` is not satisfied
--> tests/deref_mut_derive/missing_deref_fail.rs:7:10
@ -19,21 +16,18 @@ error[E0277]: the trait bound `TupleStruct: Deref` is not satisfied
= note: this error originates in the derive macro `DerefMut` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `Struct: Deref` is not satisfied
--> tests/deref_mut_derive/missing_deref_fail.rs:15:8
|
15 | struct Struct {
| ^^^^^^ the trait `Deref` is not implemented for `Struct`
|
--> tests/deref_mut_derive/missing_deref_fail.rs:14:8
|
14 | struct Struct {
| ^^^^^^ the trait `Deref` is not implemented for `Struct`
|
note: required by a bound in `DerefMut`
--> $RUSTUP_HOME/.rustup/toolchains/stable-x86_64-pc-windows-msvc/lib/rustlib/src/rust/library/core/src/ops/deref.rs:264:21
|
264 | pub trait DerefMut: Deref {
| ^^^^^ required by this bound in `DerefMut`
--> /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/ops/deref.rs:290:1
error[E0277]: the trait bound `Struct: Deref` is not satisfied
--> tests/deref_mut_derive/missing_deref_fail.rs:13:10
--> tests/deref_mut_derive/missing_deref_fail.rs:12:10
|
13 | #[derive(DerefMut)]
12 | #[derive(DerefMut)]
| ^^^^^^^^ the trait `Deref` is not implemented for `Struct`
|
= note: this error originates in the derive macro `DerefMut` (in Nightly builds, run with -Z macro-backtrace for more info)

View File

@ -0,0 +1,14 @@
use bevy_ecs::prelude::*;
// this should fail since the function is required to have the signature
// (DeferredWorld, HookContext) -> ()
#[derive(Component)]
//~^ E0057
#[component(
on_add = wrong_bazzing("foo"),
)]
pub struct FooWrongCall;
fn wrong_bazzing(path: &str) -> impl Fn(bevy_ecs::world::DeferredWorld) {
|world| {}
}

View File

@ -0,0 +1,32 @@
warning: unused variable: `path`
--> tests/ui/component_hook_call_signature_mismatch.rs:12:18
|
12 | fn wrong_bazzing(path: &str) -> impl Fn(bevy_ecs::world::DeferredWorld) {
| ^^^^ help: if this is intentional, prefix it with an underscore: `_path`
|
= note: `#[warn(unused_variables)]` on by default
warning: unused variable: `world`
--> tests/ui/component_hook_call_signature_mismatch.rs:13:6
|
13 | |world| {}
| ^^^^^ help: if this is intentional, prefix it with an underscore: `_world`
error[E0057]: this function takes 1 argument but 2 arguments were supplied
--> tests/ui/component_hook_call_signature_mismatch.rs:8:14
|
5 | #[derive(Component)]
| --------- unexpected argument #2 of type `HookContext`
...
8 | on_add = wrong_bazzing("foo"),
| ^^^^^^^^^^^^^^^^^^^^
|
note: opaque type defined here
--> tests/ui/component_hook_call_signature_mismatch.rs:12:33
|
12 | fn wrong_bazzing(path: &str) -> impl Fn(bevy_ecs::world::DeferredWorld) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: aborting due to 1 previous error; 2 warnings emitted
For more information about this error, try `rustc --explain E0057`.

View File

@ -0,0 +1,63 @@
use bevy_ecs::prelude::*;
mod case1 {
use super::*;
#[derive(Component, Debug)]
#[component(on_insert = foo_hook)]
//~^ ERROR: Custom on_insert hooks are not supported as relationships already define an on_insert hook
#[relationship(relationship_target = FooTargets)]
pub struct FooTargetOfFail(Entity);
#[derive(Component, Debug)]
#[relationship_target(relationship = FooTargetOfFail)]
//~^ E0277
pub struct FooTargets(Vec<Entity>);
}
mod case2 {
use super::*;
#[derive(Component, Debug)]
#[component(on_replace = foo_hook)]
//~^ ERROR: Custom on_replace hooks are not supported as RelationshipTarget already defines an on_replace hook
#[relationship_target(relationship = FooTargetOf)]
pub struct FooTargetsFail(Vec<Entity>);
#[derive(Component, Debug)]
#[relationship(relationship_target = FooTargetsFail)]
//~^ E0277
pub struct FooTargetOf(Entity);
}
mod case3 {
use super::*;
#[derive(Component, Debug)]
#[component(on_replace = foo_hook)]
//~^ ERROR: Custom on_replace hooks are not supported as Relationships already define an on_replace hook
#[relationship(relationship_target = BarTargets)]
pub struct BarTargetOfFail(Entity);
#[derive(Component, Debug)]
#[relationship_target(relationship = BarTargetOfFail)]
//~^ E0277
pub struct BarTargets(Vec<Entity>);
}
mod case4 {
use super::*;
#[derive(Component, Debug)]
#[component(on_despawn = foo_hook)]
//~^ ERROR: Custom on_despawn hooks are not supported as this RelationshipTarget already defines an on_despawn hook, via the 'linked_spawn' attribute
#[relationship_target(relationship = BarTargetOf, linked_spawn)]
pub struct BarTargetsFail(Vec<Entity>);
#[derive(Component, Debug)]
#[relationship(relationship_target = BarTargetsFail)]
//~^ E0277
pub struct BarTargetOf(Entity);
}
fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::component::HookContext) {}

View File

@ -0,0 +1,91 @@
error: Custom on_insert hooks are not supported as relationships already define an on_insert hook
--> tests/ui/component_hook_relationship.rs:7:5
|
7 | #[component(on_insert = foo_hook)]
| ^
error: Custom on_replace hooks are not supported as RelationshipTarget already defines an on_replace hook
--> tests/ui/component_hook_relationship.rs:22:5
|
22 | #[component(on_replace = foo_hook)]
| ^
error: Custom on_replace hooks are not supported as Relationships already define an on_replace hook
--> tests/ui/component_hook_relationship.rs:37:5
|
37 | #[component(on_replace = foo_hook)]
| ^
error: Custom on_despawn hooks are not supported as this RelationshipTarget already defines an on_despawn hook, via the 'linked_spawn' attribute
--> tests/ui/component_hook_relationship.rs:52:5
|
52 | #[component(on_despawn = foo_hook)]
| ^
error[E0277]: the trait bound `FooTargetOfFail: Relationship` is not satisfied
--> tests/ui/component_hook_relationship.rs:13:42
|
13 | #[relationship_target(relationship = FooTargetOfFail)]
| ^^^^^^^^^^^^^^^ the trait `Relationship` is not implemented for `FooTargetOfFail`
|
= help: the following other types implement trait `Relationship`:
BarTargetOf
ChildOf
FooTargetOf
note: required by a bound in `bevy_ecs::relationship::RelationshipTarget::Relationship`
--> $BEVY_ROOT/bevy_ecs/src/relationship/mod.rs:167:24
|
167 | type Relationship: Relationship<RelationshipTarget = Self>;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RelationshipTarget::Relationship`
error[E0277]: the trait bound `FooTargetsFail: bevy_ecs::relationship::RelationshipTarget` is not satisfied
--> tests/ui/component_hook_relationship.rs:28:42
|
28 | #[relationship(relationship_target = FooTargetsFail)]
| ^^^^^^^^^^^^^^ the trait `bevy_ecs::relationship::RelationshipTarget` is not implemented for `FooTargetsFail`
|
= help: the following other types implement trait `bevy_ecs::relationship::RelationshipTarget`:
BarTargets
Children
FooTargets
note: required by a bound in `bevy_ecs::relationship::Relationship::RelationshipTarget`
--> $BEVY_ROOT/bevy_ecs/src/relationship/mod.rs:79:30
|
79 | type RelationshipTarget: RelationshipTarget<Relationship = Self>;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Relationship::RelationshipTarget`
error[E0277]: the trait bound `BarTargetOfFail: Relationship` is not satisfied
--> tests/ui/component_hook_relationship.rs:43:42
|
43 | #[relationship_target(relationship = BarTargetOfFail)]
| ^^^^^^^^^^^^^^^ the trait `Relationship` is not implemented for `BarTargetOfFail`
|
= help: the following other types implement trait `Relationship`:
BarTargetOf
ChildOf
FooTargetOf
note: required by a bound in `bevy_ecs::relationship::RelationshipTarget::Relationship`
--> $BEVY_ROOT/bevy_ecs/src/relationship/mod.rs:167:24
|
167 | type Relationship: Relationship<RelationshipTarget = Self>;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RelationshipTarget::Relationship`
error[E0277]: the trait bound `BarTargetsFail: bevy_ecs::relationship::RelationshipTarget` is not satisfied
--> tests/ui/component_hook_relationship.rs:58:42
|
58 | #[relationship(relationship_target = BarTargetsFail)]
| ^^^^^^^^^^^^^^ the trait `bevy_ecs::relationship::RelationshipTarget` is not implemented for `BarTargetsFail`
|
= help: the following other types implement trait `bevy_ecs::relationship::RelationshipTarget`:
BarTargets
Children
FooTargets
note: required by a bound in `bevy_ecs::relationship::Relationship::RelationshipTarget`
--> $BEVY_ROOT/bevy_ecs/src/relationship/mod.rs:79:30
|
79 | type RelationshipTarget: RelationshipTarget<Relationship = Self>;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Relationship::RelationshipTarget`
error: aborting due to 8 previous errors
For more information about this error, try `rustc --explain E0277`.

View File

@ -0,0 +1,14 @@
use bevy_ecs::prelude::*;
// the proc macro allows general paths, which means normal structs are also passing the basic
// parsing. This test makes sure that we don't accidentally allow structs as hooks through future
// changes.
//
// Currently the error is thrown in the generated code and not while executing the proc macro
// logic.
#[derive(Component)]
#[component(
on_add = Bar,
//~^ E0425
)]
pub struct FooWrongPath;

View File

@ -0,0 +1,9 @@
error[E0425]: cannot find value `Bar` in this scope
--> tests/ui/component_hook_struct_path.rs:11:14
|
11 | on_add = Bar,
| ^^^ not found in this scope
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0425`.

View File

@ -9,8 +9,8 @@ use syn::{
punctuated::Punctuated,
spanned::Spanned,
token::{Comma, Paren},
Data, DataStruct, DeriveInput, ExprClosure, ExprPath, Field, Fields, Ident, Index, LitStr,
Member, Path, Result, Token, Type, Visibility,
Data, DataStruct, DeriveInput, Expr, ExprCall, ExprClosure, ExprPath, Field, Fields, Ident,
Index, LitStr, Member, Path, Result, Token, Type, Visibility,
};
pub fn derive_event(input: TokenStream) -> TokenStream {
@ -80,8 +80,12 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
let storage = storage_path(&bevy_ecs_path, attrs.storage);
let on_add_path = attrs.on_add.map(|path| path.to_token_stream());
let on_remove_path = attrs.on_remove.map(|path| path.to_token_stream());
let on_add_path = attrs
.on_add
.map(|path| path.to_token_stream(&bevy_ecs_path));
let on_remove_path = attrs
.on_remove
.map(|path| path.to_token_stream(&bevy_ecs_path));
let on_insert_path = if relationship.is_some() {
if attrs.on_insert.is_some() {
@ -95,7 +99,9 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
Some(quote!(<Self as #bevy_ecs_path::relationship::Relationship>::on_insert))
} else {
attrs.on_insert.map(|path| path.to_token_stream())
attrs
.on_insert
.map(|path| path.to_token_stream(&bevy_ecs_path))
};
let on_replace_path = if relationship.is_some() {
@ -121,7 +127,9 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
Some(quote!(<Self as #bevy_ecs_path::relationship::RelationshipTarget>::on_replace))
} else {
attrs.on_replace.map(|path| path.to_token_stream())
attrs
.on_replace
.map(|path| path.to_token_stream(&bevy_ecs_path))
};
let on_despawn_path = if attrs
@ -139,7 +147,9 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
Some(quote!(<Self as #bevy_ecs_path::relationship::RelationshipTarget>::on_despawn))
} else {
attrs.on_despawn.map(|path| path.to_token_stream())
attrs
.on_despawn
.map(|path| path.to_token_stream(&bevy_ecs_path))
};
let on_add = hook_register_function_call(&bevy_ecs_path, quote! {on_add}, on_add_path);
@ -441,14 +451,64 @@ pub const ON_DESPAWN: &str = "on_despawn";
pub const IMMUTABLE: &str = "immutable";
/// All allowed attribute value expression kinds for component hooks
#[derive(Debug)]
enum HookAttributeKind {
/// expressions like function or struct names
///
/// structs will throw compile errors on the code generation so this is safe
Path(ExprPath),
/// function call like expressions
Call(ExprCall),
}
impl HookAttributeKind {
fn from_expr(value: Expr) -> Result<Self> {
match value {
Expr::Path(path) => Ok(HookAttributeKind::Path(path)),
Expr::Call(call) => Ok(HookAttributeKind::Call(call)),
// throw meaningful error on all other expressions
_ => Err(syn::Error::new(
value.span(),
[
"Not supported in this position, please use one of the following:",
"- path to function",
"- call to function yielding closure",
]
.join("\n"),
)),
}
}
fn to_token_stream(&self, bevy_ecs_path: &Path) -> TokenStream2 {
match self {
HookAttributeKind::Path(path) => path.to_token_stream(),
HookAttributeKind::Call(call) => {
quote!({
fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::component::HookContext) {
(#call)(world, ctx)
}
_internal_hook
})
}
}
}
}
impl Parse for HookAttributeKind {
fn parse(input: syn::parse::ParseStream) -> Result<Self> {
input.parse::<Expr>().and_then(Self::from_expr)
}
}
struct Attrs {
storage: StorageTy,
requires: Option<Punctuated<Require, Comma>>,
on_add: Option<ExprPath>,
on_insert: Option<ExprPath>,
on_replace: Option<ExprPath>,
on_remove: Option<ExprPath>,
on_despawn: Option<ExprPath>,
on_add: Option<HookAttributeKind>,
on_insert: Option<HookAttributeKind>,
on_replace: Option<HookAttributeKind>,
on_remove: Option<HookAttributeKind>,
on_despawn: Option<HookAttributeKind>,
relationship: Option<Relationship>,
relationship_target: Option<RelationshipTarget>,
immutable: bool,
@ -513,19 +573,19 @@ fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
};
Ok(())
} else if nested.path.is_ident(ON_ADD) {
attrs.on_add = Some(nested.value()?.parse::<ExprPath>()?);
attrs.on_add = Some(nested.value()?.parse::<HookAttributeKind>()?);
Ok(())
} else if nested.path.is_ident(ON_INSERT) {
attrs.on_insert = Some(nested.value()?.parse::<ExprPath>()?);
attrs.on_insert = Some(nested.value()?.parse::<HookAttributeKind>()?);
Ok(())
} else if nested.path.is_ident(ON_REPLACE) {
attrs.on_replace = Some(nested.value()?.parse::<ExprPath>()?);
attrs.on_replace = Some(nested.value()?.parse::<HookAttributeKind>()?);
Ok(())
} else if nested.path.is_ident(ON_REMOVE) {
attrs.on_remove = Some(nested.value()?.parse::<ExprPath>()?);
attrs.on_remove = Some(nested.value()?.parse::<HookAttributeKind>()?);
Ok(())
} else if nested.path.is_ident(ON_DESPAWN) {
attrs.on_despawn = Some(nested.value()?.parse::<ExprPath>()?);
attrs.on_despawn = Some(nested.value()?.parse::<HookAttributeKind>()?);
Ok(())
} else if nested.path.is_ident(IMMUTABLE) {
attrs.immutable = true;

View File

@ -319,6 +319,25 @@ use thiserror::Error;
/// }
/// ```
///
/// This also supports function calls that yield closures
///
/// ```
/// # use bevy_ecs::component::{Component, HookContext};
/// # use bevy_ecs::world::DeferredWorld;
/// #
/// #[derive(Component)]
/// #[component(on_add = my_msg_hook("hello"))]
/// #[component(on_despawn = my_msg_hook("yoink"))]
/// struct ComponentA;
///
/// // a hook closure generating function
/// fn my_msg_hook(message: &'static str) -> impl Fn(DeferredWorld, HookContext) {
/// move |_world, _ctx| {
/// println!("{message}");
/// }
/// }
/// ```
///
/// # Implementing the trait for foreign types
///
/// As a consequence of the [orphan rule], it is not possible to separate into two different crates the implementation of `Component` from the definition of a type.