← Home

Rust's std macros like include_str! and env! have a superpower, and how can we achieve it?

27 February, 2025

So you can use the concat! macro to pass string literals into macros:

env!("ab")
env!(concat!("a", "b"))

Both of the above calls will become env!("ab")

However, a macro created using the syn crate that expects a single syn::LitStr (literal string) as its input, such as Dioxus's asset! macro:

asset!("/assets/icons/github.svg")

You aren't able to pass concat! to this macro for some reason.

This does not compile: Expected a string literal.

asset!(concat!("/assets/icons/", "github", ".svg"))

This seems to be because the asset! macro expects a string literal. But is concat!("...", "...") a string literal? Since you can use it inside of concat!(): concat!(concat!("")), it feels like it is. But the macro has to be first expanded and then passed inside of the macro

What I'm seeing is that Rust's own macros like include_str! and env! have a special "superpower" where they can expand the concat! macro before parsing. However, user-created declarative macros using the syn crate are unable to.

These macros get the raw TokenStream corresponding to the tokens in "concat!("/assets/icons/", "github", ".svg"))"

Thankfully, the amazing David Tolnay shared a pretty cool crate called macro-string which allows creating a derive macro which eagerly evaluates the following std macros, at the time of writing:

Meaning you'll be able to pass them into macros that expect a string literal, and they'll get expanded before!