Skip to content

strconv: add a zero-alloc integer-to-text formatter that writes into a caller-provided []u8 buffer #27509

Description

@enghitalo

Describe the feature

There is currently no public, zero-allocation integer-to-text formatter in the
stdlib
that writes digits into a caller-provided []u8 buffer and returns the number
of bytes written. Every path from an integer to its textual form heap-allocates:

  • int.str() / i32.str() / i16.str() / u16.str() go through str_l(max), which
    does malloc_noscan(max + 1) and returns tos(buf, diff)vlib/builtin/int.v:94.
  • i64.str() / int_literal.str() go through impl_i64_to_string, which does
    malloc_noscan(max + 1)vlib/builtin/int.v:221.
  • u32.str() / u64.str() each malloc_noscanvlib/builtin/int.v:172, :267.
  • String interpolation allocates too. With v -printfn main__main:
fn main() {
	x := 12345
	s := '${x}'         // -> builtin__int_str(x)            (malloc)
	t := 'val=${x} end' // -> int_str(x) + string_plus_many  (malloc)
	u := '${x:08d}'     // -> builtin__str_intp(...)         (Builder + result string)
	println('${s} ${t} ${u} ${x.str()}')
}

Generated C:

string s = builtin__int_str(x);
string t = builtin__string_plus_many(3, _MOV((string[3]){_S("val="), builtin__int_str(x), _S(" end")}));
string u = builtin__str_intp(2, _MOV((StrIntpData[]){{_SLIT0, 0x8010fe27, {.d_i32 = x}, 0, 0, 0}, {_SLIT0, 0, { .d_c = 0 }, 0, 0, 0}}));

builtin__int_strstr_lmalloc_noscan (vlib/builtin/int.v:94).
builtin__str_intpstrings.new_builder(64) + res.str() (heap), see
vlib/builtin/string_interpolation.v:698-716.

The actual digit-emitting core is already zero-alloc internally: strconv.format_dec_sb
fills a stack [32]u8{} buffer (vlib/strconv/format_mem.c.v:97-122). The only
thing missing is a public variant whose target is a caller buffer rather than a
heap-backed strings.Builder.

Use Case

Hot loops and allocation-sensitive code (logging, serializers, network/codec encoders,
embedded / -gc none, arena allocators) need to render integers into an already-owned
buffer without per-value heap traffic. Today anyone who wants this must hand-roll a
private helper (a write_decimal / emit_int that indexes the digit table and writes
into a []u8), duplicating logic that already lives — but is not reachable — inside
strconv.format_dec_sb.

Note: strings.Builder already has a zero-alloc write_decimal(n i64)
(vlib/strings/builder.c.v:78), which solves the Builder target. What is still
missing is the lower-level primitive that writes into a plain []u8 (no Builder, no
backing heap array at all).

Proposed Solution

Add public buffer-target formatters in strconv that write into a caller-provided slice
and return the count, allocating nothing:

// returns the number of bytes written, or -1 if buf is too small.
pub fn write_dec(n i64, mut buf []u8) int
pub fn write_dec_u(n u64, mut buf []u8) int
// optional: arbitrary radix without per-digit concatenation
pub fn write_int(n i64, radix int, mut buf []u8) int

The implementation already exists in spirit — strconv.format_dec_sb
(vlib/strconv/format_mem.c.v:44-133) builds digits into a stack [32]u8{}
(lines 97-122) and then writes into a strings.Builder. The new API would do the same
digit loop but copy into the caller's []u8, using strconv.dec_digits(n)
(vlib/strconv/utilities.v:183) to bounds-check the destination first. No
malloc_noscan and no strings.Builder would be involved.

Sketch:

@[direct_array_access]
pub fn write_dec_u(n u64, mut buf []u8) int {
	nd := dec_digits(n)
	if buf.len < nd {
		return -1
	}
	mut tmp := [20]u8{}
	mut i := 20
	mut v := n
	for {
		i--
		tmp[i] = u8(v % 10) + `0`
		v /= 10
		if v == 0 {
			break
		}
	}
	unsafe { vmemcpy(&buf[0], &tmp[i], nd) }
	return nd
}

A signed wrapper would emit - then delegate, with the existing min_i64 special case
from vlib/builtin/int.v:217.

Other Information

Acknowledgements

  • I may be able to implement this feature request
  • This feature might incur a breaking change

Version used

V 0.5.1 6dd9033.eb1d47b

Environment details (OS name and version, etc.)

linux, Ubuntu 24.04 LTS; cc (GCC) 14.2.0

Note

You can use the 👍 reaction to increase the issue's priority for developers.

Please note that only the 👍 reaction to the issue itself counts as a vote.
Other reactions and those to comments will not be taken into account.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Feature/Enhancement RequestThis issue is made to request a feature or an enhancement to an existing one.Unit: vlibBugs/feature requests, that are related to the vlib.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions