#[reflect_impl]
This attribute can be attached to an impl block to easily generate a structure containing the DynamicFunction
equivalent of items within that block.
impl MyType {/* ... */}
) and trait impl blocks (e.g. impl MyTrait for MyType {/* ... */}
). There may be zero or more impl blocks for any given type.#[reflect_impl]
, the latter two are ignored. These may also be referred to as "members" or simply "items" depending on the context.self
as the receiver (e.g. &self
, &mut self
, self: Box<Self>
, etc.). Such functions are callable as my_type.my_method()
.self
and must be called as MyType::my_function()
.MyType::CONSTANT
.#[reflect]
attribute, allowing it to be accessible via reflection.fn foo<T>() {/* ... */}
would have to monomorphized down to foo::<u32>
in order to be reflected.This attribute currently only supports the following impl items:
All tracked associated constants must be reflectable. Additionally, all arguments and return types for tracked associated functions and methods must also be reflectable.
All tracked associated types must have a 'static
lifetime.
The impl Trait
type is supported in argument position using the as
attribute. For return types, impl Trait
only works if given the necessary bounds for return type function reflection (currently Reflect
, TypePath
, GetOwnership
, and IntoReturn
).
There are two kinds of generics that this macro is aware of: impl generics and item generics.
Impl generics are the generics that are defined on the impl definition:
impl<'a, T: Debug> MyGenericType<'a, T> {
// ...
}
Impl generics don't need to be monomorphized for the macro. That job is left to the consumer of the generated struct. However, generics that are used by one of the tracked items may require additional reflection-only bounds.
These bounds do not affect the impl block directly, but instead are used in the generated trait implementations to ensure compatability with reflection.
See the where
attribute for details.
Item generics, on the other hand, are generics defined on the individual items:
impl SomeTrait for MyType {
type Foo<T> = Vec<T>;
fn bar<T: Debug>() {
// ...
}
fn baz<'a, const N: usize>(&self, value: N) {
// ...
}
}
Due to limitations in Rust, these kinds of generics need to be monomorphized. This is done with the as
attribute.
Lifetimes are effectively ignored in almost all cases.
The only time lifetimes tend to really matter is when the return type of an associated function or method is a reference type. In such cases, its lifetime must be tied to the first argument in the function's signature (most often &self
/&mut self
).
It's also important to remember that reflection requires a 'static
lifetime. For example:
fn foo(&self, new_value: Foo) {/* ... */}
In the code above, &self
implies Self: 'static
and allows any lifetime for the reference (i.e. &
), and Foo
also implies Foo: 'static
.
Again, this is due to the fact that Reflect
requires 'static
.
All #[reflect_impl]
attributes require exactly one argument: the identifier of the generated struct.
For example, #[reflect_impl(FooImpl)]
would generate a struct called FooImpl
in the same scope as the impl block.
This macro comes with a set of quasi-helper attributes. These are attributes that are used to configure the generated output or behavior of the macro itself.
All helper attributes are defined with #[reflect]
as the base attribute.
vis
vis = VISIBILITY
This attribute controls the visibility of the generated struct. For example, to make the generated struct publicly accessible, you would write #[reflect(vis = pub)]
.
doc
doc = STRING_LITERAL | BOOLEAN
With the documentation
feature enabled, impl blocks will store their doc comments on the generated struct.
This attribute can be used to override that behavior by providing a custom doc string.
/// Regular doc comment, not picked up by reflection
/// thanks to the custom `doc` attribute.
#[reflect(doc = "Custom reflection-only doc comment!")]
impl Foo {
// ...
}
You can also completely opt-out of documentation for the block by specifying doc = false
.
/// Regular doc comment, not picked up by reflection
/// thanks to the custom `doc` attribute.
#[reflect(doc = false)]
impl Foo {
// ...
}
where
where WHERE_CLAUSE_PREDICATE
Generic types that are used within tracked items may need additional bounds in order to be reflectable. Most often, they will need to either be Reflect
or FromReflect
.
In such cases, it's required that those bounds be added manually for the generated output. This attribute allows that to be done without having to modify the impl block itself.
#[reflect(where T: Reflect, U: FromReflect)]
impl<T, U> MyType<T, U> {
#[reflect]
fn foo() -> T {/* ... */}
#[reflect]
fn bar() -> Vec<U> {/* ... */}
}
By default, items that are not tagged with a #[reflect]
attribute will not be included in the generated output. This makes the feature purely opt-in, allowing reflectable items to coexist in the same impl block as non-reflectable items.
name
name = STRING_LITERAL
This attribute defines the name of the DynamicFunction
associated with this item.
For example the following method will generate a DynamicFunction
with the name "get"
.
#[reflect(name = "get")]
fn value(&self) -> f32 {
self.value
}
By contrast, items that do not define a name will be named by their fully-qualified path as returned by std::any::type_name_of_val
. In the example above, this might look something like "my_crate::MyType::value"
.
If uniqueness is desired, it's recommended to not define a custom name for your item.
doc
doc = STRING_LITERAL | BOOLEAN
With the documentation
feature enabled, items will keep store their doc comments on their associated DynamicFunction
.
This attribute can be used to override that behavior by providing a custom doc string.
/// Regular doc comment, not picked up by reflection
/// thanks to the custom `doc` attribute.
#[reflect(doc = "Custom reflection-only doc comment!")]
fn value(&self) -> f32 {
self.value
}
You can also completely opt-out of documentation for an item by specifying doc = false
.
/// Regular doc comment, not picked up by reflection
/// thanks to the custom `doc` attribute.
#[reflect(doc = false)]
fn value(&self) -> f32 {
self.value
}
as
as FUNCTION
| as FN_POINTER
impl Trait
argumentsGeneric items and impl Trait
arguments need to be manually monomorphized in order to be reflected. This means that the concrete types must be provided manually by the attribute.
For items that do not contain impl Trait
arguments, you can do this by specifying the generics of you function:
#[reflect(as Self::parse_string::<i32, f32>)]
fn parse_string<In, Out>(&mut self, input: In, value: String, options: &ParseOptions) -> Out {
// ...
}
If the item does contain impl Trait
arguments, then you must instead provide a valid function pointer to define the monomorphization of your item:
#[reflect(as fn(&mut Self, i32, String, &ParseOptions) -> f32]
fn parse_with<In, Out>(&mut self, input: In, value: impl ToString, options: &ParseOptions) -> Out {
// ...
}
As a tip, you can optionally replace non-generic types with _
to allow the compiler to infer them since they will always be the same type.
#[reflect(as fn(_, i32, String, _) -> f32]
fn parse_with<In, Out>(&mut self, input: In, value: impl ToString, options: &ParseOptions) -> Out {
// ...
}
Additionally, you can provide argument names to improve readability:
#[reflect(as fn(_, input: i32, value: String, _) -> f32]
fn parse_with<In, Out>(&mut self, input: In, value: impl ToString, options: &ParseOptions) -> Out {
// ...
}
Also note that when coercing to a function pointer, the default name of the item becomes None
(since function pointers are considered anonymous) and can't be registered. Therefore, it's recommended to also ensure the item has a custom name:
fn #[reflect(name = "parse_with", as fn(_, input: i32, value: String, _) -> f32]
fn parse_with<In, Out>(&mut self, input: In, value: impl ToString, options: &ParseOptions) -> Out {
// ...
}
This attribute can be paired with the variant
and overload_with
attributes to define multiple monomorphizations or overloads of an item.
overload_with
overload_with(FUNC_0, ...FUNCTION_N )
where FUNC
can either be FN_POINTER
or FUNCTION [as FN_POINTER]
This attribute allows a method or associated function to be overloaded with one or more items of the same kind.
Any provided function must must have a different argument signature than the original item and all other provided overloads. Duplicate argument signatures cannot be detected at compile-time and will instead result in a runtime error so it is important to ensure the argument signatures all differ.
If the item is generic or contains impl Trait
arguments, a valid monomorphization must be provided.
#[reflect(
as fn(u32) -> bool,
overload_with(
// Referencing another item:
Self::convert_empty,
// Using monomorphized function pointers for this item:
fn(u8) -> bool,
fn(u16) -> bool,
// Using a fully-qualified monomorphization of this item:
Self::convert::<u64, bool>,
// Referencing another item that needs to be monomorphized:
Self::convert_string as fn(String) -> bool,
// Note: uncommenting this line would cause a runtime error
// since its argument signature matches `Self::convert_empty`:
// Self::convert::<(), ()>
)
)]
fn convert<In, Out>(input: In) -> Out {
// ...
}
fn convert_string<Out>(input: impl ToString) -> Out {
// ...
}
fn convert_empty(input: ()) {}
variant
variant(name = STRING_LITERAL, ...ITEM_ATTRIBUTES)
This attribute allows multiple versions of a single item to be created. Any other item attribute defined within this one will be passed on to the associated DynamicFunction
.
When there are two or more instances of this attribute on an item, a name
attribute must be defined within each.
Additionally, any attributes defined outside this attribute will automatically be defaulted to if not provided within the variant
.
#[reflect(doc = "This doc comment is used by `foo` and `bar`.")]
#[reflect(variant(name = "foo"))]
#[reflect(variant(name = "bar"))]
#[reflect(variant(name = "baz", doc = "Custom `baz` doc comment!"))]
fn value(&self) -> f32 {
self.value
}
This is attribute is commonly used with the as
attribute to define mutliple instances of a generic item.
@TYPE
Custom attributes can be assigned to certain items similar to how the Reflect
derive macro handles it.
Custom attributes are essentially just types that can be accessed via the item's type information (e.g. FunctionInfo
).
#[reflect(@RangeInclusive::<i32>::new(0, 100))]
fn set(&mut self, value: i32) {
// ...
}
/// My impl block!
#[reflect_impl(MyTypeImpl)]
#[reflect(where T: Reflect)]
impl<T: Debug> MyType<T> {
#[reflect]
const CONSTANT: bool = true;
/// Create a new instance of `MyType`.
#[reflect]
fn new() -> Self {/* ... */}
#[reflect(doc = "Returns the value.")]
#[reflect(variant(name = "get"))]
#[reflect(variant(name = "value"))]
fn value(&self) -> &T {/* ... */}
#[reflect(name = "set")]
fn set_value(&mut self, value: T) {/* ... */}
fn is_set(&self) -> bool {/* ... */}
#[reflect(variant(name = "contains_u32", as fn(_, u32) -> _))]
#[reflect(variant(name = "contains_i32", as fn(_, i32) -> _))]
#[reflect(variant(
name = "contains_signed",
as fn(_, i8) -> bool,
overload_with(
fn(_, i16) -> bool,
fn(_, i32) -> bool,
fn(_, i64) -> bool,
fn(_, i128) -> bool,
)
))]
#[reflect(variant(
name = "contains_unsigned",
as Self::contains::<u8>,
overload_with(
Self::contains::<u16>,
Self::contains::<u32>,
Self::contains::<u64>,
Self::contains::<u128>,
)
))]
fn contains<U>(&self, value: U) -> bool {/* ... */}
#[reflect(overload_with(Self::increment_by))]
fn increment(&mut self) {/* ... */}
fn increment_by(&mut self, size: usize) {/* ... */}
#[reflect(as fn(_, i32) -> [_; N])]
fn print<const N: usize>(&self, value: impl Debug) -> [String; N] {/* ... */}
}
The following sections contain samples of what the generated output might look like. Types and bounds have been removed and/or simplified for brevity.
struct MyTypeImpl<T> {
CONSTANT: DynamicFunction<'static>,
new: DynamicFunction<'static>,
value: [DynamicFunction<'static>; 2],
set_value: DynamicFunction<'static>,
contains: [DynamicFunction<'static>; 4],
increment: DynamicFunction<'static>,
print: DynamicFunction<'static>,
_phantom: PhantomData<fn() -> (T,)>,
}
impl<T: Debug> MyTypeImpl<T>
where
T: Reflect,
{
pub fn CONSTANT(&self) -> &DynamicFunction<'static> {
&self.CONSTANT
}
pub fn new(&self) -> &DynamicFunction<'static> {
&self.new
}
pub fn value(&self, variant: &str) -> Result<&DynamicFunction<'static>, UnknownFunctionVariant> {
match variant {
"get" => Ok(&self.value[0]),
"value" => Ok(&self.value[1]),
_ => Err(UnknownFunctionVariant(err.to_string()))
}
}
pub fn set_value(&self) -> &DynamicFunction<'static> {
&self.set_value
}
pub fn contains(&self, variant: &str) -> Result<&DynamicFunction<'static>, UnknownFunctionVariant> {
match variant {
"contains_u32" => Ok(&self.contains[0]),
"contains_i32" => Ok(&self.contains[1]),
"contains_signed" => Ok(&self.contains[2]),
"contains_unsigned" => Ok(&self.contains[3]),
_ => Err(UnknownFunctionVariant(err.to_string()))
}
}
pub fn increment(&self) -> &DynamicFunction<'static> {
&self.increment
}
pub fn print(&self) -> &DynamicFunction<'static> {
&self.print
}
}
Default
Impl
impl<T: Debug> Default for MyTypeImpl<T>
where
T: Reflect,
{
fn default() -> Self {
CONSTANT: (|| <MyType<T>>::CONSTANT).into_function(),
new: <MyType<T>>::new.into_function()
.with_doc("Create a new instance of `MyType`."),
value: [
<MyType<T>>::value.into_function()
.with_name("get")
.with_doc("Returns the value."),
<MyType<T>>::value.into_function()
.with_name("value")
.with_doc("Returns the value."),
],
set_value: <MyType<T>>::set_value.into_function()
.with_name("set"),
contains: [
(<MyType<T>>::contains as fn(_, u32) -> _).into_function()
.with_name("contains_u32"),
(<MyType<T>>::contains as fn(_, i32) -> _).into_function()
.with_name("contains_i32"),
(<MyType<T>>::contains as fn(_, i8) -> _).into_function()
.with_overload((<MyType<T>>::contains as fn(_, i16) -> _))
.with_overload((<MyType<T>>::contains as fn(_, i32) -> _))
.with_overload((<MyType<T>>::contains as fn(_, i64) -> _))
.with_overload((<MyType<T>>::contains as fn(_, i128) -> _))
.with_name("contains_signed"),
<MyType<T>>::contains::<u8>.into_function()
.with_overload(<MyType<T>>::contains::<u16>)
.with_overload(<MyType<T>>::contains::<u32>)
.with_overload(<MyType<T>>::contains::<u64>)
.with_overload(<MyType<T>>::contains::<u128>)
.with_name("contains_unsigned"),,
],
increment: <MyType<T>>::increment.into_function()
.with_overload(<MyType<T>>::increment_by),
print: (<MyType<T>>::print as fn(_, i32) -> [_; N]).into_function(),
_phantom: Default::default(),
}
}
RegisterFunctions
Impl
impl<T: Debug> RegisterFunctions for MyTypeImpl<T>
where
T: Reflect,
{
fn register_functions(registry: &mut FunctionRegistry) -> Result<(), FunctionRegistrationError> {
let this = Self::default();
registry.register(this.CONSTANT)?;
registry.register(this.new)?
{
let [value_0, value_1] = this.value;
registry.register(value_0)?;
registry.register(value_1)?;
}
registry.register(this.set_value)?
{
let [contains_0, contains_1, contains_2, contains_3] = this.contains;
registry.register(contains_0)?;
registry.register(contains_1)?;
registry.register(contains_2)?;
registry.register(contains_3)?;
}
registry.register(this.increment)?;
// Not possible without custom name:
// registry.register(this.print)?;
Ok(())
}
}
Impl
Impl
impl<T: Debug> Impl for MyTypeImpl<T>
where
T: Reflect,
{
type Type = MyType<T>;
// Note that `print` is inaccessible by name since
// it was cast to a function pointer and not given
// an explicit name
fn get(&self, name: &str) -> Option<&DynamicFunction<'static>> {
match name {
// === Custom Names === //
"get" => Some(&self.value[0]),
"value" => Some(&self.value[1]),
"set" => Some(&self.set_value),
"contains_u32" => Some(&self.contains[0]),
"contains_i32" => Some(&self.contains[1]),
"contains_signed" => Some(&self.contains[2]),
"contains_unsigned" => Some(&self.contains[3]),
// === Default Short Names (requires namespacing) === //
// "new" => Some(&self.new),
// "increment" => Some(&self.increment),
// "print" => Some(&self.print),
// "CONSTANT" => Some(&self.constant),
// === Default Long Names === //
name if name == std::any::type_name_of_val(&Self::Type::new) => Some(&self.new),
name if name == std::any::type_name_of_val(&Self::Type::increment) => Some(&self.increment),
name if match_const(std::any::type_name::<Self::Type>(), "CONSTANT", name) => Some(&self.CONSTANT),
// === Default === //
_ => None
}
}
fn assoc_constants(&self) -> impl Iterator<Item=&DynamicFunction<'static>> {
[&self.CONSTANT].into_iter()
}
fn assoc_functions(&self) -> impl Iterator<Item=&DynamicFunction<'static>> {
[&self.new].into_iter()
}
fn methods(&self) -> impl Iterator<Item=&DynamicFunction<'static>> {
[
&self.value[0],
&self.value[1],
&self.set_value,
&self.contains[0],
&self.contains[1],
&self.contains[2],
&self.contains[3],
&self.increment,
&self.print,
].into_iter()
}
fn documentation(&self) -> Option<Cow<'static, str>> {
Some(Cow::Borrowed("My impl block!"))
}
}
Obviously, there's a lot to bikeshed. I don't think we should get hung up on naming too much—these can always be changed—but there's also no harm in giving some space to share opinions.
Right now we only accept string literals. Should we include syntax for customizing the string?
For example, name = "{Self}::foo"
might generate a name like "my_crate::MyType::<u32>::foo"
.
Or maybe name("{}::foo", std::any::type_name::<Self>())
to give even more control? The contents would just be pasted verbatim inside a format!
.
Not every attribute needs to be part of the initial MVP. But it's good to think about as many as we might want to add so that we can set ourselves up for when we do eventually get to that point.
If there are any attributes we should add (or any we should remove), feel free to share!
This macro should be completely usable on its own.
It generates a struct that provides all the immediate functionality needed by users.
Eventually, though, we'd like to associate the generated impl struct on the actual type. For example:
#[derive(Reflect)]
#[reflect(with_impls(MyTypeImpl))]
struct MyType<T> {/* ... */}
This would have two effects. Firstly, it would enable users to register all impls for a type just like they can with type data. We would just need to implement the RegisterFunctions
trait on the type as well.
Secondly, it would allow us to add methods to Struct
, TupleStruct
, etc. for accessing these impl items.
Additionally, if we ever add automatic registration, we can likely do this all automatically (possibly without even needing to generate a separate impl struct).
A big feature that has yet to be added to function reflection is namespacing. How we handle creating, managing, and registering namespaces will likely alter the behavior of this attribute.
For example, rather than using type_name
as the default name, we might instead opt to use the namespaced name as the default.
Such decisions can be made once namespacing is figured out, but it's important we have the thought in the back of our minds as we move forward with this feature.
We can also make this attribute work for remote types by simply consuming the impl block.
This allows a user to copy+paste the impl block from the third-party's crate and automatically generate an impl struct for it. This can be used to reflect impls that aren't reflected or override already-reflected impls with your own. Of course, this requires that the items themselves are reflectable.
This won't be part of the initial MVP, but is something to be mindful of when working on this macro. Namely, we want to avoid backing ourselves into a corner with regards to the orphan rule. So we should ensure all generated output can easily work with external types/traits as well.