438: format_args! for Zero-Alloc Formatting
Difficulty: 3 Level: Advanced Capture format arguments lazily without allocating a `String` โ the low-level primitive that `println!`, `format!`, and logging frameworks build on.The Problem This Solves
`format!("{} = {}", key, value)` always allocates a `String`, even if you're just going to write it to a buffer immediately. In hot loops, logging infrastructure, or embedded systems where the heap is precious, this allocation is waste. You want to describe what to format without paying the allocation cost until the bytes are actually written somewhere. `format_args!` is the compile-time macro that captures format arguments as a `fmt::Arguments` struct โ a lazy description of the formatting operation. No allocation happens. You pass `fmt::Arguments` to any `Write` implementor (`File`, `TcpStream`, your custom buffer) and it formats directly into the destination. `println!`, `format!`, `write!`, and `log!` all use `format_args!` internally. This matters when building logging, tracing, or I/O infrastructure. Accept `fmt::Arguments` in your API and callers get zero-alloc formatting for free.The Intuition
`format_args!` captures a format expression as a value that describes the formatting to do, without doing it โ allocation happens only when you write it to a concrete output.How It Works in Rust
use std::fmt;
use std::fmt::Write;
// format_args! returns fmt::Arguments โ no allocation
let args = format_args!("{} + {} = {}", 1, 2, 3);
// Nothing allocated yet โ args is a stack value
// Write to a String (allocates only here)
let mut s = String::new();
write!(s, "{}", args).unwrap(); // "1 + 2 = 3"
// Write to stderr directly โ no intermediate String
eprintln!("{}", args); // format_args! inside
// Accept fmt::Arguments in your own API โ zero-alloc logging
fn log(level: &str, args: fmt::Arguments<'_>) {
// Could write to file, buffer, network โ whatever
println!("[{}] {}", level, args);
}
// Macro wrapper so callers use familiar syntax
macro_rules! my_log {
($level:expr, $($arg:tt)*) => {
log($level, format_args!($($arg)*))
};
}
my_log!("INFO", "user {} logged in from {}", user_id, ip);
// โ No String allocation โ format_args captures the args, log() writes them
// format_args! in a struct for deferred formatting
struct Lazy<'a>(fmt::Arguments<'a>);
impl<'a> fmt::Display for Lazy<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(self.0)
}
}
1. `format_args!(...)` โ same syntax as `format!` but returns `fmt::Arguments` instead of `String`.
2. `fmt::Arguments` is a stack value referencing the original arguments โ no heap allocation.
3. Pass to `write!`, `writeln!`, `print!`, or any `fmt::Write`/`io::Write` implementor.
4. Build macro wrappers: accept `$($arg:tt)` and forward to `format_args!($($arg))`.
What This Unlocks
- Zero-allocation logging: Build log macros that format directly to a sink without intermediate `String`.
- Deferred formatting: Capture what to format, decide where to write it later.
- Efficient I/O: Write formatted output directly to `TcpStream`, `File`, or custom buffers without intermediate allocation.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Format without allocation | `Format.fprintf` with `formatter` | `format_args!` + `fmt::Write` |
| Deferred formatting | `Format.kdprintf` continuation-passing | `fmt::Arguments` value โ pass around |
| Format to sink | `Format.fprintf out_channel` | `write!(sink, "{}", args)` |
| Building format macros | `Format.kasprintf` | `macro_rules!` + `format_args!($($arg)*)` |
| String allocation | `Format.asprintf` | `format!` (allocates); `format_args!` (doesn't) |
// format_args! for zero-alloc formatting in Rust
use std::fmt::{self, Write};
// Custom Write target (stack-allocated buffer)
struct StackBuf {
buf: [u8; 256],
len: usize,
}
impl StackBuf {
fn new() -> Self { StackBuf { buf: [0; 256], len: 0 } }
fn as_str(&self) -> &str {
std::str::from_utf8(&self.buf[..self.len]).unwrap_or("")
}
}
impl fmt::Write for StackBuf {
fn write_str(&mut self, s: &str) -> fmt::Result {
let bytes = s.as_bytes();
let new_len = self.len + bytes.len();
if new_len > self.buf.len() { return Err(fmt::Error); }
self.buf[self.len..new_len].copy_from_slice(bytes);
self.len = new_len;
Ok(())
}
}
// Logger that formats without allocating a String
struct Logger { prefix: &'static str }
impl Logger {
fn log(&self, args: fmt::Arguments) {
// Write directly to stderr without String allocation
let mut buf = StackBuf::new();
write!(buf, "[{}] ", self.prefix).ok();
buf.write_fmt(args).ok();
eprintln!("{}", buf.as_str());
}
}
macro_rules! log_info {
($logger:expr, $($arg:tt)*) => {
$logger.log(format_args!($($arg)*))
};
}
// format_args! enables writing to multiple sinks
fn write_to_all(args: fmt::Arguments) {
// Write to stdout
println!("stdout: {}", args);
// Write to a String buffer
let mut s = String::new();
write!(s, "{}", args).unwrap();
println!(" (captured: {:?})", s);
// Write to stack buffer
let mut buf = StackBuf::new();
buf.write_fmt(args).ok();
println!(" (stack buf: '{}')", buf.as_str());
}
// Deferred formatting
fn log_if<'a>(condition: bool, msg: fmt::Arguments<'a>) {
if condition {
println!("Condition true: {}", msg);
}
// If false, the format is never evaluated
}
fn main() {
let logger = Logger { prefix: "INFO" };
// format_args! โ no heap allocation
log_info!(logger, "Server started on port {}", 8080);
log_info!(logger, "Request from {} user agent: {}", "127.0.0.1", "curl/7.0");
// Direct format_args usage
let name = "World";
let n = 42;
write_to_all(format_args!("Hello, {}! Value = {}", name, n));
// Lazy evaluation โ format only happens if condition is true
log_if(true, format_args!("Count: {}", 100));
log_if(false, format_args!("Expensive: {}", (0..1000000).sum::<i64>())); // lazy!
// Stack buffer โ truly zero allocation
let mut buf = StackBuf::new();
write!(buf, "Pi = {:.6}", std::f64::consts::PI).unwrap();
println!("Stack formatted: {}", buf.as_str());
// Demonstrate: format! vs format_args!
let _with_alloc: String = format!("Hello, {}!", name); // allocates
// format_args!("Hello, {}!", name) // no allocation โ lazy
println!("format! allocates, format_args! doesn't!");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stack_buf() {
let mut buf = StackBuf::new();
write!(buf, "Hello, {}!", "Rust").unwrap();
assert_eq!(buf.as_str(), "Hello, Rust!");
}
#[test]
fn test_format_args_zero_alloc() {
let mut buf = StackBuf::new();
let args = format_args!("value = {}", 42);
buf.write_fmt(args).unwrap();
assert_eq!(buf.as_str(), "value = 42");
}
}
(* format_args! concept in OCaml *)
(* OCaml's Format module has similar deferred formatting *)
let format_to_buffer buf fmt_fn =
let b = Buffer.create 64 in
let f = Format.formatter_of_buffer b in
fmt_fn f;
Format.pp_print_flush f ();
Buffer.add_buffer buf b
(* Format without allocation to a custom formatter *)
let to_stderr fmt_fn =
let f = Format.formatter_of_out_channel stderr in
fmt_fn f;
Format.pp_print_flush f ()
let with_prefix prefix fmt_fn =
let b = Buffer.create 64 in
let f = Format.formatter_of_buffer b in
fmt_fn f;
Format.pp_print_flush f ();
prefix ^ Buffer.contents b
let () =
let buf = Buffer.create 64 in
format_to_buffer buf (fun f ->
Format.fprintf f "Hello, %s! Value = %d" "World" 42
);
Printf.printf "Buffer: %s\n" (Buffer.contents buf);
let msg = with_prefix "[INFO] " (fun f ->
Format.fprintf f "Server started on port %d" 8080
) in
Printf.printf "%s\n" msg