Swiftorial Logo
Home
Swift Lessons
Matchups
CodeSnaps
Tutorials
Career
Resources

Procedural Macros in Rust

What are Procedural Macros?

Procedural macros in Rust allow developers to write code that generates code at compile time. This feature is particularly useful for reducing boilerplate code and enhancing productivity. Unlike declarative macros, which match patterns and replace them with code, procedural macros operate on the syntax tree and can perform more complex manipulations.

Types of Procedural Macros

There are three types of procedural macros in Rust:

  • Custom Derive Macros: Allow you to create custom implementations of traits.
  • Attribute-like Macros: Allow you to create custom attributes that can be applied to items.
  • Function-like Macros: Allow you to define macros that look like functions and can take token streams as input.

Creating a Custom Derive Macro

To create a custom derive macro, you need to set up a new Rust library project. Let’s create a simple example that derives a trait to display the struct as a string.

1. Create a new library project:

cargo new my_macro --lib

2. Add the dependencies in Cargo.toml:

                    [dependencies]
                    proc-macro2 = "1.0"
                    quote = "1.0"
                    syn = { version = "1.0", features = ["full"] }
                    

3. Implement the macro in src/lib.rs:

                    use proc_macro::TokenStream;
                    use quote::quote;
                    use syn;

                    #[proc_macro_derive(MyTrait)]
                    pub fn my_trait_derive(input: TokenStream) -> TokenStream {
                        let ast = syn::parse(input).unwrap();
                        let name = &ast.ident;

                        let gen = quote! {
                            impl MyTrait for #name {
                                fn display(&self) -> String {
                                    format!("{:?}", self)
                                }
                            }
                        };
                        gen.into()
                    }
                    

4. Use the macro in your main project:

                    #[derive(MyTrait)]
                    struct MyStruct {
                        value: i32,
                    }

                    fn main() {
                        let my_struct = MyStruct { value: 42 };
                        println!("{}", my_struct.display());
                    }
                    

Attribute-like Macros

Attribute-like macros allow you to annotate items with custom attributes. Here’s an example of creating a simple logging attribute.

1. Create the attribute-like macro:

                    #[proc_macro_attribute]
                    pub fn log(attr: TokenStream, item: TokenStream) -> TokenStream {
                        let input = syn::parse(item).unwrap();
                        let name = &input.ident;

                        let gen = quote! {
                            fn #name() {
                                println!("Calling function: {}", stringify!(#name));
                                #input
                            }
                        };
                        gen.into()
                    }
                    

2. Use the log attribute:

                    #[log]
                    fn my_function() {
                        println!("Inside my_function");
                    }

                    fn main() {
                        my_function();
                    }
                    

Function-like Macros

Function-like macros look like function calls and can take input token streams. Here is an example of a macro that creates a vector.

1. Create the function-like macro:

                    #[proc_macro]
                    pub fn my_vec(input: TokenStream) -> TokenStream {
                        let input = syn::parse_macro_input!(input as syn::Expr);
                        let gen = quote! {
                            vec![#input]
                        };
                        gen.into()
                    }
                    

2. Use the function-like macro:

                    fn main() {
                        let v = my_vec![1, 2, 3, 4];
                        println!("{:?}", v);
                    }
                    

Conclusion

Procedural macros are a powerful feature of Rust that allow for code generation and manipulation at compile time. They can significantly reduce boilerplate code and improve code readability. Understanding how to create and use procedural macros can greatly enhance your Rust programming capabilities.