• Stars
    star
    180
  • Rank 213,097 (Top 5 %)
  • Language
    Rust
  • Created over 3 years ago
  • Updated over 3 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Simple derive macros in Rust

#[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.

More Repositories

1

tower-cookies

A tower (axum) cookies manager
Rust
115
star
2

aiohttp-login

Registration and authorization (including social) for aiohttp apps.
Python
51
star
3

axum-client-ip

A client IP address extractor for Axum
Rust
40
star
4

step-machine

Run your Rust CLI programs as state machines with persistence and recovery abilities
Rust
34
star
5

sapper-google-analytics

Google Analytics for Sapper (Svelte)
HTML
34
star
6

tower-request-id

A tower (hyper, axum, warp) service to add an unique id for every request
Rust
25
star
7

sapper-page-loading-bar

Page preloader for Sapper
HTML
23
star
8

pg_task

FSM-based Resumable Postgres tasks
Rust
19
star
9

aiohttp_skeleton

Skeleton for aiohttp site
Python
19
star
10

aiolog

Asynchronous handlers for standard python logging library
Python
17
star
11

async-django-user

Using django user with async frameworks like aiohttp, starlette, etc.
Python
11
star
12

japronto-extra

Python
11
star
13

async_django_session

Django-compatible session for async frameworks
Python
10
star
14

django-route-decorator

A flask-style `route` decorator for django views
Python
10
star
15

habr-modern-tornado

Python
9
star
16

asjson

Json dumps and loads with datetime and date support
Python
7
star
17

sqlite_dbm

Dict-style DBM based on sqlite3
Python
7
star
18

mongo

simple pymongo object wrapper
Python
7
star
19

tagged-channels

Tag channels and message using tags (WebSockets, SSE users management)
Rust
6
star
20

django-raw-api

Async-friendly straightforward Django API helpers
Python
5
star
21

aiorest-angular-template

python asyncio + aiorest + jinja2 + angular.js + gulp.js + bower.js + nginx
Python
4
star
22

unidbm

Dict-style key value wrapper around some embeded databases
Python
3
star
23

action-loop

Offline-first web app with Hoodie & React
JavaScript
3
star
24

embed-file

Embeds files content into the binary in release mode, but loads it from the fs in debug
Rust
3
star
25

single_access

Single access to run a python script
Python
2
star
26

sqlx_error

A wrapper around `sqlx::Error` to provide error path and additional context
Rust
2
star
27

derby-yamlpages

Flexible flatpages for derby.js based on yaml syntax
JavaScript
2
star
28

django_skeleton

Python
2
star
29

csv-line

Fast deserializer for a single csv line
Rust
2
star
30

server-setup

Shell
2
star
31

flatpage

A simple file system based markdown flat page
Rust
2
star
32

aiohttp_tools

A set of little tools for aiohttp-based sites.
Python
2
star
33

express-url4

Reversing routes for express and browser. Like a Rails or Flask url_for method.
JavaScript
1
star
34

serde-csv-extra

Csv-related serde addons
Rust
1
star
35

webassets-ng-annotate

ng-annotate filter for webassets
Python
1
star
36

flask-peewee-skeleton

Python
1
star
37

aws-resource-id

Copy-able stack-only AWS Resource IDs
Rust
1
star
38

wheezy-setup

Shell
1
star
39

csvfile

A simple way of working with csv files.
Python
1
star
40

jsondict

Dict with json file saving / restoring support
Python
1
star
41

colt-faq

1
star
42

notes

Personal notes
Rust
1
star
43

lesync

Http helpers for django-channels
Python
1
star
44

firefox_jar

Python
1
star
45

lz4_flex_py

A Python wrapper for lz4_flex Rust crate
Rust
1
star
46

csv-partial-cache

Csv index with partially cached columns
Rust
1
star
47

seq-timer

A simple timer for sequential events
Rust
1
star