DEV Community

TakaakiFuruse
TakaakiFuruse

Posted on • Edited on

1

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

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (1)

Collapse
 
takaakifuruse profile image
TakaakiFuruse

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

👋 Kindness is contagious

If this article connected with you, consider tapping ❤️ or leaving a brief comment to share your thoughts!

Okay