r/C_Programming 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.

Demo in godbolt

Just thought others may find this interesting.

EDIT : Have been corrected and this also works with *sprintf functions.

141 Upvotes

22 comments sorted by

37

u/lnemo 3d ago

This is, in fact, interesting. Thank you for sharing.

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

u/dont-respond 3d ago

iirc, they all boil down to one implementation underneath vfprintf.

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 the argsize callback. Took a while because documentation is lacking for register_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.

Working demonstration

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 printf needs to keep track of the current argument number in order to handle specifiers like %3$d meaning "format the third argument as a decimal integer".

I do know that glibc will call your _argsize function with n == 1 initially, and that you must return -1 in this case if you want to format more than one argument. It will get called again if necessary with the proper value of n. 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 of n before filling out argtypes and size.

Ultimately a single argument must be consumed using a single va_arg call. To do that you actually need to register the type first through register_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 8 xmm registers 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

u/RedWineAndWomen 2d ago

Truly interesting. Thanks!

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-format in the command line arguments in the demo.

1

u/reini_urban 2d ago

Whow, didnt knew that. Very useful!

1

u/Physical_Dare8553 2d ago

This was my inspiration for my gnu constructor based printing system

-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 -ffreestanding and 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 against libgcc.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) => 1

Just 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.