Passing file name dynamically to a procedural macro as argument

I am working on a Rust macro that needs to accept the file name as a argument. Based on the file name I want to parse and do some operations and generate some code. I believe proc_macro in Rust should help me achieve my requirements. However, I am facing issues passing file!() as argument to the procedural macro. The idea is to get the file name via the file!() macro and pass it as argument to my procedural macro. This is my current implementation:

// lib.rs - proc_macro crate

#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as LitStr);

    // perform operation on input string


    let expanded = quote! {
        #output_str
    };

    TokenStream::from(expanded)
}
fn main() {
    let dir_name =  my_macro!(file!());
    println!("Dir name: {:?}", dir_name);
}

With this approach, I am getting compiler error:

error: expected string literal
  --> src/main.rs
   |
12 |     let dir_name =  my_macro!(file!());
   |                               ^^^^

If I pass in the static strings, it works, for example:

let dir_name =  my_macro!("my_string");

I am not sure how to pass the file name dynamically to procedural macros. Is there any other way to achieve this? Any help/reference is appreciated.

file!() is an expression that expands to a string literal. Your proc_macro can parse it as an expression and then evaluate it.

#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as Expr);

    let expanded = quote! {{
        let file = #input;
        println!("{file}");

        file
    }};

    TokenStream::from(expanded)
}

More context: Procedural macros receive a sequence of tokens (token stream). The compiler does not resolve nested macros the same way it calls nested functions. Macro resolution is "inverted" in this regard. Macro invocations are resolved recursively; the file!() expression is produced in its output token stream, and the compiler resolves it later.

You can see this by adding:

println!("Macro input: `{}`", stringify!(#input));

Prints:

Macro input: `file! ()`

Please, don't repost post StackOverflow questions here, without a link back to SO.

2 Likes

Does this actually expand to a path of the file callsite is located in? I would be significantly surprised if it did.

I added more context to my answer.

Yes, it does. It prints src/main.rs. Here's the cargo expand output:

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2024::*;
#[macro_use]
extern crate std;
use promac::my_macro;
fn main() {
    let dir_name = {
        {
            ::std::io::_print(format_args!("{0}\n", "file! ()"));
        };
        let file = "src\\main.rs";
        {
            ::std::io::_print(format_args!("{0}\n", file));
        };
        file
    };
}

Ah, makes sense.

Your proc_macro can parse it as an expression and then evaluate it.

I disagree with this wording though, the aim is to paste file!() into the output verbatim, not to expand it and paste the result, as the word "evaluate", to me, would imply.

That's very fair! The compiler still does the evaluation. It just occurs outside of your macro. I find it interesting that proc macros are just token processors. That's the real takeaway, here.

Thank @parasyte ! This does help me achieve my goal for the macro. Much appreciated!

One follow-up question to understand better, in Rust, if we have expressions that are compile time constants, such as this parsing of base directory from file!(), should we use these proc_macros approach or use constant functions? As I understand with my limited knowledge in Rust, constant functions gets evaluated compile time and help reduce the runtime overhead.

Apologies @HJVT , I did not know of the convention. I will take care for any other questions going forward.

Macros and const eval can both achieve similar levels of reduction to runtime overhead. Const eval is still a work in progress and may not have all of the necessary machinery stabilized. Const eval is potentially better for ergonomics as long as your use case is part of the language subset that is eligible for constant evaluation.

It might be obvious, but I should also point out that const fn is not a guarantee that the function runs only at compile time. The modifier relaxes constraints in the type system to enable evaluation in constant contexts [1]. E.g., assigning the result of a const fn call to a const value. See: When are Rust's const fns executed? Otherwise, it's just a normal function call at runtime.

Also note that I'm not super familiar with const eval in Rust, having used it only sparingly. I don't really know the full scope of what's currently supported or the maximal subset that can be evaluated at compile-time. Procedural macros don't have the same limitations; macros can run any Rust code at compile-time (including I/O)! It's an unusual way to do metaprogramming and it doesn't have any type system information (no reflection), but it's quite capable regardless.


  1. This relaxation is a result of constraining the types that the function is allowed to accept and return, and the body can only contain constant expressions. â†Šī¸Ž

I see, looks like the proc_macros is the way to go then for now. Thank you for the insights @parasyte !

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.