git: f68d7bfc1479 - main - libc: Add strfromd, strfromf, and strfroml per C23
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Sun, 02 Aug 2026 12:10:52 UTC
The branch main has been updated by fuz:
URL: https://cgit.FreeBSD.org/src/commit/?id=f68d7bfc1479042184e09431bd55771c50c47f68
commit f68d7bfc1479042184e09431bd55771c50c47f68
Author: Faraz Vahedi <kfv@kfv.io>
AuthorDate: 2026-06-27 13:34:21 +0000
Commit: Robert Clausecker <fuz@FreeBSD.org>
CommitDate: 2026-08-02 12:10:02 +0000
libc: Add strfromd, strfromf, and strfroml per C23
strfromd(), strfromf(), and strfroml() are implemented directly
in terms of gdtoa. If a non-conforming format string is passed,
the string "EDOOFUS" is returned and errno set to EDOOFUS as an
extension.
Reviewed by: fuz
MFC after: 1 month
Pull-Request: https://github.com/freebsd/freebsd-src/pull/2301
Signed-off-by: Faraz Vahedi <kfv@kfv.io>
---
include/stdlib.h | 4 +
lib/libc/stdlib/Makefile.inc | 8 +-
lib/libc/stdlib/Symbol.map | 3 +
lib/libc/stdlib/strfrom.c | 476 ++++++++++++++++++++++++++++
lib/libc/stdlib/strfrom.h | 35 +++
lib/libc/stdlib/strfromd.3 | 109 +++++++
lib/libc/stdlib/strfromd.c | 45 +++
lib/libc/stdlib/strfromf.c | 47 +++
lib/libc/stdlib/strfroml.c | 46 +++
lib/libc/tests/stdlib/Makefile | 1 +
lib/libc/tests/stdlib/strfrom_test.c | 590 +++++++++++++++++++++++++++++++++++
11 files changed, 1363 insertions(+), 1 deletion(-)
diff --git a/include/stdlib.h b/include/stdlib.h
index 82a347f5317e..c1f465e21fd1 100644
--- a/include/stdlib.h
+++ b/include/stdlib.h
@@ -175,6 +175,10 @@ _Noreturn void
size_t memalignment(const void *) __pure2;
void free_sized(void *, size_t) __noexcept;
void free_aligned_sized(void *, size_t, size_t) __noexcept;
+int strfromd(char * __restrict, size_t, const char * __restrict, double);
+int strfromf(char * __restrict, size_t, const char * __restrict, float);
+int strfroml(char * __restrict, size_t,
+ const char * __restrict, long double);
#endif /* __ISO_C_VISIBLE >= 2023 */
/*
diff --git a/lib/libc/stdlib/Makefile.inc b/lib/libc/stdlib/Makefile.inc
index 2cdff5b934cb..7ff6a6c46ea5 100644
--- a/lib/libc/stdlib/Makefile.inc
+++ b/lib/libc/stdlib/Makefile.inc
@@ -55,6 +55,10 @@ MISRCS+= \
remque.c \
set_constraint_handler_s.c \
strfmon.c \
+ strfrom.c \
+ strfromd.c \
+ strfromf.c \
+ strfroml.c \
strtoimax.c \
strtol.c \
strtold.c \
@@ -96,7 +100,7 @@ MAN+= a64l.3 abort.3 abs.3 atexit.3 atof.3 \
quick_exit.3 \
radixsort.3 rand.3 random.3 reallocarray.3 reallocf.3 realpath.3 \
set_constraint_handler_s.3 \
- strfmon.3 strtod.3 strtol.3 strtonum.3 strtoul.3 system.3 \
+ strfmon.3 strfromd.3 strtod.3 strtol.3 strtonum.3 strtoul.3 system.3 \
tsearch.3
MLINKS+=a64l.3 l64a.3 \
@@ -143,6 +147,8 @@ MLINKS+=reallocarray.3 recallocarray.3
MLINKS+=set_constraint_handler_s.3 abort_handler_s.3
MLINKS+=set_constraint_handler_s.3 ignore_handler_s.3
MLINKS+=strfmon.3 strfmon_l.3
+MLINKS+=strfromd.3 strfromf.3 \
+ strfromd.3 strfroml.3
MLINKS+=strtod.3 strtof.3 \
strtod.3 strtold.3
MLINKS+=strtol.3 strtoll.3 \
diff --git a/lib/libc/stdlib/Symbol.map b/lib/libc/stdlib/Symbol.map
index 373006b4a388..df4922f1df60 100644
--- a/lib/libc/stdlib/Symbol.map
+++ b/lib/libc/stdlib/Symbol.map
@@ -134,6 +134,9 @@ FBSD_1.8 {
FBSD_1.9 {
memalignment;
recallocarray;
+ strfromd;
+ strfromf;
+ strfroml;
strtonumx;
tdestroy;
};
diff --git a/lib/libc/stdlib/strfrom.c b/lib/libc/stdlib/strfrom.c
new file mode 100644
index 000000000000..01725ba30d1f
--- /dev/null
+++ b/lib/libc/stdlib/strfrom.c
@@ -0,0 +1,476 @@
+/*
+ * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Shared helpers for strfromd, strfromf, and strfroml (C23 §7.24.1.3).
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <locale.h>
+#include <stdlib.h>
+
+#include "strfrom.h"
+
+struct sf_buf {
+ char *s;
+ size_t n;
+ int pos;
+};
+
+/*
+ * Write c to b->s[b->pos] if within the n-1 writable bytes; always advance
+ * b->pos. n is the total buffer capacity, including the null slot.
+ */
+static void
+sf_putc(struct sf_buf *b, char c)
+{
+ if (b->n > 0 && (size_t)b->pos < b->n - 1)
+ b->s[b->pos] = c;
+ b->pos++;
+}
+
+/*
+ * Write len bytes from src.
+ */
+static void
+sf_write(struct sf_buf *b, const char *src, int len)
+{
+ int i;
+
+ for (i = 0; i < len; i++)
+ sf_putc(b, src[i]);
+}
+
+/*
+ * Write a null terminated string.
+ */
+static void
+sf_puts(struct sf_buf *b, const char *src)
+{
+ while (*src != '\0')
+ sf_putc(b, *src++);
+}
+
+/*
+ * Write count copies of c.
+ */
+static void
+sf_padc(struct sf_buf *b, char c, int count)
+{
+ int i;
+
+ for (i = 0; i < count; i++)
+ sf_putc(b, c);
+}
+
+/*
+ * Seal the buffer: null terminate at min(pos, n-1).
+ */
+static void
+sf_seal(struct sf_buf *b)
+{
+ if (b->n > 0)
+ b->s[(size_t)b->pos < b->n ? b->pos : b->n - 1] = '\0';
+}
+
+/*
+ * Emit the radix. Interfaces are defined as snprintf(3) (C23 §7.24.1.3p2),
+ * whose '.' is the LC_NUMERIC decimal point; localeconv() resolves the same
+ * per-thread locale as vfprintf(3). The radix may be multibyte.
+ */
+static void
+sf_putdp(struct sf_buf *b)
+{
+ sf_puts(b, localeconv()->decimal_point);
+}
+
+/*
+ * Emit "[eEpP][+-]ddd": exponent sign and decimal magnitude, zero-padded to at
+ * least mindig digits (2 for %e/%E and %g/%G, 1 for %a/%A per C23 §7.23.6.1).
+ *
+ * Handles the full exponent range of every supported type.
+ */
+static void
+sf_emit_exp(struct sf_buf *b, char ec, int exp, int mindig)
+{
+ char buf[16];
+ int len;
+
+ sf_putc(b, ec);
+ if (exp < 0) {
+ sf_putc(b, '-');
+ exp = -exp;
+ } else {
+ sf_putc(b, '+');
+ }
+
+ len = 0;
+ do {
+ buf[len++] = '0' + exp % 10;
+ exp /= 10;
+ } while (exp != 0);
+ while (len < mindig)
+ buf[len++] = '0';
+ while (len > 0)
+ sf_putc(b, buf[--len]);
+}
+
+/*
+ * Parse "%[.prec]conv" per C23 §7.24.1.3.
+ *
+ * Returns the conversion specifier character, or '\0' with errno set to
+ * EDOOFUS if fmt is not one of the forms the standard permits; *prec is
+ * -1 when the precision is absent.
+ */
+char
+__sf_parse_fmt(const char *fmt, int *prec)
+{
+ const char *p;
+
+ *prec = -1;
+ if (*fmt != '%')
+ return ('\0');
+ p = fmt + 1;
+
+ if (*p == '.') {
+ *prec = 0;
+ while (*++p >= '0' && *p <= '9')
+ *prec = *prec * 10 + (*p - '0');
+ }
+
+ switch (*p) {
+ case 'a': case 'A':
+ case 'e': case 'E':
+ case 'f': case 'F':
+ case 'g': case 'G':
+ break;
+ default:
+ return ('\0');
+ }
+
+ return (p[1] == '\0' ? *p : '\0');
+}
+
+/*
+ * Report a format string the standard does not permit.
+ *
+ * Sets errno and renders "EDOOFUS" under the ordinary snprintf(3) truncation
+ * rules, which keeps a faulty caller alive and its output obviously wrong.
+ */
+int
+__sf_edoofus(char *s, size_t n)
+{
+ struct sf_buf b;
+
+ errno = EDOOFUS;
+ b.s = s;
+ b.n = n;
+ b.pos = 0;
+ sf_puts(&b, "EDOOFUS");
+ sf_seal(&b);
+ return (b.pos);
+}
+
+/*
+ * Render Inf or NaN into b->s[0..n-1].
+ *
+ * is_nan: non-zero if digits[0] == 'N' (NaN), zero for Infinity.
+ */
+static int
+sf_special(struct sf_buf *b, char conv, int signflag, int is_nan)
+{
+ int upper;
+
+ b->pos = 0;
+ upper = isupper((unsigned char)conv);
+ if (!is_nan && signflag)
+ sf_putc(b, '-');
+ sf_puts(b, is_nan ? (upper ? "NAN" : "nan") : (upper ? "INF" : "inf"));
+ sf_seal(b);
+ return (b->pos);
+}
+
+/*
+ * Render %e / %E.
+ *
+ * dtoa was called with mode=2, ndigits=prec+1. decpt is the position of the
+ * first significant digit relative to the decimal point (= exponent + 1), as
+ * returned by dtoa.
+ */
+static int
+sf_efmt(struct sf_buf *b, int prec, char conv,
+ const char *digits, int ndig, int decpt, int signflag)
+{
+ int avail, copy;
+
+ b->pos = 0;
+ if (signflag)
+ sf_putc(b, '-');
+
+ /*
+ * Leading significant digit.
+ */
+ sf_putc(b, ndig > 0 ? digits[0] : '0');
+
+ if (prec > 0) {
+ sf_putdp(b);
+ avail = ndig > 1 ? ndig - 1 : 0;
+ copy = avail < prec ? avail : prec;
+ sf_write(b, digits + 1, copy);
+ sf_padc(b, '0', prec - copy);
+ }
+ sf_emit_exp(b, isupper((unsigned char)conv) ? 'E' : 'e', decpt - 1, 2);
+
+ sf_seal(b);
+ return (b->pos);
+}
+
+/*
+ * Render %f / %F.
+ *
+ * dtoa was called with mode=3, ndigits=prec. decpt gives the number of digits
+ * before the decimal point (may be <= 0).
+ */
+static int
+sf_ffmt(struct sf_buf *b, int prec, int signflag,
+ const char *digits, int ndig, int decpt)
+{
+ int avail, copy, rem, zc;
+
+ b->pos = 0;
+ if (signflag)
+ sf_putc(b, '-');
+
+ if (decpt <= 0) {
+ sf_putc(b, '0');
+ if (prec > 0) {
+ sf_putdp(b);
+ zc = -decpt < prec ? -decpt : prec;
+ rem = prec - zc;
+ copy = ndig < rem ? ndig : rem;
+ sf_padc(b, '0', zc);
+ sf_write(b, digits, copy);
+ sf_padc(b, '0', rem - copy);
+ }
+ } else {
+ /*
+ * decpt digits (or zeros) before the point.
+ */
+ copy = ndig < decpt ? ndig : decpt;
+ sf_write(b, digits, copy);
+ sf_padc(b, '0', decpt - copy);
+ if (prec > 0) {
+ sf_putdp(b);
+ avail = ndig - decpt;
+ if (avail < 0)
+ avail = 0;
+ copy = avail < prec ? avail : prec;
+ sf_write(b, digits + decpt, copy);
+ sf_padc(b, '0', prec - copy);
+ }
+ }
+
+ sf_seal(b);
+ return (b->pos);
+}
+
+/*
+ * Render %g / %G.
+ *
+ * dtoa was called with mode=2, ndigits=max(1, prec).
+ */
+static int
+sf_gfmt(struct sf_buf *b, int prec, char conv,
+ const char *digits, int ndig, int decpt, int signflag)
+{
+ int ep, copy, frac;
+
+ b->pos = 0;
+ /*
+ * Precision 0 is treated as 1 per C23 §7.23.6.1.
+ */
+ ep = prec == 0 ? 1 : prec;
+
+ /*
+ * Strip trailing zeros (no ALT flag is accepted).
+ */
+ while (ndig > 1 && digits[ndig - 1] == '0')
+ ndig--;
+
+ if (signflag)
+ sf_putc(b, '-');
+
+ /*
+ * Standard says −4 ≤ exponent < P test: exponent = decpt − 1
+ */
+ if (decpt > -4 && decpt <= ep) {
+ /*
+ * %f style, per C23 §7.23.6.1.
+ */
+ if (decpt <= 0) {
+ sf_putc(b, '0');
+ if (ndig > 0) {
+ sf_putdp(b);
+ sf_padc(b, '0', -decpt);
+ sf_write(b, digits, ndig);
+ }
+ } else {
+ copy = ndig < decpt ? ndig : decpt;
+ sf_write(b, digits, copy);
+ sf_padc(b, '0', decpt - copy);
+ frac = ndig - decpt;
+ if (frac > 0) {
+ sf_putdp(b);
+ sf_write(b, digits + decpt, frac);
+ }
+ }
+ } else {
+ /*
+ * %e style, per C23 §7.23.6.1.
+ */
+ sf_putc(b, ndig > 0 ? digits[0] : '0');
+ if (ndig > 1) {
+ sf_putdp(b);
+ sf_write(b, digits + 1, ndig - 1);
+ }
+ sf_emit_exp(b, isupper((unsigned char)conv) ? 'E' : 'e',
+ decpt - 1, 2);
+ }
+
+ sf_seal(b);
+ return (b->pos);
+}
+
+/*
+ * Render %a / %A.
+ *
+ * digits / ndig: hex significand digits from __hdtoa / __hldtoa; the first
+ * digit represents the integer part of the mantissa (normally '1').
+ *
+ * decpt: binary exponent such that p-exponent = decpt - 1.
+ *
+ * user_prec: digits after the hex point (-1 for shortest-exact).
+ */
+static int
+sf_afmt(struct sf_buf *b, int user_prec, char conv,
+ const char *digits, int ndig, int decpt, int signflag)
+{
+ int after, copy;
+
+ b->pos = 0;
+ if (signflag)
+ sf_putc(b, '-');
+ sf_putc(b, '0');
+ sf_putc(b, isupper((unsigned char)conv) ? 'X' : 'x');
+
+ /*
+ * Integer part of the significand.
+ */
+ sf_putc(b, ndig > 0 ? digits[0] : '0');
+
+ after = ndig - 1;
+ if (user_prec < 0) {
+ /*
+ * Shortest-exact: emit only the non-zero significant tail.
+ */
+ if (after > 0) {
+ sf_putdp(b);
+ sf_write(b, digits + 1, after);
+ }
+ } else if (user_prec > 0) {
+ sf_putdp(b);
+ copy = after < user_prec ? after : user_prec;
+ sf_write(b, digits + 1, copy);
+ sf_padc(b, '0', user_prec - copy);
+ }
+ sf_emit_exp(b, isupper((unsigned char)conv) ? 'P' : 'p', decpt - 1, 1);
+
+ sf_seal(b);
+ return (b->pos);
+}
+
+/*
+ * Hex digit table for %a / %A, selected by specifier case.
+ */
+const char *
+__sf_xdigits(char conv)
+{
+ return (isupper((unsigned char)conv) ? "0123456789ABCDEF" :
+ "0123456789abcdef");
+}
+
+/*
+ * Map a decimal specifier (lowercased) to its gdtoa mode and digit count.
+ */
+void
+__sf_decimal_mode(char lc, int prec, int *mode, int *ndig_req)
+{
+ switch (lc) {
+ case 'e':
+ *mode = 2;
+ *ndig_req = prec + 1;
+ break;
+ case 'f':
+ *mode = 3;
+ *ndig_req = prec;
+ break;
+ case 'g':
+ *mode = 2;
+ *ndig_req = prec == 0 ? 1 : prec;
+ break;
+ default:
+ __unreachable();
+ }
+}
+
+/*
+ * Render a finished %a / %A conversion. __hdtoa / __hldtoa always flag
+ * Inf/NaN with decpt == INT_MAX.
+ */
+int
+__sf_render_hex(char *s, size_t n, char conv, int prec,
+ char *digits, char *dend, int decpt, int signflag)
+{
+ struct sf_buf b;
+ int ndig, usprec;
+
+ b.s = s;
+ b.n = n;
+ if (decpt == INT_MAX)
+ return (sf_special(&b, conv, signflag, digits[0] == 'N'));
+ ndig = (int)(dend - digits);
+ usprec = prec >= 0 ? prec : ndig - 1;
+ return (sf_afmt(&b, usprec, conv, digits, ndig, decpt, signflag));
+}
+
+/*
+ * Render a finished %e/%f/%g conversion. is_special is set when decpt holds
+ * the caller's Inf/NaN sentinel (dtoa: 9999, __ldtoa: INT_MAX).
+ */
+int
+__sf_render_decimal(char *s, size_t n, char conv, char lc, int prec,
+ char *digits, char *dend, int decpt, int signflag, int is_special)
+{
+ struct sf_buf b;
+ int ndig;
+
+ b.s = s;
+ b.n = n;
+ if (is_special)
+ return (sf_special(&b, conv, signflag, digits[0] == 'N'));
+ ndig = (int)(dend - digits);
+ switch (lc) {
+ case 'e':
+ return (sf_efmt(&b, prec, conv, digits, ndig, decpt, signflag));
+ case 'f':
+ return (sf_ffmt(&b, prec, signflag, digits, ndig, decpt));
+ case 'g':
+ return (sf_gfmt(&b, prec, conv, digits, ndig, decpt, signflag));
+ default:
+ __unreachable();
+ }
+}
diff --git a/lib/libc/stdlib/strfrom.h b/lib/libc/stdlib/strfrom.h
new file mode 100644
index 000000000000..967897ea7f12
--- /dev/null
+++ b/lib/libc/stdlib/strfrom.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Internal declarations for strfrom* numeric conversion (C23 §7.24.1.3).
+ *
+ * Note: gdtoa dtoa() modes used:
+ * 2 – ndigits significant digits (%e, %g)
+ * 3 – ndigits digits after '.' (%f)
+ */
+
+#ifndef STRFROM_H
+#define STRFROM_H
+
+#include <limits.h>
+#include <stddef.h>
+
+#define dtoa __dtoa
+#define freedtoa __freedtoa
+
+#include "../stdio/floatio.h"
+#include "gdtoa.h"
+
+char __sf_parse_fmt(const char *, int *);
+int __sf_edoofus(char *, size_t);
+const char *__sf_xdigits(char);
+void __sf_decimal_mode(char, int, int *, int *);
+int __sf_render_hex(char *, size_t, char, int, char *, char *, int, int);
+int __sf_render_decimal(char *, size_t, char, char, int, char *, char *,
+ int, int, int);
+
+#endif /* STRFROM_H */
diff --git a/lib/libc/stdlib/strfromd.3 b/lib/libc/stdlib/strfromd.3
new file mode 100644
index 000000000000..acd3867c0425
--- /dev/null
+++ b/lib/libc/stdlib/strfromd.3
@@ -0,0 +1,109 @@
+.\"
+.\" Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd June 27, 2026
+.Dt STRFROMD 3
+.Os
+.Sh NAME
+.Nm strfromd ,
+.Nm strfromf ,
+.Nm strfroml
+.Nd convert floating point value to
+.Tn ASCII
+string
+.Sh LIBRARY
+.Lb libc
+.Sh SYNOPSIS
+.In stdlib.h
+.Ft int
+.Fn strfromd "char * restrict str" "size_t size" "const char * restrict format" "double fp"
+.Ft int
+.Fn strfromf "char * restrict str" "size_t size" "const char * restrict format" "float fp"
+.Ft int
+.Fn strfroml "char * restrict str" "size_t size" "const char * restrict format" "long double fp"
+.Sh DESCRIPTION
+Functions
+.Fn strfromd ,
+.Fn strfromf ,
+and
+.Fn strfroml
+convert the floating-point value
+.Fa fp
+to a null terminated string, storing at most
+.Fa size
+bytes
+.Pq including the terminating null character
+into the array pointed to by
+.Fa str .
+.Pp
+These functions are equivalent to
+.Fn snprintf "s" "n" "format" "fp" ,
+except that default argument promotions are not applied to
+.Fa fp ,
+and the
+.Fa format
+string is restricted.
+It shall consist of exactly the character
+.Ql % ,
+followed by an optional precision that does not contain an asterisk
+.Ql * ,
+followed by one of the conversion specifiers
+.Cm a , A , e , E , f , F , g ,
+or
+.Cm G .
+The conversion specifier applies to the type indicated by the
+function suffix, rather than by a length modifier.
+.Pp
+Use of any other format string results in undefined behaviour,
+as does any undefined behaviour inherited from
+.Xr snprintf 3
+per se.
+In case of a format string not specified by the standard, the string
+.Ql EDOOFUS
+is written to
+.Fa str ,
+subject to the same truncation rules as a successful conversion, and
+.Va errno
+is set to
+.Er EDOOFUS .
+.Sh RETURN VALUES
+The
+.Fn strfromd ,
+.Fn strfromf ,
+and
+.Fn strfroml
+functions, as per
+.Xr snprintf 3 ,
+return the number of characters
+.Po
+not counting the terminating null character
+.Pc
+that would have been written
+had
+.Fa n
+been sufficiently large.
+Thus, the null terminated output has been completely written if
+and only if the returned value is both nonnegative and less than
+.Fa size .
+.Sh ERRORS
+.Bl -tag -width Er
+.It Bq Er EDOOFUS
+The
+.Fa format
+string is not of the form the standard specifies.
+.El
+.Sh SEE ALSO
+.Xr snprintf 3 ,
+.Xr strtod 3 ,
+.Xr strtol 3 ,
+.Xr strtoul 3
+.Sh STANDARDS
+Functions
+.Fn strfromd ,
+.Fn strfromf ,
+and
+.Fn strfroml
+conform to
+.St -isoC-2023 .
diff --git a/lib/libc/stdlib/strfromd.c b/lib/libc/stdlib/strfromd.c
new file mode 100644
index 000000000000..b7d364c0d9cd
--- /dev/null
+++ b/lib/libc/stdlib/strfromd.c
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <ctype.h>
+#include <stdlib.h>
+
+#include "strfrom.h"
+
+int
+strfromd(char * __restrict s, size_t n, const char * __restrict fmt, double fp)
+{
+ char conv, lc, *digits, *dend;
+ int prec, decpt, signflag, ret, mode, ndig_req;
+
+ conv = __sf_parse_fmt(fmt, &prec);
+ if (conv == '\0')
+ return (__sf_edoofus(s, n));
+ lc = tolower((unsigned char)conv);
+
+ if (lc == 'a') {
+ digits = __hdtoa(fp, __sf_xdigits(conv),
+ prec >= 0 ? prec + 1 : -1, &decpt, &signflag, &dend);
+ ret = __sf_render_hex(s, n, conv, prec, digits, dend, decpt,
+ signflag);
+ freedtoa(digits);
+ return (ret);
+ }
+
+ if (prec < 0)
+ prec = 6;
+
+ __sf_decimal_mode(lc, prec, &mode, &ndig_req);
+
+ digits = dtoa(fp, mode, ndig_req, &decpt, &signflag, &dend);
+
+ ret = __sf_render_decimal(s, n, conv, lc, prec, digits, dend, decpt,
+ signflag, decpt == 9999);
+
+ freedtoa(digits);
+
+ return (ret);
+}
diff --git a/lib/libc/stdlib/strfromf.c b/lib/libc/stdlib/strfromf.c
new file mode 100644
index 000000000000..adb87cf2fdda
--- /dev/null
+++ b/lib/libc/stdlib/strfromf.c
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <ctype.h>
+#include <stdlib.h>
+
+#include "strfrom.h"
+
+int
+strfromf(char * __restrict s, size_t n, const char * __restrict fmt, float fp)
+{
+ char conv, lc, *digits, *dend;
+ int prec, decpt, signflag, ret, mode, ndig_req;
+ double dbl;
+
+ dbl = (double)fp;
+ conv = __sf_parse_fmt(fmt, &prec);
+ if (conv == '\0')
+ return (__sf_edoofus(s, n));
+ lc = tolower((unsigned char)conv);
+
+ if (lc == 'a') {
+ digits = __hdtoa(dbl, __sf_xdigits(conv),
+ prec >= 0 ? prec + 1 : -1, &decpt, &signflag, &dend);
+ ret = __sf_render_hex(s, n, conv, prec, digits, dend, decpt,
+ signflag);
+ freedtoa(digits);
+ return (ret);
+ }
+
+ if (prec < 0)
+ prec = 6;
+
+ __sf_decimal_mode(lc, prec, &mode, &ndig_req);
+
+ digits = dtoa(dbl, mode, ndig_req, &decpt, &signflag, &dend);
+
+ ret = __sf_render_decimal(s, n, conv, lc, prec, digits, dend, decpt,
+ signflag, decpt == 9999);
+
+ freedtoa(digits);
+
+ return (ret);
+}
diff --git a/lib/libc/stdlib/strfroml.c b/lib/libc/stdlib/strfroml.c
new file mode 100644
index 000000000000..9e213a5ecbee
--- /dev/null
+++ b/lib/libc/stdlib/strfroml.c
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <ctype.h>
+#include <stdlib.h>
+
+#include "strfrom.h"
+
+int
+strfroml(char * __restrict s, size_t n, const char * __restrict fmt,
+ long double fp)
+{
+ char conv, lc, *digits, *dend;
+ int prec, decpt, signflag, ret, mode, ndig_req;
+
+ conv = __sf_parse_fmt(fmt, &prec);
+ if (conv == '\0')
+ return (__sf_edoofus(s, n));
+ lc = tolower((unsigned char)conv);
+
+ if (lc == 'a') {
+ digits = __hldtoa(fp, __sf_xdigits(conv),
+ prec >= 0 ? prec + 1 : -1, &decpt, &signflag, &dend);
+ ret = __sf_render_hex(s, n, conv, prec, digits, dend, decpt,
+ signflag);
+ freedtoa(digits);
+ return (ret);
+ }
+
+ if (prec < 0)
+ prec = 6;
+
+ __sf_decimal_mode(lc, prec, &mode, &ndig_req);
+
+ digits = __ldtoa(&fp, mode, ndig_req, &decpt, &signflag, &dend);
+
+ ret = __sf_render_decimal(s, n, conv, lc, prec, digits, dend, decpt,
+ signflag, decpt == INT_MAX);
+
+ freedtoa(digits);
+
+ return (ret);
+}
diff --git a/lib/libc/tests/stdlib/Makefile b/lib/libc/tests/stdlib/Makefile
index 0dbe059e5264..1a0206bcdab9 100644
--- a/lib/libc/tests/stdlib/Makefile
+++ b/lib/libc/tests/stdlib/Makefile
@@ -17,6 +17,7 @@ ATF_TESTS_C+= qsort_s_test
ATF_TESTS_C+= qsort_bench
ATF_TESTS_C+= set_constraint_handler_s_test
ATF_TESTS_C+= strfmon_test
+ATF_TESTS_C+= strfrom_test
ATF_TESTS_C+= system_test
ATF_TESTS_C+= tsearch_test
ATF_TESTS_CXX+= cxa_thread_atexit_test
diff --git a/lib/libc/tests/stdlib/strfrom_test.c b/lib/libc/tests/stdlib/strfrom_test.c
new file mode 100644
index 000000000000..8898cde200a6
--- /dev/null
+++ b/lib/libc/tests/stdlib/strfrom_test.c
@@ -0,0 +1,590 @@
+/*
+ * Copyright (c) 2026 Faraz Vahedi <kfv@kfv.io>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Tests for strfromd(3), strfromf(3), and strfroml(3).
+ *
+ * C23 §7.24.1.3 states that these functions are equivalent to snprintf(3),
+ * albeit with some subtle exceptions, and therefore snprintf(3) is used as
+ * the correctness oracle. Hardcoded string checks guard against both
+ * producing the same wrong answer.
+ */
+
+#include <errno.h>
+#include <float.h>
+#include <locale.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <atf-c.h>
+
+/*
+ * Buffer large enough for any finite value in %f form, including LDBL_MAX on
+ * platforms with 80-bit or 128-bit long double. Sizing for the full output
+ * keeps the snprintf(3) oracle a full-length comparison rather than letting
+ * both sides truncate alike.
+ */
+#define FBUF 8192
+
+static void
+check_d(const char *fmt, double val)
+{
+ char got[FBUF], ref[FBUF];
+ int rlen, glen;
+
+ rlen = snprintf(ref, sizeof(ref), fmt, val);
+ glen = strfromd(got, sizeof(got), fmt, val);
+ ATF_CHECK_MSG(glen == rlen,
+ "strfromd(\"%s\", %g): return %d, want %d", fmt, val, glen, rlen);
+ ATF_CHECK_STREQ_MSG(ref, got, "strfromd(\"%s\", %g)", fmt, val);
+}
+
+static void
+check_f(const char *fmt, float val)
+{
+ char got[FBUF], ref[FBUF];
+ int rlen, glen;
+
+ rlen = snprintf(ref, sizeof(ref), fmt, (double)val);
+ glen = strfromf(got, sizeof(got), fmt, val);
+ ATF_CHECK_MSG(glen == rlen,
+ "strfromf(\"%s\", %g): return %d, want %d",
+ fmt, (double)val, glen, rlen);
+ ATF_CHECK_STREQ_MSG(ref, got, "strfromf(\"%s\", %g)", fmt, (double)val);
+}
+
+static void
+check_l(const char *fmt, long double val)
+{
+ char got[FBUF], ref[FBUF], lfmt[32];
+ int rlen, glen;
+
+ /*
+ * snprintf requires the 'L' length modifier for long double.
+ */
+ snprintf(lfmt, sizeof(lfmt), "%.*sL%c",
+ (int)(strlen(fmt) - 1), fmt, fmt[strlen(fmt) - 1]);
+ rlen = snprintf(ref, sizeof(ref), lfmt, val);
+ glen = strfroml(got, sizeof(got), fmt, val);
+ ATF_CHECK_MSG(glen == rlen,
+ "strfroml(\"%s\", %Lg): return %d, want %d", fmt, val, glen, rlen);
+ ATF_CHECK_STREQ_MSG(ref, got, "strfroml(\"%s\", %Lg)", fmt, val);
+}
*** 513 LINES SKIPPED ***