Skip to content

Latest commit

 

History

History

3_2_macro

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Step 3.2: Declarative and procedural macros

Estimated time: 1 day

Rust provides strong and convenient built-in capabilities for code generation in a form of macros.

The term macro refers to a family of features in Rust: declarative macros with macro_rules! and three kinds of procedural macros:

  • Custom #[derive] macros that specify code added with the derive attribute used on structs and enums
  • Attribute-like macros that define custom attributes usable on any item
  • Function-like macros that look like function calls but operate on the tokens specified as their argument

Declarative macros

Declarative macros represent the most primitive form of macros in Rust. They are quite limited in their capabilities and their syntax (which represents a DSL-based match expression) may become quite cumbersome in complex cases.

They are called declarative, because macro implementation represents a declaration of code transforming rules (you're declaring how your code will be transformed):

macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

let v = vec![1, 2, 3];

The good part about declarative macros is that they are hygienic.

Code generation purpose is not the only one declarative macros are used for. Quite often they are used for building abstractions and APIs too, because they all to implement much more ergonomic features than regular functions do: named arguments, variadics, etc.

For better understanding declarative macros design, concepts, usage and features, read through the following articles:

Procedural macros

Procedural macros represent much more powerful code generation tool. They are called procedural, because macro implementation represents a regular Rust code, which works directly with AST of transformed code (you're writing procedures which transform your code). Procedural macro requires a separate proc-macro = true crate to be implemented in.

Procedural macros are unhygienic, so implementing one you need to be careful to ensure that macro works in as many contexts as possible.

There are three kinds of procedural macros in Rust at the moment:

  • proc_macro function-like macros, which usage looks like regular declarative macros usage, but they accept arbitrary tokens on input (while declarative ones don't), and are more powerful in general (can contain complex logic for generating simple code):

    #[proc_macro]
    pub fn make_answer(_: TokenStream) -> TokenStream {
        "fn answer() -> u32 { 42 }".parse().unwrap()
    }
    make_answer!();
  • proc_macro_attribute attribute macros, which allow to create custom Rust attributes:

    #[proc_macro_attribute]
    pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
       // code...
    }
    #[route(GET, "/")]
    fn index() {}
  • proc_macro_derive derive macros, which allow to provide custom implementations for #[derive(Trait)] attribute:

    #[proc_macro_derive(AnswerFn)]
    pub fn derive_answer_fn(_: TokenStream) -> TokenStream {
        "impl Struct{ fn answer() -> u32 { 42 } }".parse().unwrap()
    }
    #[derive(AnswerFn)]
    struct Struct;

    Idiomatically, proc_macro_derive should be used for deriving trait implementations only. For arbitrary functions generation it's better to go with proc_macro_attribute.

Rust ecosystem has some well-know crates, which almost always are used for procedural macros implementation:

  • syn crate represents implementation of Rust's AST.
  • quote crate provides quasi-quoting, which allows to turn Rust syntax tree data structures into tokens of source code in an ergonomic and readable way.
  • proc-macro2 crate provides unified proc_macro API across all Rust compiler versions and makes procedural macros unit testable.

Additionally, darling crate should be mentioned, which makes declarative attribute parsing more straight-forward and ergonomic.

For better understanding procedural macros design, concepts, usage and features, read through the following articles:

Task

Implement a btreemap! macro, which allows to create BTreeMap in an ergonomic and declarative way (similarly to vec!).

Provide two implementations: one via declarative macro and other one via procedural macro.