Zero‑Cost Fixed‑Size Buffers in Rust with Const Generics
Learn how Rust const generics let you enforce fixed array lengths at compile time, producing zero‑cost abstractions for buffers and drivers.
02 Oct 2025, 17:35 UTC

The Problem: Runtime Overhead for Known Sizes
When writing drivers or tight loops you often need a buffer whose size is known at compile time – for example a 256‑byte receive ring for a UART. Using a plain [u8; N] array works, but if you want to expose the buffer through a generic API you either lose the size information (using Vec) or resort to unsafe transmutes. Both approaches add either runtime checks or the risk of undefined behaviour.
Thesis: Const Generics Give Compile‑Time Lengths
Rust’s const generics let you treat a usize value as a type parameter. The compiler monomorphizes the code for each concrete length, so the generated machine code contains no length checks or heap allocations – the abstraction is zero‑cost.
Worked Example: A UART‑Style Stack
use core::marker::PhantomData;
/// A simple LIFO stack backed by a fixed‑size array.
pub struct Stack {
buf: [u8; N],
len: usize,
}
impl Stack {
/// Create an empty stack.
pub const fn new() -> Self {
Self { buf: [0; N], len: 0 }
}
/// Push a byte, returning an error if the stack is full.
pub fn push(&mut self, byte: u8) -> Result<(), ()> {
if self.len >= N {
return Err(());
}
self.buf[self.len] = byte;
self.len += 1;
Ok(())
}
/// Pop a byte, returning None if the stack is empty.
pub fn pop(&mut self) -> Option {
if self.len == 0 {
return None;
}
self.len -= 1;
Some(self.buf[self.len])
}
}
fn main() {
let mut s = Stack::<32>::new();
s.push(0xAA).unwrap();
assert_eq!(s.pop(), Some(0xAA));
}
To verify that no runtime length checks appear, compile with cargo build --release and inspect the generated assembly:
$ objdump -d target/release/your_binary | grep -A2 -B2 "push\|pop"
You will see only straight‑line moves and increments; the comparison if self.len >= N becomes a compile‑time constant when N is known, so the branch is eliminated for constant‑propagation optimizations.
Trade‑offs and Limits
- Each distinct
Ncreates a separate monomorphized copy ofStack, increasing compile time and binary size. - Only values that the compiler can evaluate as const expressions may be used (literals,
const fnresults, orconst { expr }blocks). Runtime values cannot become a const generic argument. - Debugging const‑generic errors can be awkward because the error points to the definition site; you must inspect the call site to see which
Ntriggered the problem.
Actionable Advice
- Start by wrapping existing array‑based APIs in a thin const‑generic wrapper like the
Stackabove. - Benchmark compile‑time impact for the sizes you actually need (e.g.,
time cargo clean && time cargo build --releasefor N = 8, 32, 256). - Enable the wrapper only for lengths that are truly known at compile time; keep a fallback
Vec-based version for dynamic cases. - If you hit const‑expression limits, consider moving the computation to a
const fnon stable Rust (≥1.51) or use nightly features likegeneric_const_exprsfor more complex formulas.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.