#[derive(MyTrait)]
A copypastable guide to implementing simple derive macros in Rust.
The goal
Let's say we have a trait with a getter
trait MyTrait {
fn answer() -> i32 {
42
}
}
And we want to be able to derive it and initialize the getter
#[derive(MyTrait)]
struct Foo;
#[derive(MyTrait)]
#[my_trait(answer = 0)]
struct Bar;
#[test]
fn default() {
assert_eq!(Foo::answer(), 42);
}
#[test]
fn getter() {
assert_eq!(Bar::answer(), 0);
}
So these derives would expand into
impl MyTrait for Foo {}
impl MyTrait for Bar {
fn answer() -> i32 {
0
}
}
Step 0: prerequisites
Install Cargo extended tools
cargo install cargo-edit
cargo install cargo-expand
Step 1: a separate crate for the macro
Proc macros should live in a separate crate. Let's create one in a sub-folder and make it a dependency for our root crate
cargo new --lib mytrait-derive
cargo add mytrait-derive --path mytrait-derive
We should also tell Cargo that mytrait-derive
is a proc-macro crate:
cat >> mytrait-derive/Cargo.toml << EOF
[lib]
proc-macro = true
EOF
Step 2: default trait implementation
Now let's make #[derive(MyTrait)]
work. We'll need to add a few dependencies to our macro crate
cd mytrait-derive
cargo add [email protected] [email protected]
cargo add [email protected] --features full
And here's our default trait implementation (mytrait-derive/src/lib.rs
):
use proc_macro::{self, TokenStream};
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(MyTrait)]
pub fn derive(input: TokenStream) -> TokenStream {
let DeriveInput { ident, .. } = parse_macro_input!(input);
let output = quote! {
impl MyTrait for #ident {}
};
output.into()
}
You can think of ident
as a name of a struct or enum we're deriving the implementation for. We're getting it from the parse_macro_input!
and then we use it in the quote!
, which is like a template engine for Rust code generation.
Now this test (src/lib.rs
) should pass:
use mytrait_derive::MyTrait;
trait MyTrait {
fn answer() -> i32 {
42
}
}
#[derive(MyTrait)]
struct Foo;
#[test]
fn default() {
assert_eq!(Foo::answer(), 42);
}
Also you should be able to find the implementation in the output of cargo-expand
cargo expand | grep 'impl MyTrait'
impl MyTrait for Foo {}
Step 3: the getter initialization
Now it's time to make our getter initializable by #[my_trait(answer = ...)]
attribute. We'll need one more crate for convenient parsing of the initialization value
cd mytrait-derive
cargo add [email protected]
Here's the final version of our macro (mytrait-derive/src/lib.rs
):
use darling::FromDeriveInput;
use proc_macro::{self, TokenStream};
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[derive(FromDeriveInput, Default)]
#[darling(default, attributes(my_trait))]
struct Opts {
answer: Option<i32>,
}
#[proc_macro_derive(MyTrait, attributes(my_trait))]
pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input);
let opts = Opts::from_derive_input(&input).expect("Wrong options");
let DeriveInput { ident, .. } = input;
let answer = match opts.answer {
Some(x) => quote! {
fn answer() -> i32 {
#x
}
},
None => quote! {},
};
let output = quote! {
impl MyTrait for #ident {
#answer
}
};
output.into()
}
Struct Opts
describes parameters of the #[my_trait(...)]
attribute. Here we have only one of them - answer
. Notice that it's optional, because we don't want to overwrite the default fn answer()
implementation if the attribute wasn't used.
The quote!
macro is composable - we can use output of one of them in another. So in the match
we check if the initializer is passed and create the method implementation or just nothing. And finally we use the result in the outer quote!
template.
That's all, clone this repo to play with the code.