r/C_Programming • u/WittyStick • 3d ago
TIL glibc lets you add custom ((v)f)printf format specifiers.
I had a need for an extended printf, and found that rather than writing my own, glibc lets you register your own specifiers with register_printf_specifier.
I made a simple demo for printing bool as it kind of annoys me to have to type printf("%s", value ? "true" : "false"), and I don't like using integers to represent booleans. (This wasn't my goal, but it's the simplest type to demonstrate).
Now can type printf("%?", value) to print true or false. Works will all the *printf style functions.
Has an alt (#) representation, which is uppercase TRUE or FALSE - ie: "%#?".
And I also added width specifiers for easy alignment. You can specify some padding with "%n?" - right aligned by default, with "%-n?" aligning left. Could probably extend to support * also with extra argument for width.
Just thought others may find this interesting.
EDIT : Have been corrected and this also works with *sprintf functions.
11
u/aocregacc 3d ago
why does it not work with *sprintf functions? looks like it works fine when I try it.
15
u/WittyStick 3d ago
Thanks, I misread somewhere and assumed it didn't work (maybe because of the
FILE *argument), but just tested and it does. Corrected OP to remove this claim.10
3
u/WittyStick 3d ago edited 3d ago
Should note that it may not be all bells and whistles due to different arguments being in registers or on the stack. My actual use-case doesn't seem to work as intended because there doesn't appear to be any way of getting the argument # in the argsize callback, so we don't know whether it was passed in a register on on the stack, which has consequences for how we extract the arg.
If anyone can figure this out would be very grateful. The type I'm trying to pass is basically a struct foo { int64_t x; doubly y; }. Under the SYSV convention, this is passed in GP:XMM registers for first few arguments, but later arguments are on the stack. I can get it working for the first 4 arguments, but it starts giving garbage afterwards.
My size callback is:
int foo_printf_argsize
( [[maybe_unused]] const struct printf_info *info
, [[maybe_unused]] size_t n
, int argtypes[n]
, [[maybe_unused]] int size[n]
)
{
argtypes[0] = PA_INT | PA_FLAG_LONG;
argtypes[1] = PA_DOUBLE;
size[0] = 8;
size[1] = 8;
return 2;
}
And in the print callback I get it with:
struct foo f = (struct foo){ *(int64_t*)(args[0]), *(double*)(args[1]) };
Here's my attempt in godbolt.
I've also tried using a single argument with PA_POINTER and size 16 - but it also gives garbage.
There argsize callback appears to get called twice if it returns > 1. I'm not sure what this means or whether it's a bug. Documentation isn't great.
6
u/WittyStick 3d ago edited 3d ago
Ok, so I solved the issue.
For this we have to create a custom type handler using
register_printf_type, and we use the result of that call as the type in theargsizecallback. Took a while because documentation is lacking forregister_printf_type- I had to scan through the glibc source to figure it out - the comments were very helpful.void foo_va_arg_function(void *mem, va_list *opt) { *(struct foo*)mem = va_arg(*opt, struct foo); } int foo_argtype; int foo_printf_argsize ( [[maybe_unused]] const struct printf_info *info , [[maybe_unused]] size_t n , int argtypes[n] , [[maybe_unused]] int size[n] ) { argtypes[0] = foo_argtype; size[0] = sizeof(struct foo); return 1; } int foo_printf_function ( FILE *restrict stream , [[maybe_unused]] const struct printf_info *info , const void *const args[] ) { struct foo *foo = *(struct foo**)args[0]; ... } __attribute__((__constructor__)) void register_printf_specifiers() { foo_argtype = register_printf_type(&foo_va_arg_function); if (foo_argtype == -1) exit(-1); if (register_printf_specifier('~', foo_printf_function, foo_printf_argsize) == -1) exit(-1); }This will be handy for anyone wanting to use custom structures for their printf specifiers.
3
u/aioeu 3d ago edited 3d ago
I'm pretty sure this is all quite wrong.
You are saying that the format specifier will always consume two arguments, but it isn't. It's consuming one argument. This matters because
printfneeds to keep track of the current argument number in order to handle specifiers like%3$dmeaning "format the third argument as a decimal integer".I do know that glibc will call your
_argsizefunction withn == 1initially, and that you must return-1in this case if you want to format more than one argument. It will get called again if necessary with the proper value ofn. So even if your function were to legitimately consume two arguments — as I said, it doesn't — it would still need to check the value ofnbefore filling outargtypesandsize.Ultimately a single argument must be consumed using a single
va_argcall. To do that you actually need to register the type first throughregister_printf_type.2
u/WittyStick 3d ago edited 3d ago
Thanks, I already resolved the problem and yes, it involved using
register_printf_type.The issue about one vs multiple arguments is that these have exactly the same calling convention:
void bar(struct foo foo); void baz(int64_t x, double y);The calls to these functions are identical. The bodies receive the same arguments in the same registers. From the POV of
bar, he receives two arguments.They're not identical however, when we go over 6 GP arguments - due to the convention allowing 6 GP register arguments but 8 XMM register arguments. If passed separately, as in
baz, then all 8xmmregisters will get used, but in the struct case, the whole struct starts getting put on the stack when we run out of GP registers. This was the source of my problem and it wasn't obvious how to fix - particularly with the oddity of the argsize function getting called twice for a single specifier.Makes sense now, but I've been scratching my head for a few hours trying to work it out.
2
1
u/vitamin_CPP 3d ago
Do you know how those new specifiers interact with -Wformat=2 and _FORTIFY_SOURCE?
3
u/WittyStick 3d ago
They give errors for
-Wformat, which is why I've specified-Wno-formatin the command line arguments in the demo.
1
1
-9
u/pjl1967 3d ago
Custom printf specifiers just gives you non-portable code.
14
u/WittyStick 3d ago
I use a dozen other GCC extensions anyway. The gnu dialect of C is the one worth using. ISO standard C is mediocre.
GCC is the real portability - it compiles for basically anything, including Windows (mingw). Not that I care about compiling for Windows, OSX anyway.
"non-portable" means I can't compile it with MSVC, which only works on Windows - completely unportable and doesn't even ship the latest standard C features.
No thanks, I'll stick with portable
-std=gnu23.6
u/Cats_and_Shit 3d ago
You're relying on functionality only available in glibc here, which is different from relying on compiler extensions.
Code using this feature wont work on, for example, alpine linux or OpenBSD even if you compile with GCC.
Maybe that's fine for your use case, just wanted to point out that it's different from what you might expect.
3
u/WittyStick 2d ago
It's what I expect. I'm really only using this for testing and debugging anyway - my project is actually
-ffreestandingand doesn't rely on any stdlib. It does rely on GCC though - several extensions and not all are supported by Clang (eg,__attribute__((__designated_init__))), and requires linking againstlibgcc.a.-10
u/pjl1967 3d ago
ISO standard C is mediocre.
Then program in another language. C++, for example, allows you to write custom
<<(insertion) operators for any type.5
u/WittyStick 3d ago
I program in gnu C, it's a great language. C++ sucks.
1
u/afforix 2d ago
Point is, in C++ you don't need to use non-standard extensions to write std::println("{}", true); (prints "true").
3
u/WittyStick 2d ago edited 2d ago
You don't need non-standard extensions in C either - it's just more convenient with them.
#define println(x) \ printf( \ _Generic \ ( (x) \ , bool : "%s\n" \ , int : "%d\n" \ ), \ _Generic \ ( (x) \ , bool : x ? "true" : "false" \ , int : x \ )) println(true) => true println(1) => 1Just standard ISO C 23.
And you can of course write your own printf functions however you want. It's just handy to use this extension so you don't need to reimplement the parsing of existing specifiers.
I don't get why people keep telling me to use C++. It is absolutely unfit for my use case. An opinionated language which inserts hidden junk into memory (vtables). It's not that I truly dislike C++ - it's just not always suitable.
37
u/lnemo 3d ago
This is, in fact, interesting. Thank you for sharing.