├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── mytrait-derive ├── Cargo.lock ├── Cargo.toml └── src │ └── lib.rs └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "darling" 5 | version = "0.13.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "757c0ded2af11d8e739c4daea1ac623dd1624b06c844cf3f5a39f1bdbd99bb12" 8 | dependencies = [ 9 | "darling_core", 10 | "darling_macro", 11 | ] 12 | 13 | [[package]] 14 | name = "darling_core" 15 | version = "0.13.0" 16 | source = "registry+https://github.com/rust-lang/crates.io-index" 17 | checksum = "2c34d8efb62d0c2d7f60ece80f75e5c63c1588ba68032740494b0b9a996466e3" 18 | dependencies = [ 19 | "fnv", 20 | "ident_case", 21 | "proc-macro2", 22 | "quote", 23 | "strsim", 24 | "syn", 25 | ] 26 | 27 | [[package]] 28 | name = "darling_macro" 29 | version = "0.13.0" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "ade7bff147130fe5e6d39f089c6bd49ec0250f35d70b2eebf72afdfc919f15cc" 32 | dependencies = [ 33 | "darling_core", 34 | "quote", 35 | "syn", 36 | ] 37 | 38 | [[package]] 39 | name = "derive-mytrait" 40 | version = "0.1.0" 41 | dependencies = [ 42 | "mytrait-derive", 43 | ] 44 | 45 | [[package]] 46 | name = "fnv" 47 | version = "1.0.7" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 50 | 51 | [[package]] 52 | name = "ident_case" 53 | version = "1.0.1" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 56 | 57 | [[package]] 58 | name = "mytrait-derive" 59 | version = "0.1.0" 60 | dependencies = [ 61 | "darling", 62 | "proc-macro2", 63 | "quote", 64 | "syn", 65 | ] 66 | 67 | [[package]] 68 | name = "proc-macro2" 69 | version = "1.0.27" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" 72 | dependencies = [ 73 | "unicode-xid", 74 | ] 75 | 76 | [[package]] 77 | name = "quote" 78 | version = "1.0.9" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 81 | dependencies = [ 82 | "proc-macro2", 83 | ] 84 | 85 | [[package]] 86 | name = "strsim" 87 | version = "0.10.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 90 | 91 | [[package]] 92 | name = "syn" 93 | version = "1.0.72" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" 96 | dependencies = [ 97 | "proc-macro2", 98 | "quote", 99 | "unicode-xid", 100 | ] 101 | 102 | [[package]] 103 | name = "unicode-xid" 104 | version = "0.2.2" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 107 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "derive-mytrait" 3 | version = "0.1.0" 4 | authors = ["imbolc "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | mytrait-derive = { path = "mytrait-derive" } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # #[derive(MyTrait)] 2 | 3 | A copypastable guide to implementing simple derive macros in Rust. 4 | 5 | ## The goal 6 | 7 | Let's say we have a trait with a getter 8 | 9 | ```rust 10 | trait MyTrait { 11 | fn answer() -> i32 { 12 | 42 13 | } 14 | } 15 | ``` 16 | 17 | And we want to be able to derive it and initialize the getter 18 | 19 | ```rust 20 | #[derive(MyTrait)] 21 | struct Foo; 22 | 23 | #[derive(MyTrait)] 24 | #[my_trait(answer = 0)] 25 | struct Bar; 26 | 27 | #[test] 28 | fn default() { 29 | assert_eq!(Foo::answer(), 42); 30 | } 31 | 32 | #[test] 33 | fn getter() { 34 | assert_eq!(Bar::answer(), 0); 35 | } 36 | ``` 37 | 38 | So these derives would expand into 39 | 40 | ```rust 41 | impl MyTrait for Foo {} 42 | 43 | impl MyTrait for Bar { 44 | fn answer() -> i32 { 45 | 0 46 | } 47 | } 48 | ``` 49 | 50 | ## Step 0: prerequisites 51 | 52 | Install Cargo extended tools 53 | 54 | ```sh 55 | cargo install cargo-edit 56 | cargo install cargo-expand 57 | ``` 58 | 59 | 60 | ## Step 1: a separate crate for the macro 61 | 62 | Proc macros should live in a separate crate. Let's create one in a sub-folder 63 | and make it a dependency for our root crate 64 | 65 | ```sh 66 | cargo new --lib mytrait-derive 67 | cargo add mytrait-derive --path mytrait-derive 68 | ``` 69 | 70 | We should also tell Cargo that `mytrait-derive` is a proc-macro crate: 71 | ```sh 72 | cat >> mytrait-derive/Cargo.toml << EOF 73 | [lib] 74 | proc-macro = true 75 | EOF 76 | ``` 77 | 78 | ## Step 2: default trait implementation 79 | 80 | Now let's make `#[derive(MyTrait)]` work. We'll need to add a few dependencies 81 | to our macro crate 82 | 83 | ```sh 84 | cd mytrait-derive 85 | cargo add proc-macro2@1.0 quote@1.0 86 | cargo add syn@1.0 --features full 87 | ``` 88 | 89 | And here's our default trait implementation (`mytrait-derive/src/lib.rs`): 90 | 91 | ```rust 92 | use proc_macro::{self, TokenStream}; 93 | use quote::quote; 94 | use syn::{parse_macro_input, DeriveInput}; 95 | 96 | #[proc_macro_derive(MyTrait)] 97 | pub fn derive(input: TokenStream) -> TokenStream { 98 | let DeriveInput { ident, .. } = parse_macro_input!(input); 99 | let output = quote! { 100 | impl MyTrait for #ident {} 101 | }; 102 | output.into() 103 | } 104 | ``` 105 | 106 | You can think of `ident` as a name of a struct or enum we're deriving the 107 | implementation for. We're getting it from the `parse_macro_input!` and then we 108 | use it in the `quote!`, which is like a template engine for Rust code 109 | generation. 110 | 111 | Now this test (`src/lib.rs`) should pass: 112 | 113 | ```rust 114 | use mytrait_derive::MyTrait; 115 | 116 | trait MyTrait { 117 | fn answer() -> i32 { 118 | 42 119 | } 120 | } 121 | 122 | #[derive(MyTrait)] 123 | struct Foo; 124 | 125 | #[test] 126 | fn default() { 127 | assert_eq!(Foo::answer(), 42); 128 | } 129 | ``` 130 | 131 | Also you should be able to find the implementation in the output of [cargo-expand][] 132 | 133 | ```sh 134 | cargo expand | grep 'impl MyTrait' 135 | impl MyTrait for Foo {} 136 | ``` 137 | 138 | ## Step 3: the getter initialization 139 | 140 | Now it's time to make our getter initializable by `#[my_trait(answer = ...)]` 141 | attribute. We'll need one more crate for convenient parsing of the 142 | initialization value 143 | 144 | ```sh 145 | cd mytrait-derive 146 | cargo add darling@0.13 147 | ``` 148 | 149 | Here's the final version of our macro (`mytrait-derive/src/lib.rs`): 150 | 151 | ```rust 152 | use darling::FromDeriveInput; 153 | use proc_macro::{self, TokenStream}; 154 | use quote::quote; 155 | use syn::{parse_macro_input, DeriveInput}; 156 | 157 | #[derive(FromDeriveInput, Default)] 158 | #[darling(default, attributes(my_trait))] 159 | struct Opts { 160 | answer: Option, 161 | } 162 | 163 | #[proc_macro_derive(MyTrait, attributes(my_trait))] 164 | pub fn derive(input: TokenStream) -> TokenStream { 165 | let input = parse_macro_input!(input); 166 | let opts = Opts::from_derive_input(&input).expect("Wrong options"); 167 | let DeriveInput { ident, .. } = input; 168 | 169 | let answer = match opts.answer { 170 | Some(x) => quote! { 171 | fn answer() -> i32 { 172 | #x 173 | } 174 | }, 175 | None => quote! {}, 176 | }; 177 | 178 | let output = quote! { 179 | impl MyTrait for #ident { 180 | #answer 181 | } 182 | }; 183 | output.into() 184 | } 185 | ``` 186 | 187 | Struct `Opts` describes parameters of the `#[my_trait(...)]` attribute. Here we 188 | have only one of them - `answer`. Notice that it's optional, because we don't 189 | want to overwrite the default `fn answer()` implementation if the attribute 190 | wasn't used. 191 | 192 | The `quote!` macro is composable - we can use output of one of them in another. 193 | So in the `match` we check if the initializer is passed and create the method 194 | implementation or just nothing. And finally we use the result in the outer 195 | `quote!` template. 196 | 197 | That's all, clone this repo to play with the code. 198 | 199 | [cargo-expand]: https://github.com/dtolnay/cargo-expand 200 | -------------------------------------------------------------------------------- /mytrait-derive/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "darling" 5 | version = "0.13.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "757c0ded2af11d8e739c4daea1ac623dd1624b06c844cf3f5a39f1bdbd99bb12" 8 | dependencies = [ 9 | "darling_core", 10 | "darling_macro", 11 | ] 12 | 13 | [[package]] 14 | name = "darling_core" 15 | version = "0.13.0" 16 | source = "registry+https://github.com/rust-lang/crates.io-index" 17 | checksum = "2c34d8efb62d0c2d7f60ece80f75e5c63c1588ba68032740494b0b9a996466e3" 18 | dependencies = [ 19 | "fnv", 20 | "ident_case", 21 | "proc-macro2", 22 | "quote", 23 | "strsim", 24 | "syn", 25 | ] 26 | 27 | [[package]] 28 | name = "darling_macro" 29 | version = "0.13.0" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "ade7bff147130fe5e6d39f089c6bd49ec0250f35d70b2eebf72afdfc919f15cc" 32 | dependencies = [ 33 | "darling_core", 34 | "quote", 35 | "syn", 36 | ] 37 | 38 | [[package]] 39 | name = "fnv" 40 | version = "1.0.7" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 43 | 44 | [[package]] 45 | name = "ident_case" 46 | version = "1.0.1" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 49 | 50 | [[package]] 51 | name = "mytrait-derive" 52 | version = "0.1.0" 53 | dependencies = [ 54 | "darling", 55 | "proc-macro2", 56 | "quote", 57 | "syn", 58 | ] 59 | 60 | [[package]] 61 | name = "proc-macro2" 62 | version = "1.0.27" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" 65 | dependencies = [ 66 | "unicode-xid", 67 | ] 68 | 69 | [[package]] 70 | name = "quote" 71 | version = "1.0.9" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 74 | dependencies = [ 75 | "proc-macro2", 76 | ] 77 | 78 | [[package]] 79 | name = "strsim" 80 | version = "0.10.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 83 | 84 | [[package]] 85 | name = "syn" 86 | version = "1.0.72" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" 89 | dependencies = [ 90 | "proc-macro2", 91 | "quote", 92 | "unicode-xid", 93 | ] 94 | 95 | [[package]] 96 | name = "unicode-xid" 97 | version = "0.2.2" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 100 | -------------------------------------------------------------------------------- /mytrait-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mytrait-derive" 3 | version = "0.1.0" 4 | authors = ["imbolc "] 5 | edition = "2018" 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | darling = "0.13" 12 | proc-macro2 = "1.0" 13 | quote = "1.0" 14 | syn = { version = "1.0", features = ["full"] } 15 | -------------------------------------------------------------------------------- /mytrait-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use darling::FromDeriveInput; 2 | use proc_macro::{self, TokenStream}; 3 | use quote::quote; 4 | use syn::{parse_macro_input, DeriveInput}; 5 | 6 | #[derive(FromDeriveInput, Default)] 7 | #[darling(default, attributes(my_trait), forward_attrs(allow, doc, cfg))] 8 | struct Opts { 9 | answer: Option, 10 | } 11 | 12 | #[proc_macro_derive(MyTrait, attributes(my_trait))] 13 | pub fn derive(input: TokenStream) -> TokenStream { 14 | let input = parse_macro_input!(input); 15 | let opts = Opts::from_derive_input(&input).expect("Wrong options"); 16 | let DeriveInput { ident, .. } = input; 17 | 18 | let answer = match opts.answer { 19 | Some(x) => quote! { 20 | fn answer() -> i32 { 21 | #x 22 | } 23 | }, 24 | None => quote! {}, 25 | }; 26 | 27 | let output = quote! { 28 | impl MyTrait for #ident { 29 | #answer 30 | } 31 | }; 32 | output.into() 33 | } 34 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use mytrait_derive::MyTrait; 2 | 3 | trait MyTrait { 4 | fn answer() -> i32 { 5 | 42 6 | } 7 | } 8 | 9 | #[derive(MyTrait)] 10 | struct Foo; 11 | 12 | #[derive(MyTrait)] 13 | #[my_trait(answer = 0)] 14 | struct Bar; 15 | 16 | #[test] 17 | fn default() { 18 | assert_eq!(Foo::answer(), 42); 19 | } 20 | 21 | #[test] 22 | fn getter() { 23 | assert_eq!(Bar::answer(), 0); 24 | } 25 | --------------------------------------------------------------------------------