#Macros lost me

1 messages · Page 1 of 1 (latest)

blazing perch
#

Context:

Hello i'm kinda new to rust and currently reading The Book .

At the same time i'm trying to play a bit with the language.
I am trying to do stuff with the rocket framework (just basic todo api).

Question

I was wondering if it was possible to write a macro #[controller] that can take a whole module inside like so :

#[controller(base = "/route")]
mod TodoController {
    #[get("/")]
    fn some_stuff() -> &'static str {
        return "yes"
    }
}

This macro should do the same as this but without all the "Boilerplate" :


#[get("/")]
pub fn todo_index() -> &'static str {
    "Todo Index"
}

inventory::submit! {
    crate::inventory::modules::Module {
        base: "/todo",
        routes: || rocket::routes![
            todo_index,
        ],
    }
}

would that be something possible with the macros ?
PS: yes i'm trying to simulate the same kind of things as in NestJs

gentle harness
#

needs to be procmacros

blazing perch
#

I didn't really understand the proc_macros

#

i read the docs and didn't understand

gentle harness
blazing perch
#

I understand the proc as so :
Rewrite the parsing of the code and add stuff

gentle harness
#

id just do it the normal way and not complicate myself with procmacros

blazing perch
#

yea fair but i want to see the limits of the famous rust macros hehe

#

but doable ?

#

