When the file needs to travel with the code
You are building a command-line tool that generates reports. The report format depends on a template file. You zip the binary and send it to a colleague. They run it. The tool panics. The template file isn't there. Your colleague didn't copy the template. They only copied the binary. You could document that users must keep the template in the same directory. Or you could embed the template inside the binary so it is always available.
That is the job for include_str! and include_bytes!. These macros read files during compilation and embed their contents directly into the executable. The file becomes part of the binary. No external dependencies. No missing file errors at runtime.
Think of it like baking a cake. The recipe isn't a separate card you keep in your pocket. The ingredients are mixed into the batter. The cake carries the recipe's result with it everywhere you take it. include_str! bakes text files into your code. include_bytes! bakes binary files.
How the compiler bakes files into your binary
When the compiler processes your crate, it encounters the macro. It stops parsing the current file. It resolves the path relative to the crate root. It opens the file. It reads the content. It inserts the content as a literal value into the abstract syntax tree. The macro call disappears. In its place sits a string literal or byte array. The rest of the compilation proceeds as if you had typed the content by hand.
// The path is relative to the crate root, not this file.
// The compiler reads this file and embeds the text here.
const DEFAULT_CONFIG: &str = include_str!("config/default.toml");
fn main() {
// No file I/O happens at runtime.
// The string is already in memory.
println!("{}", DEFAULT_CONFIG);
}
The result is a &'static str or &'static [u8]. The lifetime is 'static. The data lives in the binary's read-only memory segment. The OS loads it when the program starts. You never allocate this memory on the heap. You never free it. The pointer is valid for the entire lifetime of the process.
This has performance implications. Accessing the data is a memory read. No file I/O. No disk latency. No blocking. The data is already in RAM. If you parse the string, you allocate a new structure, but the source data stays put. This is faster than reading from disk every time.
Check your crate root before blaming the compiler.
Memory layout and the static lifetime
The embedded data ends up in the .rodata section of the binary. This section is marked read-only by the operating system. If you try to mutate the data through a mutable reference, the program crashes with a segmentation fault. The compiler prevents this by giving you an immutable reference. You get &'static str, not &'static mut str.
The 'static lifetime means the reference is valid as long as the program runs. You can return this reference from any function. You can store it in a struct. You can pass it to threads. The data never moves. It never gets dropped. This eliminates a whole class of lifetime errors. You don't need to track when the file content goes out of scope. It never does.
Convention aside: use const for simple inclusions. The compiler inlines the value at every use site. This can increase binary size if the string is large and used many times. Use static if you want a single location in memory. The convention is const for readability unless profiling shows inlining is a problem.
Don't fight the read-only memory. If you need mutation, parse the data into a mutable structure.
Real-world patterns
Embedding files is common for configuration defaults, templates, certificates, and small assets. Here is a realistic pattern for loading a default configuration.
use serde::Deserialize;
// Embed the config at compile time.
// The path is explicit from the crate root.
const DEFAULT_CONFIG: &str = include_str!("config/default.toml");
#[derive(Deserialize)]
struct AppConfig {
port: u16,
host: String,
}
/// Returns the default configuration parsed from the embedded file.
fn load_default_config() -> AppConfig {
// The string is in memory. Parsing is the only cost.
// The compiler guarantees the file exists and is valid UTF-8.
toml::from_str(DEFAULT_CONFIG).expect("Default config must be valid TOML")
}
For binary data, like a TLS certificate, use include_bytes!.
// Embed the certificate as raw bytes.
// Certificates are often DER or PEM encoded binary data.
const CERT_PEM: &[u8] = include_bytes!("certs/server.pem");
/// Returns the certificate bytes for the TLS config.
fn get_cert() -> &'static [u8] {
CERT_PEM
}
Convention aside: use include_bytes! for TLS certificates. Embedding the cert avoids runtime path issues and keeps the deployment simple. You don't need to ship a separate cert file. The binary is self-contained.
If it is not UTF-8, switch to bytes.
Pitfalls and compiler errors
The most common mistake is path confusion. The path is relative to the crate root, not the source file containing the macro. If you put include_str!("data.txt") in src/lib.rs, the compiler looks for data.txt at the root of the project, not src/data.txt. The compiler rejects this with a "file not found" error during macro expansion. This happens before type checking. The error message points to the macro invocation.
Another trap is encoding. include_str! requires valid UTF-8. If the file contains binary data or invalid sequences, compilation fails. The compiler reports "invalid UTF-8 in file". Use include_bytes! for non-text files. If you try to assign include_bytes! to a &str, the compiler rejects this with E0308 (mismatched types). The types don't match. Bytes are not strings.
Large files cause problems too. Including a 100MB file makes your binary 100MB larger. Compilation slows down. Memory usage spikes. The compiler has to read the file and embed it. Don't include huge files. Use these macros for configuration, templates, certificates, and small assets. For large data, load it from disk at runtime or stream it.
Build scripts interact with these macros. You can use build.rs to generate a file, then include_str! it. The build script runs, writes generated.rs, then the main code includes it. This bridges the gap between build-time computation and runtime code. The generated file must exist before the main compilation starts. The build script handles that dependency.
Trust the borrow checker. It usually has a point.
Decision: when to use this vs alternatives
Use include_str! when you need text content embedded in the binary and the file is guaranteed to be valid UTF-8. Use include_bytes! when you need raw binary data, like certificates, images, or non-UTF-8 text. Use std::fs::read_to_string when the file might change after deployment or when the path is dynamic. Use include! when you want to include Rust source code, not data files.