Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/imbolc/rust-derive-macro-guide
Simple derive macros in Rust
https://github.com/imbolc/rust-derive-macro-guide
Last synced: about 2 months ago
JSON representation
Simple derive macros in Rust
- Host: GitHub
- URL: https://github.com/imbolc/rust-derive-macro-guide
- Owner: imbolc
- Created: 2021-06-03T08:12:31.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2021-06-04T04:35:49.000Z (over 3 years ago)
- Last Synced: 2024-06-12T17:08:22.526Z (3 months ago)
- Language: Rust
- Size: 14.6 KB
- Stars: 176
- Watchers: 4
- Forks: 11
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# #[derive(MyTrait)]
A copypastable guide to implementing simple derive macros in Rust.
## The goal
Let's say we have a trait with a getter
```rust
trait MyTrait {
fn answer() -> i32 {
42
}
}
```And we want to be able to derive it and initialize the getter
```rust
#[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
```rust
impl MyTrait for Foo {}impl MyTrait for Bar {
fn answer() -> i32 {
0
}
}
```## Step 0: prerequisites
Install Cargo extended tools
```sh
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```sh
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:
```sh
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```sh
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`):
```rust
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:
```rust
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][]
```sh
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```sh
cd mytrait-derive
cargo add [email protected]
```Here's the final version of our macro (`mytrait-derive/src/lib.rs`):
```rust
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,
}#[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.
[cargo-expand]: https://github.com/dtolnay/cargo-expand