Repository navigation
NumBuffer<T> ties a buffer to one integer type; no reusable buffer for mixed-integer formatting #159341
Description
The std-replacement-data PR rust-lang/std-replacement-data#19 flagged core::fmt::NumBuffer as the std replacement for itoa. I tried to migrate off itoa and couldn't: the current API doesn't let me reuse one buffer across integer types.
NumBuffer<T> is generic over the concrete integer type, and format_into takes &mut NumBuffer<Self>, so a buffer is bound to exactly one integer type. Code that formats several integer types has to hold a separate buffer per type, and can't hide that behind a single generic helper, since the caller still has to supply the matching NumBuffer<T>.
itoa (and zmij for floats) don't have this: Buffer is a single non-generic type and format is generic over the value, so one buffer instance serves every integer (or float) type.
I hit this in a printf implementation. With itoa, one buffer covers every integer the format string throws at it:
use core::ffi::{c_char, c_double, c_int, c_long, c_uint};
// parsing/writing omitted for brevity
unsafe fn printf_internal(mut args: va_list) {
let mut ints = itoa::Buffer::new(); // one buffer, every integer type
let mut floats = zmij::Buffer::new(); // one buffer, every float type
let buf: &[u8] = match spec {
b'd' | b'i' => ints.format(unsafe { args.next_arg::<c_int>() }).as_bytes(),
b'u' => ints.format(unsafe { args.next_arg::<c_uint>() }).as_bytes(),
b'l' => ints.format(unsafe { args.next_arg::<c_long>() }).as_bytes(),
b'f' => floats.format(unsafe { args.next_arg::<c_double>() }).as_bytes(),
b's' => unsafe { CStr::from_ptr(args.next_arg::<*const c_char>()) }.to_bytes(),
_ => todo!(),
};
}The NumBuffer translation needs one buffer per integer type, and the list grows with every width/signedness a real printf supports (%lld, %zu, %jd, %td, ...):
use core::ffi::{c_char, c_double, c_int, c_long, c_uint};
use core::fmt::NumBuffer;
unsafe fn printf_internal(mut args: va_list) {
let mut floats = zmij::Buffer::new(); // NumBuffer is integers only
let mut i = NumBuffer::<c_int>::new();
let mut u = NumBuffer::<c_uint>::new();
let mut l = NumBuffer::<c_long>::new();
// + one NumBuffer per additional type, up to 10 to cover all widths
let buf: &[u8] = match spec {
b'd' | b'i' => unsafe { args.next_arg::<c_int>() }.format_into(&mut i).as_bytes(),
b'u' => unsafe { args.next_arg::<c_uint>() }.format_into(&mut u).as_bytes(),
b'l' => unsafe { args.next_arg::<c_long>() }.format_into(&mut l).as_bytes(),
b'f' => floats.format(unsafe { args.next_arg::<c_double>() }).as_bytes(),
b's' => unsafe { CStr::from_ptr(args.next_arg::<*const c_char>()) }.to_bytes(),
_ => todo!(),
};
}What I'd like
Some sanctioned way to drive one reusable buffer for every integer type, matching itoa's Buffer::format.
#143636 attempted this by making format_into generic (one NumBuffer<u128> backing any narrower integer). From what I can tell that was rejected, at least in that form: the suggestion was instead a method on NumBuffer that converts it to a NumBuffer of a different type, gated by trait bounds, and tracked separately from the rest of int_format_into. This issue could be that follow-up. I don't have a preference on the mechanism, I just need one buffer to serve multiple integer types without a distinct binding per type.
Refs: #138215 (tracking issue), #143636 (rejected generic format_into attempt).
Activity
GuillaumeGomez commentedon Jul 15, 2026
Thanks for opening this issue! I'll send a PR to add the method conversion between different
NumBuffertypes so the @rust-lang/libs-api can decide which approach they prefer.hanna-kruppe commentedon Jul 15, 2026
Can you share a little more of the surrounding code? I assume there’s a reason for the
bufvariable pointing into a longer-lived buffer, as opposed to the usualfoo(n.format_into(&mut NumBuffer::new()))pattern, but it would be good to know for sure what the reasons are.rosymati commentedon Jul 15, 2026
I can't share the full code since it's not open yet, but here's an outdated but still relevant snippet for context:
The buffer needs to outlive the match. Each arm resolves to one
&[u8]that's consumed after the match, so if the buffer lived inside the match it would be dropped before that and the borrow wouldn't compile.The real implementation is larger: there's shared code for padding and buffering that's more involved than a plain
write_all, and keeping the buffer outside the match lets that run once afterward. Otherwise it would have to be duplicated into every arm.Technically I could push a helper call into each arm (duplicating that logic) or keep ~10 separate buffers, but both are clearly less ergonomic than just using itoa. If this worked in std I'd be able to drop the dependency.
esther-ff commentedon Jul 16, 2026
My 2 cents is that in #143636 there is a very similar case being presented about the need for a reusable buffer.
I have an idea, making a specific ZST (something like
struct Variable) implementNumBufferTraitwith it'sBUF_SIZEbeing large enough to hold ani128and exposing a specific impl (likeimpl NumBuffer<Variable>) with a genericformat_intomethod, which would makeNumBuffervery versatile.GuillaumeGomez commentedon Aug 4, 2026
I opened #160514 to try out a new approach.
Rodrigodd commentedon Oct 20, 2026
I am not completely familiar with the
NumBufferAPI, but would it be possible to do something likeimpl From<&mut Buffer<u64>> for &mut Buffer<u32>?I was actually wondering whether it would be possible to
impl TryFrom<&mut [u8]> for &mut Buffer<u32>so that any generic scratch buffer could be reused as aNumBuffer. My actual use case was to format numbers into a buffer in place, but that would only work for writing right-aligned literals.GuillaumeGomez commentedon Oct 20, 2026
It's possible. However we'd like to make it more flexible in the way that any
NumBuffer<T>can be used as aNumBuffer<U>as long asT::size >= U::size. For now we're blocked on some missing const generics features to make it work nicely.