I have 2 macros. The first is special_trait
, an attribute macro to be used on trait declarations, and the second, useful_macro
is used with such traits.
That is, the user code would write:
#[special_trait]
pub trait MyTrait{}
// meanwhile, in a different file...
use some_mod::MyTrait;
useful_macro!(MyTrait);
Now, the special_trait
macro needs to assign some metadata to MyTrait
in a way that useful_macro
can use it.
Is this possible and how?
It occurred to me that I could require all user code to specify the full path to the trait, instead of relying on use
:
#[special_trait]
pub trait MyTrait{}
// meanwhile, in a different file...
useful_macro!(some_mod::MyTrait);
Then, special_trait
merely needs to define a pub const MyTrait_METADATA: i32 = 42
, and useful_macro
can find the path to this metadata const since it has the full some_mod::MyTrait
path, and only the last segment needs to be changed: some_mod::MyTrait_METADATA
.
However, forbidding use
and requiring the full path seems mean, and I don't want to do that if there is a better way.
Can I associate a metadata const with a trait in a way that any macro having "access" to the trait also can find the metadata?
Rocket v4 has the same problem:
When a route is declared inside a module other than the root, you may find yourself with unexpected errors when mounting:
mod other { #[get("/world")] pub fn world() -> &'static str { "Hello, world!" } } #[get("/hello")] pub fn hello() -> &'static str { "Hello, outside world!" } use other::world; fn main() { // error[E0425]: cannot find value `static_rocket_route_info_for_world` > in this scope rocket::ignite().mount("/hello", routes![hello, world]); }
This occurs because the routes! macro implicitly converts the route's name into the name of a structure generated by Rocket's code generation. The solution is to refer to the route using a namespaced path instead:
rocket::ignite().mount("/hello", routes![hello, other::world]);
In Rocket v5 (currently, there's only a release candidate), this does not happen anymore. For example, this compiles with Rocket v5:
#[macro_use]
extern crate rocket;
mod module {
#[get("/bar")]
pub fn route() -> &'static str {
"Hello, world!"
}
}
use module::route;
fn main() {
rocket::build().mount("/foo", routes![route]);
}
When running cargo-expand
on this, we see that Rocket generates something like this (abbreviated by me):
#[macro_use]
extern crate rocket;
mod module {
pub fn route() -> &'static str {
"Hello, world!"
}
#[doc(hidden)]
#[allow(non_camel_case_types)]
/// Rocket code generated proxy structure.
pub struct route {}
/// Rocket code generated proxy static conversion implementations.
impl route {
#[allow(non_snake_case, unreachable_patterns, unreachable_code)]
fn into_info(self) -> ::rocket::route::StaticInfo {
// ...
}
// ...
}
// ...
}
// ...
The get
attribute macro that is applied to a function constructs a new struct with the same name as the function. That struct contains the metadata (or, more correctly, contains a function into_info()
that returns a struct with the correct metadata – though this is more of an detail of the implementation used by Rocket).
This works because function declarations live in the Value Namespace while struct declarations live in the Type Namespace. The use
declaration imports both.
Let's apply this to your example: Your trait declaration lives in the Type Namespace, just like structs. So, while you can't have your special_trait
macro declare a struct of the same name the trait has, you could have that macro declare a function of the same name that returns a struct containing the metadata. This function can then be invoked by useful_macro!
to access the metadata of the trait. So, for example, the metadata struct could look like this:
struct TraitMetadata {
name: String
}
Your macros could then expand something like this:
mod other {
#[special_trait]
pub trait MyTrait{}
}
use some_mod::MyTrait;
fn main() {
useful_macro!(MyTrait);
}
to this:
mod other {
pub trait MyTrait{}
pub fn MyTrait() -> TraitMetadata {
TraitMetadata {
name: "MyTrait".to_string()
}
}
}
use other::MyTrait;
fn main() {
do_something_with_trait_metadata(MyTrait());
}
This design has only one problem: If the user declares a function (or anything else living in the Value Namespace) that has the same name as the trait, this fails. However:
snake_case
while trait names are CamelCase
, so if the user uses idiomatic identifiers, he won't ever have a function with the same name that the trait uses.So, the only realistic way this may lead to conflicts is that another macro author is also using this construct to attach metadata to traits, and the user applies your attribute macro and the other macro on the same trait. In my opinion, this is an edge case that will happen so infrequently that it is not worth supporting.