Search code examples
rustrust-cargocompile-timerust-proc-macros

How can I handle a string generated from macro within procedural macro?


I use include_str!() to import the string from a file and pass it into a proc_marco, but it does not work. I get an expected string literal error. Here is my code:

macro_rules! ptcl_layer {
    () => {
        include_str!("tcp_to_msg.layer")
    };
}

pub static TEMPLETE: &'static str = ptcl_layer!();
pub static TEMPLETE_HEADER: &'static str = get_protocol_layer_header!(ptcl_layer!());
pub static TEMPLETE_END: &'static str = get_protocol_layer_end!(ptcl_layer!());
#[proc_macro]
pub fn get_protocol_layer_header(_s: TokenStream) -> TokenStream {
    let total_str = parse_macro_input!(_s as LitStr).value();
    let header = total_str.lines().next().unwrap();
    let header = header.replace("${.LEN}\n", "");
    let output = quote::quote! {
        #header
    };
    TokenStream::from(output)
}

#[proc_macro]
pub fn get_protocol_layer_end(_s: TokenStream) -> TokenStream {
    let input = parse_macro_input!(_s as LitStr);
    let input = input.value();
    let last_line = input.lines().last().unwrap_or("");
    let output = quote::quote! {
        #last_line
    };
    
    TokenStream::from(output)
}

The full error msg:

error: expected string literal
  --> src/controller/mod.rs:16:71
   |
16 | pub static TEMPLETE_HEADER: &'static str = get_protocol_layer_header!(ptcl_layer!());
   |                                                                       ^^^^^^^^^^

And the *.layer like:

<? lines: ${.LEN}
${.CONTENT}
<? end

I also tried another version of the code:

#[proc_macro]
pub fn get_protocol_layer_header(_s: TokenStream) -> TokenStream {
    let total_str = _s.to_string();
    let header = total_str.lines().next().unwrap();
    let header = header.replace("${.LEN}\n", "");
    header.parse().unwrap()
}

#[proc_macro]
pub fn get_protocol_layer_end(_s: TokenStream) -> TokenStream {
    let total_str = _s.to_string();
    let last_line = total_str.lines().last().unwrap_or("");
    last_line.parse().unwrap()
}

But it returned the original text without any processing.


Solution

  • As eqqyal noted, by default macros are evaluated outside in, but when some expression will result in a literal, like in your case, you can invert that using the unstable TokenStream::expand_expr:

    at the top of your_macros/src/lib.rs:

    #![feature(proc_macro_expand)]
    

    in your definitions that take a LitStr insert the following:

    #[proc_macro]
    pub fn get_protocol_layer_header(input: TokenStream) -> TokenStream {
        let input = input.expand_expr().unwrap();
        let total_str = parse_macro_input!(input as LitStr).value();
        // …
    

    This is something the outermost macros you want to use must accept so if you can't change the definiton of get_protocol_layer_header and similar, or can't use nightly you'd have to work around it and insert the corresponding string literals with a build.rs srcipt.