rustrust-macrosrust-proc-macros

How do I create a proc_macro_attribute?


Now that proc_macros have been stabilized, how does one create such a thing?

From what I've seen, there's the option of putting a #[proc_macro_attribute] annotation on a fn whatsitsname(attrs: TokenStream, code: TokenStream) -> TokenStream, but how can I register it? How can I add custom attributes?


Solution

  • The Rust compiler has a fairly complete test suite. When looking for examples of newly-introduced features, I frequently start there:

    $ rg -c proc_macro_attribute
    src/test/run-pass-fulldeps/auxiliary/proc_macro_def.rs:2
    src/test/ui-fulldeps/auxiliary/attr_proc_macro.rs:1
    [... 35 other matches ...]
    

    Here's a fully worked example:

    $ tree
    .
    ├── Cargo.toml
    ├── my_macro
    │   ├── Cargo.toml
    │   ├── src
    │   │   └── lib.rs
    └── src
        └── main.rs
    

    Cargo.toml

    We add a dependency on our macro-defining crate.

    [package]
    name = "foo"
    version = "0.1.0"
    authors = ["An Devloper <an.devloper@example.com>"]
    
    [dependencies]
    my_macro = { path = "my_macro" }
    

    src/main.rs

    We import the attribute macro and add it to a function.

    #[macro_use]
    extern crate my_macro;
    
    #[log_entry_and_exit(hello, "world")]
    fn this_will_be_destroyed() -> i32 {
        42
    }
    
    fn main() {
        dummy()
    }
    

    my_macro/Cargo.toml

    We specify crate_type as proc_macro.

    [package]
    name = "my_macro"
    version = "0.1.0"
    authors = ["An Devloper <an.devloper@example.com>"]
    
    [lib]
    crate_type = ["proc-macro"]
    

    my_macro/src/lib.rs

    We add #[proc_macro_attribute] to each function that should be a macro.

    extern crate proc_macro;
    
    use proc_macro::*;
    
    #[proc_macro_attribute]
    pub fn log_entry_and_exit(args: TokenStream, input: TokenStream) -> TokenStream {
        let x = format!(r#"
            fn dummy() {{
                println!("entering");
                println!("args tokens: {{}}", {args});
                println!("input tokens: {{}}", {input});
                println!("exiting");
            }}
        "#,
                args = args.into_iter().count(),
                input = input.into_iter().count(),
        );
    
        x.parse().expect("Generated invalid tokens")
    }
    

    cargo run

    entering
    args tokens: 3
    input tokens: 7
    exiting
    

    The "hard" part is wrangling the TokenStream into something useful and then outputting something equally useful. The crates syn and quote are the current gold standards for those two tasks. Dealing with TokenStream is covered in the macros chapter of The Rust Programming Language as well as API documentation.

    There's also #[proc_macro], which takes functions of the form:

    #[proc_macro]
    pub fn the_name_of_the_macro(input: TokenStream) -> TokenStream
    

    And can be invoked as the_name_of_the_macro!(...).