Skip to content

Commit af865e7

Browse files
authored
bevy_reflect: Improve DynamicFunction ergonomics (#14201)
# Objective Many functions can be converted to `DynamicFunction` using `IntoFunction`. Unfortunately, we are limited by Rust itself and the implementations are far from exhaustive. For example, we can't convert functions with more than 16 arguments. Additionally, we can't handle returns with lifetimes not tied to the lifetime of the first argument. In such cases, users will have to create their `DynamicFunction` manually. Let's take the following function: ```rust fn get(index: usize, list: &Vec<String>) -> &String { &list[index] } ``` This function cannot be converted to a `DynamicFunction` via `IntoFunction` due to the lifetime of the return value being tied to the second argument. Therefore, we need to construct the `DynamicFunction` manually: ```rust DynamicFunction::new( |mut args, info| { let list = args .pop() .unwrap() .take_ref::<Vec<String>>(&info.args()[1])?; let index = args.pop().unwrap().take_owned::<usize>(&info.args()[0])?; Ok(Return::Ref(get(index, list))) }, FunctionInfo::new() .with_name("get") .with_args(vec![ ArgInfo::new::<usize>(0).with_name("index"), ArgInfo::new::<&Vec<String>>(1).with_name("list"), ]) .with_return_info(ReturnInfo::new::<&String>()), ); ``` While still a small and straightforward snippet, there's a decent amount going on here. There's a lot of room for improvements when it comes to ergonomics and readability. The goal of this PR is to address those issues. ## Solution Improve the ergonomics and readability of manually created `DynamicFunction`s. Some of the major changes: 1. Removed the need for `&ArgInfo` when reifying arguments (i.e. the `&info.args()[1]` calls) 2. Added additional `pop` methods on `ArgList` to handle both popping and casting 3. Added `take` methods on `ArgList` for taking the arguments out in order 4. Removed the need for `&FunctionInfo` in the internal closure (Change 1 made it no longer necessary) 5. Added methods to automatically handle generating `ArgInfo` and `ReturnInfo` With all these changes in place, we get something a lot nicer to both write and look at: ```rust DynamicFunction::new( |mut args| { let index = args.take::<usize>()?; let list = args.take::<&Vec<String>>()?; Ok(Return::Ref(get(index, list))) }, FunctionInfo::new() .with_name("get") .with_arg::<usize>("index") .with_arg::<&Vec<String>>("list") .with_return::<&String>(), ); ``` Alternatively, to rely on type inference for taking arguments, you could do: ```rust DynamicFunction::new( |mut args| { let index = args.take_owned()?; let list = args.take_ref()?; Ok(Return::Ref(get(index, list))) }, FunctionInfo::new() .with_name("get") .with_arg::<usize>("index") .with_arg::<&Vec<String>>("list") .with_return::<&String>(), ); ``` ## Testing You can test locally by running: ``` cargo test --package bevy_reflect ``` --- ## Changelog - Removed `&ArgInfo` argument from `FromArg::from_arg` trait method - Removed `&ArgInfo` argument from `Arg::take_***` methods - Added `ArgValue` - `Arg` is now a struct containing an `ArgValue` and an argument `index` - `Arg::take_***` methods now require `T` is also `TypePath` - Added `Arg::new`, `Arg::index`, `Arg::value`, `Arg::take_value`, and `Arg::take` methods - Replaced `ArgId` in `ArgError` with just the argument `index` - Added `ArgError::EmptyArgList` - Renamed `ArgList::push` to `ArgList::push_arg` - Added `ArgList::pop_arg`, `ArgList::pop_owned`, `ArgList::pop_ref`, and `ArgList::pop_mut` - Added `ArgList::take_arg`, `ArgList::take_owned`, `ArgList::take_ref`, `ArgList::take_mut`, and `ArgList::take` - `ArgList::pop` is now generic - Renamed `FunctionError::InvalidArgCount` to `FunctionError::ArgCountMismatch` - The closure given to `DynamicFunction::new` no longer has a `&FunctionInfo` argument - Added `FunctionInfo::with_arg` - Added `FunctionInfo::with_return` ## Internal Migration Guide > [!important] > Function reflection was introduced as part of the 0.15 dev cycle. This migration guide was written for developers relying on `main` during this cycle, and is not a breaking change coming from 0.14. * The `FromArg::from_arg` trait method and the `Arg::take_***` methods no longer take a `&ArgInfo` argument. * What used to be `Arg` is now `ArgValue`. `Arg` is now a struct which contains an `ArgValue`. * `Arg::take_***` methods now require `T` is also `TypePath` * Instances of `id: ArgId` in `ArgError` have been replaced with `index: usize` * `ArgList::push` is now `ArgList::push_arg`. It also takes the new `ArgValue` type. * `ArgList::pop` has become `ArgList::pop_arg` and now returns `ArgValue`. `Arg::pop` now takes a generic type and downcasts to that type. It's recommended to use `ArgList::take` and friends instead since they allow removing the arguments from the list in the order they were pushed (rather than reverse order). * `FunctionError::InvalidArgCount` is now `FunctionError::ArgCountMismatch` * The closure given to `DynamicFunction::new` no longer has a `&FunctionInfo` argument. This argument can be removed.
1 parent 0e13b1c commit af865e7

File tree

20 files changed

+764
-317
lines changed

20 files changed

+764
-317
lines changed

crates/bevy_reflect/derive/src/impls/func/from_arg.rs

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,23 @@ pub(crate) fn impl_from_arg(
1515

1616
quote! {
1717
impl #impl_generics #bevy_reflect::func::args::FromArg for #type_path #ty_generics #where_reflect_clause {
18-
type Item<'from_arg> = #type_path #ty_generics;
19-
fn from_arg<'from_arg>(
20-
arg: #bevy_reflect::func::args::Arg<'from_arg>,
21-
info: &#bevy_reflect::func::args::ArgInfo,
22-
) -> #FQResult<Self::Item<'from_arg>, #bevy_reflect::func::args::ArgError> {
23-
arg.take_owned(info)
18+
type This<'from_arg> = #type_path #ty_generics;
19+
fn from_arg(arg: #bevy_reflect::func::args::Arg) -> #FQResult<Self::This<'_>, #bevy_reflect::func::args::ArgError> {
20+
arg.take_owned()
2421
}
2522
}
2623

2724
impl #impl_generics #bevy_reflect::func::args::FromArg for &'static #type_path #ty_generics #where_reflect_clause {
28-
type Item<'from_arg> = &'from_arg #type_path #ty_generics;
29-
fn from_arg<'from_arg>(
30-
arg: #bevy_reflect::func::args::Arg<'from_arg>,
31-
info: &#bevy_reflect::func::args::ArgInfo,
32-
) -> #FQResult<Self::Item<'from_arg>, #bevy_reflect::func::args::ArgError> {
33-
arg.take_ref(info)
25+
type This<'from_arg> = &'from_arg #type_path #ty_generics;
26+
fn from_arg(arg: #bevy_reflect::func::args::Arg) -> #FQResult<Self::This<'_>, #bevy_reflect::func::args::ArgError> {
27+
arg.take_ref()
3428
}
3529
}
3630

