Compile-time strings and metadata
You are building a CLI tool. You need the version number embedded in the binary so --version works instantly. You need to join a few constant path segments into a single string for a config file lookup. You are writing a macro and want to report the exact code the user typed when something goes wrong.
Reaching for runtime string manipulation works, but it costs cycles and memory. Rust provides three built-in macros that handle these tasks at compile time: concat!, stringify!, and env!. They transform code before the compiler produces the final executable. The result is zero runtime overhead and data that lives permanently in the binary.
The compile-time advantage
Macros run during compilation. When you write concat!("a", "b"), the macro executes while the compiler is processing your file. It produces a single string literal "ab". The compiler then sees that literal and embeds it directly into the binary. There is no string concatenation at runtime. There is no allocation. There is no function call.
Think of these macros as instructions for the compiler's factory floor. Instead of assembling strings on the assembly line while the program runs, you weld them together in the mold before production starts. The final product has the strings already fused into the metal.
These macros also return &'static str. The lifetime 'static means the string data lives for the entire duration of the program. It is stored in the read-only section of the executable. You can pass these strings anywhere without worrying about ownership, borrowing, or dropping. They are safe, fast, and immutable.
Minimal examples
Here is how each macro behaves in isolation. Copy this into a main.rs file to see the output.
fn main() {
// concat! joins string literals at compile time.
// The result is a single literal, not a runtime operation.
let greeting = concat!("Hello, ", "world!");
// stringify! captures the source text of an expression.
// It returns the code as a string, not the result of the code.
let expr_text = stringify!(x + y);
// env! reads an environment variable at compile time.
// CARGO_PKG_VERSION is set by Cargo automatically.
// If the variable is missing, compilation fails.
let version = env!("CARGO_PKG_VERSION");
println!("Greeting: {}", greeting);
println!("Expression: {}", expr_text);
println!("Version: {}", version);
}
Run this with cargo run. You will see:
Greeting: Hello, world!
Expression: x + y
Version: 0.1.0
The output confirms the behavior. concat! merged the literals. stringify! returned the text "x + y", not a number. env! pulled the version from Cargo's metadata.
Why &'static str matters
The return type of these macros is &'static str. This is a critical detail for performance and safety.
When you use format!("{} {}", a, b), Rust allocates a String on the heap. The program must manage that memory. When the String goes out of scope, the memory is freed. This allocation happens every time the code runs.
concat! and env! produce &'static str. The string data is baked into the binary. No heap allocation occurs. No memory is freed. The pointer points to a fixed location in the executable's read-only segment. This makes these macros ideal for constants, error messages, and metadata that never changes.
Use concat! and env! whenever you can construct the string from literals and compile-time variables. Reserve format! for cases where you must interpolate runtime values.
Bake it in. Don't allocate it.
Realistic usage: Build info and config
A common pattern is to gather build metadata into a struct. This struct can be queried at runtime to display version info, target architecture, or custom build flags.
use std::env;
/// Metadata about the build environment.
/// All fields are &'static str, so no allocation is needed.
struct BuildInfo {
pub version: &'static str,
pub target: &'static str,
pub debug_mode: bool,
}
impl BuildInfo {
/// Creates a BuildInfo struct using compile-time macros.
/// env! panics if the variable is not defined at compile time.
pub const fn new() -> BuildInfo {
BuildInfo {
// CARGO_PKG_VERSION is always available in Cargo projects.
version: env!("CARGO_PKG_VERSION"),
// CARGO_CFG_TARGET_ARCH is set by Cargo based on the target triple.
target: env!("CARGO_CFG_TARGET_ARCH"),
// cfg! is a compile-time macro that returns true or false.
debug_mode: cfg!(debug_assertions),
}
}
}
fn main() {
// BuildInfo::new() is a const fn, so it can be used in constants.
// The compiler evaluates this at compile time.
const INFO: BuildInfo = BuildInfo::new();
println!("Version: {}", INFO.version);
println!("Target: {}", INFO.target);
println!("Debug: {}", INFO.debug_mode);
}
This code demonstrates several points. env! accesses Cargo's built-in environment variables. cfg! checks compile-time configuration flags. The const fn allows the struct to be created at compile time, reinforcing that no runtime work is needed.
You can also set custom environment variables in a build.rs script and read them with env!. This is how crates inject build-specific data.
// In build.rs
fn main() {
// Tell the compiler to define MY_BUILD_FLAG for this crate.
println!("cargo:rustc-env=MY_BUILD_FLAG=custom_value");
}
// In lib.rs or main.rs
const FLAG: &str = env!("MY_BUILD_FLAG");
The build.rs script runs before the crate compiles. It prints instructions to Cargo, which translates them into environment variables. env! then reads those variables. If build.rs forgets to set the variable, env! causes a compile error. This ensures the build is consistent.
If env! fails, the build fails. That is a feature. It prevents broken binaries from reaching users.
Pitfalls and gotchas
These macros are simple, but they have strict rules. Violating the rules produces compile errors, not runtime bugs.
concat! requires literals
concat! only accepts string literals. You cannot pass variables or expressions.
let name = "Rust";
// This fails to compile.
// Error: concat! requires string literals
let bad = concat!("Hello, ", name);
The compiler rejects this because concat! must produce a literal at compile time. It cannot evaluate name. Use format! when you need to include variables.
stringify! does not evaluate
stringify! captures the source text. It does not run the code.
let result = stringify!(1 + 1);
// result is "1 + 1", not "2".
This is intentional. stringify! is used to inspect code, not compute values. If you need the result, evaluate the expression normally. If you need the text, use stringify!.
env! panics on missing variables
env! expects the environment variable to exist at compile time. If it does not, compilation stops.
// This fails if SECRET_KEY is not set in the build environment.
let key = env!("SECRET_KEY");
This is the correct behavior for required configuration. If the variable is missing, the code cannot work. Failing early is better than failing at runtime.
For optional variables, use option_env!. It returns Option<&'static str>.
// Returns Some("value") if set, None if missing.
let optional = option_env!("OPTIONAL_FLAG");
Convention dictates using env! for variables that must exist for the build to make sense, and option_env! for variables that are optional. Do not use env! for optional data. Use option_env! and handle the None case.
stringify! and macro expansion
stringify! stringifies the tokens you pass to it. It does not expand macros inside the argument.
macro_rules! get_value {
() => { 42 };
}
// This produces "get_value!()", not "42".
let text = stringify!(get_value!());
If you need the expanded result, you cannot use stringify! directly. This limitation matters when writing complex macros.
Treat stringify! as a mirror for source code, not a calculator.
Decision matrix
Choose the right tool based on your needs. The decision depends on whether you have literals, variables, or build-time data.
Use concat! when you need to join string literals and want zero runtime overhead. Use concat! for constructing constant paths, version strings, or error messages from fixed parts. Use format! when you need to interpolate variables or non-literal values. Use format! for dynamic messages that depend on runtime state. Use stringify! when you need the source code text of an expression for debugging, error reporting, or code generation. Use stringify! inside macros to report user input or to generate documentation. Use env! when a build depends on an environment variable and compilation should fail if it is missing. Use env! for required metadata like version numbers or target architecture. Use option_env! when the environment variable is optional and you want to handle the absence gracefully. Use option_env! for feature flags or optional configuration that may not be set. Use std::env::var when you need to read environment variables at runtime. Use std::env::var for configuration that changes between runs or depends on the deployment environment.
Keep compile-time and runtime concerns separate. Use macros for what is known at compile time. Use functions for what is known at runtime.