ISO/IEC JTC1/SC22/WG14 N3921 2026-07-12
Jakub Jelínek, jakub@redhat.com
Abstract
ChangeLog
Revision 0
Introduction and Motivation
Proposal
Possible variants
Implementation experience
Wording
Acknowledgments
This proposal offers type-generic macros for varying bit-precise integer type argument access whose width is determined at runtime, storing their values as array of integers or storing bit-precise integer value computed from array of integers.
— Initial release.
C23 introduced bit-precise integer types, however it lacks standard
facilities for writing such values to output streams and reading such
values from input streams. The
N2858
paper proposes reasonable extensions for printf and
scanf family of functions to support those. Users might also
want to add similar support to their own libraries. However there is an
implementability problem, while there are just a few standard integer types
and optionally a few extra extended integer types, there can be thousands
or millions of distinct bit-precise integer types.
In order to support following example
_BitInt(856) x;
printf("%wb175u",
42636895407386080189113439121686496074204655714062974uwb);
scanf("%wb856d", &x);
the C library needs to use separate va_arg macro invocation
for each supported width of bit-precise integer types in the
printf case or store separately each supported width of
bit-precise integer types through the pointer obtained through
va_arg macro. If an implementation supports the bare
minimum of bit-precise integer types and BITINT_MAXWIDTH
is very small, e.g. equal to LLONG_WIDTH, this can be
easily implemented e.g. using
#include <stdarg.h>
#include <stddef.h>
#include <limits.h>
void
read_bitint_n(va_list *ap, size_t n, unsigned long *p, bool end, bool uns)
{
unsigned long long r = 0;
if (uns)
switch (n)
{
#define A(n) case n: r = va_arg(*ap, unsigned _BitInt(n)); break;
#define B(n) A(n##0) A(n##1) A(n##2) A(n##3) A(n##4) \
A(n##5) A(n##6) A(n##7) A(n##8) A(n##9)
A(1) A(2) A(3) A(4) A(5) A(6) A(7) A(8) A(9)
B(1) B(2) B(3) B(4) B(5)
A(60) A(61) A(62) A(63) A(64)
}
else
switch (n)
{
#undef A
#define A(n) case n: r = va_arg(*ap, _BitInt(n)); break;
A(1) A(2) A(3) A(4) A(5) A(6) A(7) A(8) A(9)
B(1) B(2) B(3) B(4) B(5)
A(60) A(61) A(62) A(63) A(64)
}
r <<= (LLONG_WIDTH - n);
if (uns)
r >>= (LLONG_WIDTH - n);
else
r = ((long long) r) >> (LLONG_WIDTH - n);
size_t w = (n + LONG_WIDTH - 1) / LONG_WIDTH;
if (end)
for (size_t i = 0; i < w; ++i)
p[w - 1 - i] = r >> (i * LONG_WIDTH);
else
for (size_t i = 0; i < w; ++i)
p[i] = r >> (i * LONG_WIDTH);
}
If BITINT_MAXWIDTH is large (GCC currently uses value
65535 for it on architectures which do support bit-precise integers,
Clang on selected architectures uses 8388608), such an approach
is not feasible, e.g.
Godbolt example doesn't even compile in reasonable time, and
even when commenting out 59000 of the 65535 cases it resulted in
1.3MiB of code when compiled by GCC. Furthermore, if the maximum
bit-precise integer width is large, converting all bit-precise
integer values to the largest one is inefficient, for the C library
internals it is best to represent the arbitrary values as an array
of limbs and either handle conversions from or to strings on it
directly (for decimal I/O divide and modulo each limb by 10 e.g.
using arithmetics on a type larger than the limb type), or use some
Multiple Precision Arithmetics library under the hood.
The ABI for bit-precise integer types, their size, alignment, how they
are passed as function arguments and returned from functions differs
between different architectures (if it is already defined at all).
In most cases however if the bit-precise width is smaller than certain
small constants, their size, alignment and argument passing follows
passing of selected standard integer types which has width larger or
equal to the width of the bit-precise integer, and for larger width
they are handled as usually as a structure containing single member
with type array of some integer type. That integer type differs between
different architectures, common width of the limb type are 64 bits,
128 bits or 32 bits. The knowledge of that is usually confined to
compilers which support bit-precise integer types, the C library and
especially user code doing similar format string handling as the C library
ideally shouldn't hardcode such details and also details of the
va_list type. So, this paper proposes macros
which can be used both for the C library implementation and in other
user code as well.
Hardcoding for the macros a particular user limb
type (element of the array to be filled with bits of the read bit-precise
integer value) is possible, but not very C library implementor or
user-friendly.
Mandating that the limb type matches the limb type in a particular
bit-precise integer ABI would mean the library would need to implement
the reading and writing of bit-precise integers several times, e.g. once
for some 64-bit, once for some 32-bit and once for some 128-bit integer
type if not more. Hardcoding some arbitrarily chosen standard integer
type, e.g. unsigned long int, is possible, but would require
extra memory and time to recode it into the form the library needs.
Always using a very small limb type, such as unsigned char
would mean too many limbs, usually the library will want as large limb
type as possible provided that it can perform efficiently all operations
it needs, or when using an underlying Multiple Precision Arithmetic
library determined by the requirements of that library.
Another needed choice is the ordering of the limbs within the array
of limbs, whether the least significant limb comes first (little-endian)
or last (big-endian). It is certainly possible to hardcode e.g.
little-endian ordering of the limbs, however if an underlying library is
involved and needs a different ordering the C library or user code
would need to swap the limbs in a loop.
Yet another concern is when trying to build a C library which is supposed
to support bit-precise integer I/O with a compiler which doesn't support
them or doesn't support this proposal. One possibility is to pre-compile
a simple function using these macros, for the va_arg_bitint
case taking va_list * argument using a compiler which supports
these into assembly and use that as fallback when compiler which builds the
C library doesn't support it. In such a case, it is certainly useful when
the library can choose any limb type it wants instead of requiring to
match a particular ABI limb type.
For scanf there is another problem. It is undefined behavior
to use va_arg (ap, _BitInt(64) *) when the next
argument has _BitInt(65) * or _BitInt(927) *
type. So, in order to implement bit-precise integer scanf
support, one still needs pedantically a large switch with
thousands or millions of similar pointer types, even when it will actually
most likely work properly with just one va_arg invocation.
While there probably are legacy reasons why C needs to allow different
representation of e.g. function pointers and data pointers, bit-precise
integer types are a recent addition to C and to the author's knowledge
there are no ABIs which would pass pointers to bit-precise integers
with different width differently. Thus this paper suggests at least
requiring compatibility of pointer to bit-precise integer types through
va_arg.
Introduce 3 new type-generic macros,
#include <stdarg.h>
void va_arg_bitint(va_list ap, size_t n, type *limbptr, bool big_endian, bool uns);
and
#include <stdbit.h>
void stdc_load_bitint(void *bitintptr, size_t n, const type *limbptr, bool big_endian, bool uns);
void stdc_store_bitint(const void *bitintptr, size_t n, type *limbptr, bool big_endian, bool uns);
The va_arg_bitint macro executes
if (uns)
stdc_store_bitint(&(unsigned _BitInt(n)){va_arg(ap, unsigned _BitInt(n))}, n, limbptr, big_endian, uns);
else
stdc_store_bitint(&(_BitInt(n)){va_arg(ap, _BitInt(n))}, n, limbptr, big_endian, uns);
except that n (nor the other macro arguments) is not required to
be a constant expression, while _BitInt(N) keyword requires
a constant expression N, so it actually works as if a large
switch based on n handling all supported widths of
bit-precise integers.
type can be any standard unsigned integer type or extended
unsigned integer type. Bit-precise integer types are intentionally
excluded from this set, supporting splitting of bit-precise integers
into array of _BitInt(17) or array of _BitInt(921)
wouldn't be very useful and would significantly complicate the implementation
of the macros.
If type type has width W, then the
stdc_load_bitint and stdc_store_bitint macros
assume limbptr points to an array of
(n + W - 1) / W elements and
bitintptr points to an object of type
_BitInt(N) if !uns or
unsigned _BitInt(N) otherwise where N is
equal to n.
stdc_store_bitint stores, if n is greater or equal
than W, the least significant W bits of
the bit-precise integer value into
limbptr[big_endian ? (n + W - 1) / W - 1 : 0],
if n is greater or equal than 2 * W,
the second least significant W bits of the bit-precise
integer value into
limbptr[big_endian ? (n + W - 1) / W - 2 : 1],
etc., up to the most significant ((n - 1) % W) + 1
bits of the bit-precise integer value, which is stored into least
significant bits of
limbptr[big_endian ? 0 : (n + W - 1) / W - 1].
If n % W != 0 the remaining bits are
cleared if uns or sign-extended otherwise.
stdc_load_bitint conversely sets the least significant
W bits of the bit-precise integer object to
limbptr[big_endian ? (n + W - 1) / W - 1 : 0],
the second last significant W bits of the bit-precise integer object to
limbptr[big_endian ? (n + W - 1) / W - 2 : 1],
etc. and finally the most significant ((n - 1) % W) + 1
bits to
limbptr[big_endian ? 0 : (n + W - 1) / W - 1].
It is not necessary to expose the size_t type name or the
stdc_store_bitint macro when including just the
<stdarg.h> header, the intent of the wording is that it acts as if
the macro is invoked and for promotion of the va_arg_bitint
argument to size_t it can use some internal type name instead.
The above macros have both endianity (big_endian) and signedness
(uns) as bool macro arguments and allow both of them
to be constant or non-constant. At least the endianity but the signedness
as well could be if the committee prefers it required to be a constant
expression, or remove the argument or arguments and move the endianity
and/or the signedness into the name of the macro, similar how e.g.
stdc_load8_leuN functions have those in the
function name and not as arguments. That said, signedness in the actual
implementation requires just one simple conditional (whether to zero-extend
or sign-extend the bits on stdc_store_bitint when storing the
most significant limb), or for stdc_load_bitint also to guard
just a few instructions, and endianity also can guard just a small amount of
code.
The author implemented built-in functions which can be used for these macros
in GCC,
so far only on x86-64 and i686 architectures and with the built-in function
for stdc_load_bitint unfinished yet, but the design is flexible
enough that other architectures can be added easily and the unfinished
built-in function can be completed.
Implementations which support large BITINT_MAXWIDTH need to
support these likely as compiler intrinsics, implementations which only
support the bare minimum set of bit-precise integer types could use e.g. for
va_arg_bitint something similar to the above
code.
The wording is relative to the latest Working Draft at time of publication, N3886.
Modify 7.16.1 General paragraph 4:
The type declared is
va_listwhich is a complete object type suitable for holding information needed by the macros
va_start,va_arg,va_arg_bitint,va_end, andva_copyto access the varying arguments. Objects of typeva_listare generally referred to as ap in this subclause. If an ap object is passed as an argument to another function and that function invokes theva_argorva_arg_bitintmacros on ap, the representation of ap in the calling function is indeterminate and ap shall be passed to theva_endmacro before being passed to any otherva_...macros.263) Whether a byte copy ofva_listcan be used in place of the original is implementation-defined.
Modify 7.16.2.1 General paragraph 1:
The
va_startand,va_argandva_arg_bitintmacros described in this subclause shall be implemented as macros, not functions. It is unspecified whetherva_copyandva_endare macros or identifiers declared with external linkage. If a macro definition is suppressed to access an actual function, or a program defines an external identifier with the same name, the behavior is undefined. Each invocation of theva_startandva_copymacros shall be matched by a corresponding invocation of theva_endmacro in the same function.
Modify 7.16.2.2 The va_arg macro paragraph 2:
The
va_argmacro expands to an expression that has the specified type and the value of the next argument in the call. The parameter ap shall have been initialized by theva_startorva_copymacro (without an intervening invocation of theva_endmacro for the same ap). Each invocation of theva_argmacro modifies ap so that the values of successive arguments are returned in turn. The behavior is undefined if there is no actual next argument. The parameter type shall be an object type name. If type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined, except for the following cases:
— both types are pointers to qualified or unqualified versions of compatible types;
— both types are pointers to qualified or unqualified versions of bit-precise integer types;
— one type is compatible with a signed integer type, the other type is compatible with the corresponding unsigned integer type, and the value is representable in both types;
— one type is pointer to qualified or unqualified void and the other is a pointer to a qualified or unqualified character type;
— or, the type of the next argument is nullptr_t and type is a pointer type that has the same representation and alignment requirements as a pointer to a character type.265)
Add a new subclause after 7.16.2.2 The va_arg macro:
7.16.2.3 The
va_arg_bitintmacroSynopsis
#include <stdarg.h> void va_arg_bitint(va_list ap, size_t n, generic_limb_type *limbptr, bool big_endian, bool uns);Description
The type-generic macro
va_arg_bitintreads a value of the next argument in the call as ifva_arg(ap, _BitInt(N))is invoked if uns argument isfalseor as ifva_arg(ap, unsigned _BitInt(N))otherwise where N is constant equal to n, stores that value into a compound literal of the same type and then writes that value into an array pointed to by limbptr as if the type-generic macrostdc_store_bitinthas been invoked with the address of the compound literal as first argument and the n, limbptr, big_endian and uns arguments passed as remaining arguments tostdc_store_bitint.
Add two new subclauses after 7.18.22 Endian-Aware 8-Bit Store:
7.18.23 Bit-Precise Integer Load
Synopsis
#include <stdbit.h> void stdc_load_bitint(void *bitintptr, size_t n, const generic_limb_type *limbptr, bool big_endian, bool uns);Description
The type-generic function
stdc_load_bitintreads a value from the array pointed to by limbptr and writes it into an object with bit-precise signed integer type with width equal to n pointed to by bitintptr when uns isfalseor into an object with bit-precise unsigned integer type with width equal to n pointed to by bitintptr otherwise, provided that the generic_limb_type type is a standard unsigned integer type or extended unsigned integer type. If n is smaller than 1 or larger than BITINT_MAXWIDTH, the behavior is undefined.Let W be the width of the generic_limb_type type.
Let E be(n + W - 1) / W. Let the computed value avalue be:
∑index=0E - 1 bindex × 2W×index
where bindex is:
— limbptr[index], if big_endian argument isfalse;
— otherwise, limbptr[E - index - 1], if big_endian argument istrue.Let value be avalue modulo 2n.
If uns argument istrue, then value is stored into the object pointed to by bitintptr;
otherwise, if value is smaller than 2n-1, then value is stored into the object pointed to by bitintptr;
otherwise, value – 2n is stored into the object pointed to by bitintptr.7.18.24 Bit-Precise Integer Store
Synopsis
#include <stdbit.h> void stdc_store_bitint(const void *bitintptr, size_t n, generic_limb_type *limbptr, bool big_endian, bool uns);Description
The type-generic function
stdc_store_bitintreads a value from the object with bit-precise signed integer type with width equal to n pointed to by bitintptr when uns isfalseor a value from the object with bit-precise unsigned integer type with width equal to n pointed to by bitintptr otherwise and writes it into the array pointed to by limbptr, provided that the generic_limb_type type is a standard unsigned integer type or extended unsigned integer type. If n is smaller than 1 or larger than BITINT_MAXWIDTH, the behavior is undefined.Let W be the width of the generic_limb_type type. Let value be the loaded bit-precise integer type value, if uns is
truewith bit-precise unsigned integer type with width equal to n, otherwise with bit-precise signed integer type with width equal to n.
Let index be an integer in a sequence that
— starts from 0 and increments by W in the range [0, n), if the big_endian argument isfalse;
— starts from(n - 1) / W * Wand decrements by W in the range [0, n), if the big_endian argument istrue.Let ptr_index be an integer that starts from 0. For each index in the order of the above-specified sequence:
— Sets the W bits in limbptr[ptr_index] to (generic_limb_type) (value >> index).
— Increments ptr_index by 1.
Many thanks to Joseph S. Myers, Martin Uecker and Aaron Ballman.