3731
impl #impl_generics #bevy_reflect::func::args::FromArg for &'static mut #type_path #ty_generics #where_reflect_clause {
38-
type Item<'from_arg> = &'from_arg mut #type_path #ty_generics;
39-
fn from_arg<'from_arg>(
40-
arg: #bevy_reflect::func::args::Arg<'from_arg>,
41-
info: &#bevy_reflect::func::args::ArgInfo,
42-
) -> #FQResult<Self::Item<'from_arg>, #bevy_reflect::func::args::ArgError> {
43-
arg.take_mut(info)
32+
type This<'from_arg> = &'from_arg mut #type_path #ty_generics;
33+
fn from_arg(arg: #bevy_reflect::func::args::Arg) -> #FQResult<Self::This<'_>, #bevy_reflect::func::args::ArgError> {
34+
arg.take_mut()
4435
}
4536
}
4637
}
Lines changed: 160 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,200 @@
1-
use crate::func::args::{ArgError, ArgInfo, Ownership};
2-
use crate::Reflect;
1+
use crate::func::args::{ArgError, FromArg, Ownership};
2+
use crate::{Reflect, TypePath};
3+
use std::ops::Deref;
34

4-
/// Represents an argument that can be passed to a [`DynamicFunction`] or [`DynamicClosure`].
5+
/// Represents an argument that can be passed to a [`DynamicFunction`], [`DynamicClosure`],
6+
/// or [`DynamicClosureMut`].
57
///
68
/// [`DynamicFunction`]: crate::func::DynamicFunction
79
/// [`DynamicClosure`]: crate::func::DynamicClosure
10+
/// [`DynamicClosureMut`]: crate::func::DynamicClosureMut
811
#[derive(Debug)]
9-
pub enum Arg<'a> {
10-
Owned(Box<dyn Reflect>),
11-
Ref(&'a dyn Reflect),
12-
Mut(&'a mut dyn Reflect),
12+
pub struct Arg<'a> {
13+
index: usize,
14+
value: ArgValue<'a>,
1315
}
1416

1517
impl<'a> Arg<'a> {
16-
/// Returns `Ok(T)` if the argument is [`Arg::Owned`].
17-
pub fn take_owned<T: Reflect>(self, info: &ArgInfo) -> Result<T, ArgError> {
18-
match self {
19-
Arg::Owned(arg) => arg.take().map_err(|arg| ArgError::UnexpectedType {
20-
id: info.id().clone(),
21-
expected: ::std::borrow::Cow::Borrowed(info.type_path()),
22-
received: ::std::borrow::Cow::Owned(arg.reflect_type_path().to_string()),
18+
/// Create a new [`Arg`] with the given index and value.
19+
pub fn new(index: usize, value: ArgValue<'a>) -> Self {
20+
Self { index, value }
21+
}
22+
23+
/// The index of the argument.
24+
pub fn index(&self) -> usize {
25+
self.index
26+
}
27+
28+
/// Set the index of the argument.
29+
pub(crate) fn set_index(&mut self, index: usize) {
30+
self.index = index;
31+
}
32+
33+
/// The value of the argument.
34+
pub fn value(&self) -> &ArgValue<'a> {
35+
&self.value
36+
}
37+
38+
/// Take the value of the argument.
39+
pub fn take_value(self) -> ArgValue<'a> {
40+
self.value
41+
}
42+
43+
/// Take the value of the argument and attempt to convert it to a concrete value, `T`.
44+
///
45+
/// This is a convenience method for calling [`FromArg::from_arg`] on the argument.
46+
///
47+
/// # Example
48+
///
49+
/// ```
50+
/// # use bevy_reflect::func::ArgList;
51+
/// let a = 1u32;
52+
/// let b = 2u32;
53+
/// let mut c = 3u32;
54+
/// let mut args = ArgList::new().push_owned(a).push_ref(&b).push_mut(&mut c);
55+
///
56+
/// let a = args.take::<u32>().unwrap();
57+
/// assert_eq!(a, 1);
58+
///
59+
/// let b = args.take::<&u32>().unwrap();
60+
/// assert_eq!(*b, 2);
61+
///
62+
/// let c = args.take::<&mut u32>().unwrap();
63+
/// assert_eq!(*c, 3);
64+
/// ```
65+
pub fn take<T: FromArg>(self) -> Result<T::This<'a>, ArgError> {
66+
T::from_arg(self)
67+
}
68+
69+
/// Returns `Ok(T)` if the argument is [`ArgValue::Owned`].
70+
///
71+
/// If the argument is not owned, returns an error.
72+
///
73+
/// It's generally preferred to use [`Self::take`] instead of this method.
74+
///
75+
/// # Example
76+
///
77+
/// ```
78+
/// # use bevy_reflect::func::ArgList;
79+
/// let value = 123u32;
80+
/// let mut args = ArgList::new().push_owned(value);
81+
/// let value = args.take_owned::<u32>().unwrap();
82+
/// assert_eq!(value, 123);
83+
/// ```
84+
pub fn take_owned<T: Reflect + TypePath>(self) -> Result<T, ArgError> {
85+
match self.value {
86+
ArgValue::Owned(arg) => arg.take().map_err(|arg| ArgError::UnexpectedType {
87+
index: self.index,
88+
expected: std::borrow::Cow::Borrowed(T::type_path()),
89+
received: std::borrow::Cow::Owned(arg.reflect_type_path().to_string()),
2390
}),
24-
Arg::Ref(_) => Err(ArgError::InvalidOwnership {
25-
id: info.id().clone(),
91+
ArgValue::Ref(_) => Err(ArgError::InvalidOwnership {
92+
index: self.index,
2693
expected: Ownership::Owned,
2794
received: Ownership::Ref,
2895
}),
29-
Arg::Mut(_) => Err(ArgError::InvalidOwnership {
30-
id: info.id().clone(),
96+
ArgValue::Mut(_) => Err(ArgError::InvalidOwnership {
97+
index: self.index,
3198
expected: Ownership::Owned,
3299
received: Ownership::Mut,
33100
}),
34101
}
35102
}
36103

37-
/// Returns `Ok(&T)` if the argument is [`Arg::Ref`].
38-
pub fn take_ref<T: Reflect>(self, info: &ArgInfo) -> Result<&'a T, ArgError> {
39-
match self {
40-
Arg::Owned(_) => Err(ArgError::InvalidOwnership {
41-
id: info.id().clone(),
104+
/// Returns `Ok(&T)` if the argument is [`ArgValue::Ref`].
105+
///
106+
/// If the argument is not a reference, returns an error.
107+
///
108+
/// It's generally preferred to use [`Self::take`] instead of this method.
109+
///
110+
/// # Example
111+
///
112+
/// ```
113+
/// # use bevy_reflect::func::ArgList;
114+
/// let value = 123u32;
115+
/// let mut args = ArgList::new().push_ref(&value);
116+
/// let value = args.take_ref::<u32>().unwrap();
117+
/// assert_eq!(*value, 123);
118+
/// ```
119+
pub fn take_ref<T: Reflect + TypePath>(self) -> Result<&'a T, ArgError> {
120+
match self.value {
121+
ArgValue::Owned(_) => Err(ArgError::InvalidOwnership {
122+
index: self.index,
42123
expected: Ownership::Ref,
43124
received: Ownership::Owned,
44125
}),
45-
Arg::Ref(arg) => Ok(arg.downcast_ref().ok_or_else(|| ArgError::UnexpectedType {
46-
id: info.id().clone(),
47-
expected: ::std::borrow::Cow::Borrowed(info.type_path()),
48-
received: ::std::borrow::Cow::Owned(arg.reflect_type_path().to_string()),
49-
})?),
50-
Arg::Mut(_) => Err(ArgError::InvalidOwnership {
51-
id: info.id().clone(),
126+
ArgValue::Ref(arg) => {
127+
Ok(arg.downcast_ref().ok_or_else(|| ArgError::UnexpectedType {
128+
index: self.index,
129+
expected: std::borrow::Cow::Borrowed(T::type_path()),
130+
received: std::borrow::Cow::Owned(arg.reflect_type_path().to_string()),
131+
})?)
132+
}
133+
ArgValue::Mut(_) => Err(ArgError::InvalidOwnership {
134+
index: self.index,
52135
expected: Ownership::Ref,
53136
received: Ownership::Mut,
54137
}),
55138
}
56139
}
57140

58-
/// Returns `Ok(&mut T)` if the argument is [`Arg::Mut`].
59-
pub fn take_mut<T: Reflect>(self, info: &ArgInfo) -> Result<&'a mut T, ArgError> {
60-
match self {
61-
Arg::Owned(_) => Err(ArgError::InvalidOwnership {
62-
id: info.id().clone(),
141+
/// Returns `Ok(&mut T)` if the argument is [`ArgValue::Mut`].
142+
///
143+
/// If the argument is not a mutable reference, returns an error.
144+
///
145+
/// It's generally preferred to use [`Self::take`] instead of this method.
146+
///
147+
/// # Example
148+
///
149+
/// ```
150+
/// # use bevy_reflect::func::ArgList;
151+
/// let mut value = 123u32;
152+
/// let mut args = ArgList::new().push_mut(&mut value);
153+
/// let value = args.take_mut::<u32>().unwrap();
154+
/// assert_eq!(*value, 123);
155+
/// ```
156+
pub fn take_mut<T: Reflect + TypePath>(self) -> Result<&'a mut T, ArgError> {
157+
match self.value {
158+
ArgValue::Owned(_) => Err(ArgError::InvalidOwnership {
159+
index: self.index,
63160
expected: Ownership::Mut,
64161
received: Ownership::Owned,
65162
}),
66-
Arg::Ref(_) => Err(ArgError::InvalidOwnership {
67-
id: info.id().clone(),
163+
ArgValue::Ref(_) => Err(ArgError::InvalidOwnership {
164+
index: self.index,
68165
expected: Ownership::Mut,
69166
received: Ownership::Ref,
70167
}),
71-
Arg::Mut(arg) => {
72-
let received = ::std::borrow::Cow::Owned(arg.reflect_type_path().to_string());
168+
ArgValue::Mut(arg) => {
169+
let received = std::borrow::Cow::Owned(arg.reflect_type_path().to_string());
73170
Ok(arg.downcast_mut().ok_or_else(|| ArgError::UnexpectedType {
74-
id: info.id().clone(),
75-
expected: ::std::borrow::Cow::Borrowed(info.type_path()),
171+
index: self.index,
172+
expected: std::borrow::Cow::Borrowed(T::type_path()),
76173
received,
77174
})?)
78175
}
79176
}
80177
}
81178
}
179+
180+
/// Represents an argument that can be passed to a [`DynamicFunction`].
181+
///
182+
/// [`DynamicFunction`]: crate::func::DynamicFunction
183+
#[derive(Debug)]
184+
pub enum ArgValue<'a> {
185+
Owned(Box<dyn Reflect>),
186+
Ref(&'a dyn Reflect),
187+
Mut(&'a mut dyn Reflect),
188+
}
189+
190+
impl<'a> Deref for ArgValue<'a> {
191+
type Target = dyn Reflect;
192+
193+
fn deref(&self) -> &Self::Target {
194+
match self {
195+
ArgValue::Owned(arg) => arg.as_ref(),
196+
ArgValue::Ref(arg) => *arg,
197+
ArgValue::Mut(arg) => *arg,
198+
}
199+
}
200+
}

crates/bevy_reflect/src/func/args/error.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,30 @@ use alloc::borrow::Cow;
22

33
use thiserror::Error;
44

5-
use crate::func::args::{ArgId, Ownership};
5+
use crate::func::args::Ownership;
66

77
/// An error that occurs when converting an [argument].
88
///
9-
/// [argument]: crate::func::Arg
9+
/// [argument]: crate::func::args::Arg
1010
#[derive(Debug, Error, PartialEq)]
1111
pub enum ArgError {
1212
/// The argument is not the expected type.
13-
#[error("expected `{expected}` but received `{received}` (@ {id:?})")]
13+
#[error("expected `{expected}` but received `{received}` (@ argument index {index})")]
1414
UnexpectedType {
15-
id: ArgId,
15+
index: usize,
1616
expected: Cow<'static, str>,
1717
received: Cow<'static, str>,
1818
},
1919
/// The argument has the wrong ownership.
20-
#[error("expected {expected} value but received {received} value (@ {id:?})")]
20+
#[error("expected {expected} value but received {received} value (@ argument index {index})")]
2121
InvalidOwnership {
22-
id: ArgId,
22+
index: usize,
2323
expected: Ownership,
2424
received: Ownership,
2525
},
26+
/// Occurs when attempting to access an argument from an empty [`ArgList`].
27+
///
28+
/// [`ArgList`]: crate::func::args::ArgList
29+
#[error("expected an argument but received none")]
30+
EmptyArgList,
2631
}

0 commit comments

Comments
 (0)