DEV Community

TakaakiFuruse
TakaakiFuruse

Posted on • Updated on

Rust ProcMacro 101

Intro

You know this works....

fn greetings(var: String) {
    println!("{}", var)
}

greetings("Valar Morghulis!".to_string());

You know also this never works ....
If it works. It's a magic.

fn greetings_generator(var1: String, var2: String) {
    fn var2(var: String){
     var1("{}", var)
   }
}

greeetings_generator(println!, greetings1)
greetings1("Valar Morghulis!".to_string!)

There's a way to make it work.
Use Rust macro.
Macro allows you to write Rust code as if you are dealing with function.

macro_rules! greetings_generator {
    ($var1:ident, $var2:ident) => {
        fn $var2(var: String) {
            $var1!("{}", var)
        }
    };
}

greetings_generator!(print, greetings2);
greetings2("Valar Morghulis from greeting2\n".to_string());

greetings_generator!(println, greetings3);
greetings3("Valar Morghulis from greeting3".to_string());

Code on Rust Playground

Normal Approach

Now, let's order our Seven Gods whatever you want.

#[derive(Debug)]
pub enum Gods {
    Father,
    Mother,
    Maiden,
    Crone,
    Warrior,
    Smith,
    Stranger,
}

fn my_gods_order(god:&Gods) -> u32{
    match god {
        Gods::Father => 1,
        Gods::Mother => 2,
        Gods::Maiden => 3,
        Gods::Crone => 4,
        Gods::Warrior => 5,
        Gods::Smith => 6,
        Gods::Stranger => 7,
    }
}

fn your_gods_order(god:&Gods) -> u32{
    match god {
        Gods::Father => 7,
        Gods::Mother => 6,
        Gods::Maiden => 5,
        Gods::Crone => 4,
        Gods::Warrior => 3,
        Gods::Smith => 2,
        Gods::Stranger => 1,
    }
}


fn main() {
    let mut gods = vec![Gods::Stranger, Gods::Father, Gods::Mother];
    gods.sort_by(|a, b| my_gods_order(a).partial_cmp(&my_gods_order(b)).unwrap());
    println!("{:?}", gods);

    let mut gods = vec![Gods::Smith, Gods::Crone, Gods::Stranger];
    gods.sort_by(|a, b| your_gods_order(a).partial_cmp(&your_gods_order(b)).unwrap());
    println!("{:?}", gods);

}

Use Macro

The more you have orders, you'll get the more and more match statemtns...
It works, but really annoying...

What you do....?

Macro...

That's Better...

macro_rules! gods_order1 {
    ($a:ident, $b:ident,  $c:ident) => {
        fn my_gods_order1(var: &Gods) -> u32 {
            match var {
                Gods::$a => 1,
                Gods::$b => 2,
                Gods::$c => 3,
                _ => 100,
            }
        }
    };
}

fn main() {
    gods_order1!(my_gods_order1, Stranger, Father, Mother);
    let mut gods = vec![Gods::Mother, Gods::Father, Gods::Stranger];
    gods.sort_by(|a, b| my_gods_order1(a).partial_cmp(&my_gods_order1(&b)).unwrap());
    println!("{:?}", gods);

    gods_order1!(your_gods_order1, Mother, Father, Stranger);
    let mut gods = vec![Gods::Mother, Gods::Father, Gods::Stranger];
    gods.sort_by(|a, b| your_gods_order1(a).partial_cmp(&your_gods_order1(&b)).unwrap());
    println!("{:?}", gods);

}

Even better...

macro_rules! gods_order2 {
    ($func:ident, [$(($elm:tt, $i:expr)),*]) => {
        fn $func(var: &Gods) -> u32 {
            match var {
                $(Gods::$elm => $i,)*
                _ => 0,
            }
        }
    };
}

fn main() {

    gods_order2!(my_gods_order2, [(Stranger, 3), (Father, 2), (Mother, 1)]);
    let mut gods = vec![Gods::Mother, Gods::Father, Gods::Stranger];
    gods.sort_by(|a, b| my_gods_order2(a).partial_cmp(&my_gods_order2(&b)).unwrap());
    println!("{:?}", gods);

    gods_order2!(your_gods_order2, [(Mother, 1), (Father, 2), (Stranger, 3)]);
    let mut gods = vec![Gods::Mother, Gods::Father, Gods::Stranger];
    gods.sort_by(|a, b| your_gods_order2(a).partial_cmp(&your_gods_order2(&b)).unwrap());
    println!("{:?}", gods);

}

Code on Rust Playground

Use Proc Macro

Now, what if we can define order from strings.

Let's say order is pre-defined in config.toml and macro parse the order and returns defined order.

First, do cargo new enum_builder .

In src/config.toml

order="Stranger,Smith,Warrior,Crone,Maiden,Mother,Father"

Since we use proc macro, we add followings in cargo.toml

[lib]
proc-macro = true

[dependencies]
syn = { version =  "0.15.42", features = ["extra-traits", "full"] }
proc-macro2 = "0.4.30"
quote = "0.6.12"
toml = "0.4.2"

Then, src/main.rs will be...

pub mod enum_builder {
    use enum_from_string::order;

    #[derive(Debug, order)]
    pub enum Gods {
        Father,
        Mother,
        Maiden,
        Crone,
        Warrior,
        Smith,
        Stranger,
    }
}

fn main() {
    assert_eq!(enum_builder::Gods::Father.order(), 6);
    assert_eq!(enum_builder::Gods::Stranger.order(), 0);
}

Finally, lib.rs is..

extern crate proc_macro;
extern crate toml;

use proc_macro::TokenStream;
use quote::quote;
use std::fs;
use std::io::{BufReader, Read};
use toml::Value;

use syn::{parse_macro_input, DeriveInput};

fn file_open(path: String) -> Result<String, String> {
    let mut file_content = String::new();

    let mut fr = fs::File::open(path)
        .map(|f| BufReader::new(f))
        .map_err(|e| e.to_string())?;

    fr.read_to_string(&mut file_content)
        .map_err(|e| e.to_string())?;

    Ok(file_content)
}

#[proc_macro_derive(order)]
pub fn order_builder(item: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(item as DeriveInput);
    let name = &ast.ident;
    let enum_variants = if let syn::Data::Enum(syn::DataEnum { variants, .. }) = ast.data {
        variants
    } else {
        unimplemented!();
    };
    let enum_len = &enum_variants.len();

    let order_config: toml::Value = file_open("src/config.toml".to_string())
        .unwrap()
        .parse::<Value>()
        .unwrap();

    let orders: Vec<&str> = order_config["order"].as_str().unwrap().split(",").collect();

    assert_eq!(&orders.len(), enum_len);

    let enum_fields = orders.iter().enumerate().map(|(i, elm)| {
        let elm_ident = syn::Ident::new(&elm, name.span());
        let arm = quote! {
            #name::#elm_ident => #i as i32,
        };
        arm
    });

    let order_func = quote! {
        impl #name {
            pub fn order(&self) -> i32{
                match self {
                    #(#enum_fields)*
                }
            }
        }
    };
    order_func.into()
}

Refs

For Proc macro basic...
https://github.com/azriel91/proc_macro_rules

For advanced proc macro learners
https://github.com/dtolnay/proc-macro-workshop

proc-macro-workshop has many fork repos and you can find good solutions, such as...
https://github.com/ELD/proc-macro-workshop
https://github.com/jonhoo/proc-macro-workshop
https://github.com/gobanos/proc-macro-workshop

Top comments (1)

Collapse
 
takaakifuruse profile image
TakaakiFuruse