holy GPT gave me this (but since i don't really understand i have no idea if its good or not)

use proc_macro::TokenStream;
use quote::quote;
use syn::{
    parse_macro_input, AttributeArgs, ItemMod, Lit, Meta, NestedMeta
};

#[proc_macro_attribute]
pub fn controller(args: TokenStream, input: TokenStream) -> TokenStream {
    // 1. Parse “base = "/todo"” from the attribute
    let args = parse_macro_input!(args as AttributeArgs);
    let mut base_value = None;
    for nested in args {
        if let NestedMeta::Meta(Meta::NameValue(m)) = nested {
            if m.path.is_ident("base") {
                if let Lit::Str(litstr) = &m.lit {
                    base_value = Some(litstr.value());
                }
            }
        }
    }
    let base = base_value
        .expect("expected #[controller(base = \"/foo\")]");

    // 2. Parse the module we’re annotating
    let module = parse_macro_input!(input as ItemMod);
    let mod_ident = &module.ident;

    // 3. Extract all fn names in that inline module
    let fns = module
        .content
        .as_ref()
        .expect("module must be inline")
        .1
        .iter()
        .filter_map(|item| {
            if let syn::Item::Fn(func) = item {
                Some(func.sig.ident.clone())
            } else {
                None
            }
        })
        .collect::<Vec<_>>();

    // 4. Build a Vec of `module::fn_name` tokens
    let route_paths = fns.iter().map(|f| {
        quote! { #mod_ident::#f }
    });

    // 5. Re-emit the original module + an inventory submission
    let expanded = quote! {
        #module

        inventory::submit! {
            crate::inventory::modules::Module {
                base: #base,
                routes: || rocket::routes![ #(#route_paths),* ],
            }
        }
    };

    TokenStream::from(expanded)
}
gentle harness
blazing perch
#

yup i'll try

#

was wondering if it was bad code or not (not in a sense that it doesn't compile but in term of quality)

#

thx for your time 🙂

bleak pine
#

Proc macros are definitely an advanced topic, and you should probably wait until you're comfortable in the language itself to learn them, but they're really cool and awesome, so I totally understand why you'd want to learn them immediately.

#

The gist is that you can do literally anything, if you can express it as a function from an input token stream to an output token stream

#

kinda, I think

#

I'm not super confident in my ability to write them myself

#

like, you can't make a macro output branches of a match block for some reason (by which I mean "there's probably a good reason, I just don't know what it is"), iirc

#

but it seems like it should be possible, there are standard macros that take in a whole module after all

gentle harness
#

why did the don’t turn into donÄt

bleak pine
#

oh that's just a typo bc I'm german and the german letter ä is right next to the key which I need to shift+press to type '

cloud prairie
# blazing perch holy GPT gave me this *(but since i don't really understand i have no idea if it...

please don't use ai-generated code that you don't understand fully what it does, you're just hampering your own learning and painting yourself into a corner by depending on code that you don't know how to debug or fix if something goes wrong

i'd recommend giving this video a watch, it's a great introduction to proc macros: https://www.youtube.com/watch?v=SMCRQj9Hbx8

Rust procedural macros can do amazing things, including implementing an entire dang Python feature from scratch. I've wanted to make this "lecture+tutorial" combo video for about a year and it took me about that long to get around to doing it--hope you enjoy and learn something.

We'll deep dive about macros in general, some compiler architectur...

▶ Play video
#

the video specifically goes over function-like proc macros, ones that you invoke like macro!(..), but the same ideas apply to the #[macro]-style attribute macros that you're asking about

#

the tl;dr is that a proc macro is simply a function that takes a list of tokens as input and returns a new list of tokens to replace the original code. in principle you could interact with those raw streams of tokens manually, but most people use the syn crate to parse the input and quote to generate the output, which makes your life a lot easier

#

proc macros aren't very difficult in concept, but it's very easy to create very spaghettified code if you're not careful, which is why i like the approach that the video i linked above uses of defining an intermediate representation, which results in much cleaner code

blazing perch
#

i mean i use it like that

cloud prairie
#

it's not really, you have no idea how idiomatic or up to date the code chatgpt gives you is

#

and it's liable to give you bad information that makes our work harder by having to correct what it's taught you

blazing perch
#

I know haha working with LLM is kinda my work x)

#

that why i was asking ahah but i'll look at the video

#

47min ow lowd

cloud prairie
#

i'm going to guess that you've already spent more than 47 min trying to research this/get this working, so compared to the amount of time you'll probably save by watching it i'd think it's worth it

blazing perch
#

just realized that it was a video I had started

blazing perch
#

just havin some fun with rocket atm

#

thank all for your time 🙂

blazing perch
#

ok so i made it work

#

here is the full macro code :

extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;
use syn::{
    ItemMod, Lit, Token,
    parse::{Parse, ParseStream},
    parse_macro_input,
};

struct ControllerArgs {
    base: String,
}

impl Parse for ControllerArgs {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut base = None;

        while !input.is_empty() {
            let ident: syn::Ident = input.parse()?;

            if ident == "base" {
                input.parse::<Token![=]>()?;
                let lit: Lit = input.parse()?;

                match lit {
                    Lit::Str(s) => base = Some(s.value()),
                    _ => {
                        return Err(syn::Error::new_spanned(
                            lit,
                            "`base` must be a string literal",
                        ));
                    }
                }
            } else {
                return Err(syn::Error::new_spanned(ident, "unknown parameter"));
            }

            if input.peek(Token![,]) {
                input.parse::<Token![,]>()?;
            }
        }

        match base {
            Some(b) => Ok(ControllerArgs { base: b }),
            None => Err(syn::Error::new(input.span(), "missing `base = \"/...\"`")),
        }
    }
}

fn is_route_attr(path: &syn::Path) -> bool {
    if let Some(ident) = path.get_ident() {
        matches!(
            ident.to_string().as_str(),
            "get" | "post" | "put" | "delete" | "patch" | "head" | "options"
        )
    } else {
        false
    }
}
#
#[proc_macro_attribute]
pub fn controller(attr: TokenStream, item: TokenStream) -> TokenStream {
    let args = parse_macro_input!(attr as ControllerArgs);
    let base = args.base;

    let mut module = parse_macro_input!(item as ItemMod);

    let (brace_token, items) = match &mut module.content {
        Some((brace, items)) => (brace, items),
        None => {
            return syn::Error::new_spanned(
                &module,
                "#[controller] must be applied to an inline `mod { ... }`",
            )
            .to_compile_error()
            .into();
        }
    };

    let mut route_paths = Vec::new();
    for item in items.iter() {
        if let syn::Item::Fn(func) = item {
            if func.attrs.iter().any(|attr| is_route_attr(attr.path())) {
                let fn_ident = &func.sig.ident;
                let mod_ident = &module.ident;
                route_paths.push(quote!(#mod_ident::#fn_ident));
            }
        }
    }

    if route_paths.is_empty() {
        return syn::Error::new_spanned(&module.ident, "no Rocket routes found in this module")
            .to_compile_error()
            .into();
    }

    let routes_vec = quote!(#(#route_paths),*);

    let expanded = quote! {
        #module

        inventory::submit! {
            crate::inventory::modules::Module {
                base: #base,
                routes: || rocket::routes![#routes_vec],
            }
        }
    };

    expanded.into()
}
#

feels so cursed to write some code to manipulate the tokens that the compiler will parse

cyan totem