A copypastable guide to implementing simple derive macros in Rust.

Overview

#[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):

quote! { fn answer() -> i32 { #x } }, None => quote! {}, }; let output = quote! { impl MyTrait for #ident { #answer } }; output.into() } ">
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.

Issues
Easy-to-use optional function arguments for Rust

OptArgs uses const generics to ensure compile-time correctness. I've taken the liberty of expanding and humanizing the macros in the reference examples.

Jonathan Kelley 26 Apr 25, 2021
Milho (corn in portuguese) is a toy dialect of Lisp written as a way to learn more about compilers

Milho (corn in portuguese) is a toy dialect of Lisp written as a way to learn more about compilers. There are implementations in rust and go

Celso Bonutti 5 Jun 11, 2021
Uindex is a data store, for data that can be parsed as sentences in some context-free language.

Uindex - Universal index Uindex is a data store, for data that can be parsed as sentences in some context-free language.

Enrique Pérez Arnaud 3 May 23, 2021
A programming language. Better mantra pending.

Dusk Dusk is a programming language I've been working on on and off for the past while now. It's still very much in its infancy (... a quick look thro

Kaylynn Morgan 6 Jun 4, 2021
Cargo - The Rust package manager

Cargo downloads your Rust project’s dependencies and compiles your project.

The Rust Programming Language 6.9k Jun 13, 2021
Federated blogging application, thanks to ActivityPub (now on https://git.joinplu.me/ — this is just a mirror)

Plume Website — Documentation — Contribute — Instances list Plume is a federated blogging engine, based on ActivityPub. It is written in Rust, with th

Plume 1.5k Jun 12, 2021
Cassette A simple, single-future, non-blocking executor intended for building state machines.

Cassette A simple, single-future, non-blocking executor intended for building state machines. Designed to be no-std and embedded friendly. This execut

James Munns 36 Jun 7, 2021
A simple programming language for everyone.

Slang A simple programming language for everyone, made with Rust. State In very early stages. Plan is to create a byte-code compiler and make that exe

Slang, Inc. 10 May 30, 2021
Simple library to host lv2 plugins. Is not meant to support any kind of GUI.

lv2-host-minimal Simple library to host lv2 plugins. Is not meant to support any kind of GUI. Host fx plugins (audio in, audio out) Set parameters Hos

Cody Bloemhard 5 May 17, 2021
A language server for lua written in rust

lua-analyzer lua-analyzer is a lsp server for lua. This is mostly for me to learn the lsp protocol and language analysis so suggestions are helpful. T

null 49 Jun 5, 2021
A scripting language that allows complex key remapping on Linux.

Map2 A scripting language that allows complex key remapping on Linux, written in Rust. All of the functionality related to interacting with graphical

Matt 44 Jun 6, 2021
Applied offensive security with Rust

Black Hat Rust - Early Access Deep dive into offensive security with the Rust programming language Buy the book now! Summary Whether in movies or main

Sylvain Kerkour 396 Jun 11, 2021
Rustyread is a drop in replacement of badread simulate.

Rustyread is a drop in replacement of badread simulate. Rustyread is very heavily inspired by badread, it reuses the same error and quality model file. But Rustyreads is multi-threaded and benefits from other optimizations.

Pierre Marijon 16 May 21, 2021
🐀 Building a federated alternative to reddit in rust

Lemmy A link aggregator / Reddit clone for the fediverse. Join Lemmy · Documentation · Report Bug · Request Feature · Releases · Code of Conduct About

LemmyNet 5.1k Jun 13, 2021