Thread: typesafe printf hackery

typesafe printf hackery

From
Andres Freund
Date:
Hi,

I've occasionally wished for a typesafe version of pg_printf() and other
varargs functions. The compiler warnings are nice, but also far from
complete.

Here's a somewhat crazy hack/prototype for how printf could get actual
argument types.  I'm far from certain it's worth pursuing this
further... Nor the contrary.

Note that this requires removing the parentheses from VA_ARGS_NARGS_'s
result (i.e. (N) -> N). To me those parens don't make much sense, we're
pretty much guaranteed to only ever have a number there.

With the following example e.g.

    myprintf("boring fmt", 1, 0.1, (char)'c', (void*)0, "crazy stuff");
    myprintf("empty argument fmt");

yield

format string "boring fmt", 5 args
    arg number 0 is of type int: 1
    arg number 1 is of type double: 0.100000
    arg number 2 is of type bool: true
    arg number 3 is of type void*: (nil)
    arg number 4 is of type char*: crazy stuff
format string "empty argument fmt", 0 args


which'd obviously allow for error checking inside myprintf.


#include "c.h"

// hack pg version out of the way
#undef printf

// FIXME: This doesn't correctly work with zero arguments
#define VA_ARGS_EACH(wrap, ...) \
    VA_ARGS_EACH_EXPAND(VA_ARGS_NARGS(__VA_ARGS__)) (wrap, __VA_ARGS__)
#define VA_ARGS_EACH_EXPAND(count) VA_ARGS_EACH_EXPAND_REALLY(VA_ARGS_EACH_INT_, count)
#define VA_ARGS_EACH_EXPAND_REALLY(prefix, count) prefix##count

#define VA_ARGS_EACH_INT_1(wrap, el1) wrap(el1)
#define VA_ARGS_EACH_INT_2(wrap, el1, ...) wrap(el1), VA_ARGS_EACH_INT_1(wrap, __VA_ARGS__)
#define VA_ARGS_EACH_INT_3(wrap, el1, ...) wrap(el1), VA_ARGS_EACH_INT_2(wrap, __VA_ARGS__)
#define VA_ARGS_EACH_INT_4(wrap, el1, ...) wrap(el1), VA_ARGS_EACH_INT_3(wrap, __VA_ARGS__)
#define VA_ARGS_EACH_INT_5(wrap, el1, ...) wrap(el1), VA_ARGS_EACH_INT_4(wrap, __VA_ARGS__)
#define VA_ARGS_EACH_INT_6(wrap, el1, ...) wrap(el1), VA_ARGS_EACH_INT_5(wrap, __VA_ARGS__)
#define VA_ARGS_EACH_INT_7(wrap, el1, ...) wrap(el1), VA_ARGS_EACH_INT_6(wrap, __VA_ARGS__)


typedef enum printf_arg_type
{
    PRINTF_ARG_BOOL,
    PRINTF_ARG_CHAR,
    PRINTF_ARG_INT,
    PRINTF_ARG_DOUBLE,
    PRINTF_ARG_CHARP,
    PRINTF_ARG_VOIDP,
} printf_arg_type;

typedef struct arginfo
{
    printf_arg_type tp;
} arginfo;

// hackfix empty argument case
#define myprintf(...) myprintf_wrap(__VA_ARGS__, "dummy")
#define myprintf_wrap(fmt, ... ) \
    myprintf_impl(fmt, VA_ARGS_NARGS(__VA_ARGS__) - 1, ((arginfo[]){ VA_ARGS_EACH(blurttype, __VA_ARGS__) }),
__VA_ARGS__)

// FIXME: Obviously not enough types
#define blurttype(x) ((arginfo){_Generic(x, char: PRINTF_ARG_BOOL, int: PRINTF_ARG_INT, double: PRINTF_ARG_DOUBLE, char
*:PRINTF_ARG_CHARP, void *: PRINTF_ARG_VOIDP)})
 

static const char*
printf_arg_typename(printf_arg_type tp)
{
    switch (tp)
    {
        case PRINTF_ARG_BOOL:
            return "bool";
        case PRINTF_ARG_CHAR:
            return "char";
        case PRINTF_ARG_INT:
            return "int";
        case PRINTF_ARG_DOUBLE:
            return "double";
        case PRINTF_ARG_CHARP:
            return "char*";
        case PRINTF_ARG_VOIDP:
            return "void*";
    }

    return "";
}

static void
myprintf_impl(char *fmt, size_t nargs, arginfo arg[], ...)
{
    va_list args;
    va_start(args, arg);

    printf("format string \"%s\", %zu args\n", fmt, nargs);
    for (int argno = 0; argno < nargs; argno++)
    {
        printf("\targ number %d is of type %s: ",
               argno,
               printf_arg_typename(arg[argno].tp));

        switch (arg[argno].tp)
        {
            case PRINTF_ARG_BOOL:
                printf("%s", ((bool) va_arg(args, int)) ? "true" : "false");
                break;
            case PRINTF_ARG_CHAR:
                printf("%c", (char) va_arg(args, int));
                break;
            case PRINTF_ARG_INT:
                printf("%d", va_arg(args, int));
                break;
            case PRINTF_ARG_DOUBLE:
                printf("%f", va_arg(args, double));
                break;
            case PRINTF_ARG_CHARP:
                printf("%s", va_arg(args, char *));
                break;
            case PRINTF_ARG_VOIDP:
                printf("%p", va_arg(args, void *));
                break;
        }

        printf("\n");
    }
}

int main(int argc, char **argv)
{
    myprintf("boring fmt", 1, 0.1, (char)'c', (void*)0, "crazy stuff");
    myprintf("empty argument fmt");
}


Greetings,

Andres Freund