Rust allows you to define macros that “generate” functions at compile time. In this article, we will create one such macro that takes a type and generates a function to retrieve its max value.
Proc-Macro Crate
Let’s first create a new library crate:
cargo new --lib macros
and set its type as `proc-macro` in Cargo.toml:
[lib]
proc-macro = true
Next, let’s add our dependencies to parse and generate Rust code:
[dependencies]
syn = "2"
derive-syn-parse = "0.1.5"
quote = "1"
Now, we are ready to define our macro. Let’s head over to our lib.rs and define an empty macro:
use quote::quote;
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_getter(_item: TokenStream) -> TokenStream {
quote!().into()
}
Demo crate
In order to make sure that everything has been setup correctly, let’s create a new binary crate:
cargo new demo
and link it to our macros crate in Cargo.toml:
[dependencies]
macros = { path = "../macros" }
We can now modify our main.rs to call our macro:
use macros::make_getter;
make_getter!(u32);
fn main() {
}
If we build our demo crate now, it should compile successfully, but our macro is not doing anything at this point. It gets removed entirely as we return an empty `quote!()` in the current macro definition.
Type Parser
Let’s now try to parse the `u32` argument being passed to our macro:
use derive_syn_parse::Parse;
use proc_macro::TokenStream;
use syn::{parse_macro_input, Type};
use quote::quote;
#[derive(Parse)]
struct TypeParam {
ty: Type,
}
#[proc_macro]
pub fn make_getter(item: TokenStream) -> TokenStream {
let typ = parse_macro_input!(item as TypeParam);
quote!().into()
}
The code above tries to parse the provided tokens to the struct `TypeParam`. Try playing with the inputs to our macro `make_getter` to see if you are able to compile it each time.
Define getter
Finally, let’s complete our macro definition to return the maximum of the provided type:
use derive_syn_parse::Parse;
use proc_macro::TokenStream;
use syn::{parse_macro_input, Type};
use quote::quote;
#[derive(Parse)]
struct TypeParam {
ty: Type,
}
#[proc_macro]
pub fn make_getter(item: TokenStream) -> TokenStream {
let typ = parse_macro_input!(item as TypeParam);
let ty = typ.ty;
quote!(fn get() -> #ty { #ty::MAX }).into()
}
`#ty` in the code below gets replaced by the input type if the parser was able to decode it correctly.
Demo!
If we modify our `main.rs` to call our “generated” function:
use macros::make_getter;
make_getter!(u32);
fn main() {
println!("{}", get());
}
and run our binary:
cargo run
We should see the output as `4294967295`. Similarly, we can replace `u32` to `u64` and run again to see the output as `18446744073709551615`.
Thus, we were able to create a macro that expands to functions with different types based on the input parameter.