To learn how custom derive macros work in Rust, I decided to create a simple derive macro to provide getters and setters for a given struct.
It would be used like this:
#[derive(GetSet)]
pub struct Example {
#[get]
#[set]
field1: String,
#[get]
field2: i32,
}
However, there are some things that feel wrong to me about it:
- The code uses both
proc_macro
andproc_macro2
, is this necessary? - Currently, every getter / setter is in their own
impl
block. But thequote!
macro requires code that is well-formed regarding braces, so I think I cannot createimpl #name {
first and fill in the methods afterwards. Is there a better way? - The function identifiers are created with
format!
, is this okay? - Would it be possible to adapt it so one could just write
#[get, set]
field: (),
instead of
#[get]
#[set]
field: (),
Apart from that, what else would you improve or change?
Here is the code:
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{Data, DeriveInput};
#[proc_macro_derive(GetSet, attributes(get, set))]
pub fn get_set_derive(input: TokenStream) -> TokenStream {
let ast: DeriveInput = syn::parse(input).unwrap();
let mut gen = proc_macro2::TokenStream::new();
let name = &ast.ident;
let data = &ast.data;
if let Data::Struct(data_struct) = data {
let fields = &data_struct.fields;
for field in fields {
let field_name = field
.ident
.as_ref()
.expect("struct idents are required for this to work!");
let field_type = &field.ty;
for attr in &field.attrs {
let path = attr.path();
if path.is_ident("get") {
let fn_ident = Ident::new(&format!("get_{field_name}"), Span::call_site());
let getter = quote! {
impl #name {
pub fn #fn_ident(&self) -> &#field_type {
&self.#field_name
}
}
};
gen.extend(getter);
}
if path.is_ident("set") {
let fn_ident = Ident::new(&format!("set_{field_name}"), Span::call_site());
let setter = quote! {
impl #name {
pub fn #fn_ident(&mut self, new: #field_type) {
self.#field_name = new;
}
}
};
gen.extend(setter);
}
}
}
} else {
panic!("no support for enums or unions!")
}
gen.into()
}
1 Answer 1
The code uses both
proc_macro
andproc_macro2
, is this necessary?
This is normal. proc_macro
is the compiler API stub, only available to proc-macro crates, and proc_macro2
is a plain library that doesn't depend on the compiler proc macro API. It's possible to write a proc-macro crate that doesn't depend on proc_macro2
at all, but then you'd also need to avoid using syn
and quote
, or any other proc-macro utility libraries — they all depend on proc_macro2
, precisely because without it there's no way to manipulate tokens in library code (as opposed to proc-macro crate code).
Currently, every getter / setter is in their own impl block. But the quote! macro requires code that is well-formed regarding braces, so I think I cannot create
impl #name {
first and fill in the methods afterwards.
You can use an iterator or collection in quote!
to assemble the complete structure at once.
let mut functions: Vec<proc_macro2::TokenStream> = Vec::new();
// ... gather functions into vector ...
// this is the entire output of the derive (no `gen` stream)
quote! {
impl #name {
#( #functions )*
}
}
The function identifiers are created with
format!
, is this okay?
Yes.
Would it be possible to adapt it ...
No, the syntax of attributes does not allow #[foo, bar]
. You have to pick one name for the attribute, but it can have parameters: #[getset(get, set)]
.
Apart from that, what else would you improve or change?
It looks pretty good. You asked the right questions and I can't think of more things to change.