Introduction
This book is a guide on how to implement Rust Procedural Macros using well established crates such as paste and syn.
Getting Started
Rust procedural macros must be part of their own crate. The reason is that the crate is used as a plugin by the compiler. It is necesserary to indicate to cargo that the crate will contains procedural macros by adding the following to the Cargo.toml file:
[lib]
proc-macro = true
Derive Macros
#![allow(unused)] fn main() { use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; #[proc_macro_derive(MyTrait)] pub fn my_trait(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = input.ident; // Split a type’s generics into the pieces required for impl’ing a trait for that type. let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let out = quote! { impl #impl_generics MyTrait for #name #ty_generics #where_clause { } }; out.into() } }
#![allow(unused)] fn main() { trait MyTrait {} }
#![allow(unused)] fn main() { #[derive(MyTrait)] struct MyStruct {} }
Function-like macros
Function-like macros are that are used like declarative macros and can take any valid token stream input. They take a TokenStream as input, and output a TokenStream.
They have the following signature:
#![allow(unused)] fn main() { #[proc_macro] pub fn my_macro(input: TokenStream) -> TokenStream { ... } }
This macro can then be used in code as such:
#![allow(unused)] fn main() { my_macro_crate::my_macro!(...); }
As an example, lets make a macro that takes an identifier, and a literal and create a constant for us:
#![allow(unused)] fn main() { use proc_macro::TokenStream; /// The ParsedInput structure will be used to store the input to our macros. struct ParsedInput { /// Identifier for the generated constant. ident: syn::Ident, /// String expression. expr: syn::LitStr, } /// Implement the Parse trait from syn to parse the input impl syn::parse::Parse for ParsedInput { fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { // Parse our first macro argument, the identifier. let ident = input.parse()?; // Parse the comma and ignore it. let _: syn::Token![,] = input.parse()?; // Parse our second macro argument, the string expression. let expr = input.parse()?; Ok(Self { ident, expr }) } } #[proc_macro] pub fn make_constant(input: TokenStream) -> TokenStream { let ParsedInput { ident, expr } = syn::parse_macro_input!(input as ParsedInput); // Use the quote crate to create the constant. quote::quote! { const #ident: &str = #expr; } .into() } }
Attribute macros
#![allow(unused)] fn main() { use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; #[proc_macro_derive(MyTrait, attributes(my_attr))] pub fn my_trait(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = input.ident; // Split a type’s generics into the pieces required for impl’ing a trait for that type. let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let out = quote! { impl #impl_generics MyTrait for #name #ty_generics #where_clause { } }; out.into() } }
#![allow(unused)] fn main() { #[derive(MyTrait)] struct MyStruct { #[my_attr(var = 1)] field_0: bool, } }
Tips and tricks
Error management
While procedure macros do not return a Result it is still possible to report errors as part of the TokenStream. The easiest way to report an error is to use syn::Error. It takes a span as argument and an error message. The span identify where in the original source code the error message is originating. It is best to use a span coming from the most relevant token.
Example of usage:
#![allow(unused)] fn main() { let ts: TokenStream = syn::Error::new( ident.span(), "Something wrong happened." ) .into_compile_error() .into() }
Reference the crate
In declarative macros, to refer the current crate you can use $crate. There is no direct equivalent in proc_macros.
The trick is to check the content of Cargo.toml using proc_macro_crate to check if the macro is invoked from the current crate or from an other.
You can use the following function:
#![allow(unused)] fn main() { /// This funcion is used to find if this macro is invoked from `my_macro_crate` and must be refered as `crate` /// or if it is invoked from a different crate, and must be referred as `my_macro_crate`. fn my_crate() -> proc_macro2::TokenStream { use proc_macro_crate::{FoundCrate, crate_name}; let found_crate = crate_name("my_macro_crate") .expect("my_macro_crate is present in `Cargo.toml`"); match found_crate { FoundCrate::Itself => quote!(crate), FoundCrate::Name(name) => { let ident = Ident::new(&name, proc_macro2::Span::call_site()); quote!( #ident ) } } } }
Identifier
quote does not allow to combine identifier inline. Templates fragments must be full token, if you need to create new identifier, they need to be created in code before been used in a quote!. The most basic solution is to create a new syn::Ident, for instance:
#![allow(unused)] fn main() { use proc_macro2::{Ident, Span}; let my_ident = Ident:new("my_ident", Span::call_site()); }
Span::call_site() will points any error related to my_ident, use it when creating identifier out of thin air. However, it is generally better to use a span from a token parsed from the macro invokation.
To create identifier from combining strings, quote provides a convenient formatting macro:
#![allow(unused)] fn main() { let my_ident = quote::format_ident!("my_{}", ident); }
It will attempt to re-use the span from ident or create a new one.
For changing the case of identifier, between snake case (e.g. for functions or variables names) or pascal case (e.g. for struct), you can use the stringcase crate:
let function_name = Ident::new(stringcase::snake_case(ident.to_string()), ident.span());
let struct_name = Ident::new(stringcase::pascal_case(ident.to_string()), ident.span());