Search code examples
rustrust-macros

How can I assign "metadata" to a trait?


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?

Possible but sub-optimal solution

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?


Solution

  • 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:

    1. In idiomatic Rust, functions names are 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.
    2. Even if the user uses non-idiomatic names, using the same identifier for a trait and a function is just asking for trouble. I doubt that anyone (well, anyone except macro authors) would ever do this.

    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.