1

I built a printf-like variadic function for logging purposes on a custom CPU.

To ensure that I have no memory faults I wrote the function so that it only takes pointers and handles them. The preprocessor adds always a NULL-Pointer at the end of the va-list, so whenever the function recocnizes a zero-pointer it returns. In this way, I can be sure that I don't read the List. I did this all to prevent from wrong handling with the programme, it should be a safe function. Whatever the user puts into the va-list shouldn't end in a crash.

The only problem I have now is to ensure that the user can only pass pointers to the variadic function, nothing else (except the first two arguments which are not part of the va-list) my function declaration looks like this:

void LogNew( UINT LogClass, char* pMsg, ... );

And the null-Termination:

#define LogNew(...) LogNew(__VA_ARGS__, 0)

Is there a way, maybe via Preprocessor, to make sure that no one uses call by value instead of call by reference.

Thank You in Advance!

M Oehm
  • 28,726
  • 3
  • 31
  • 42
str0yd
  • 97
  • 11
  • 2
    no, you can't that why variadic function are unsafe. Maybe you could write a compiler extension that do that. – Stargateur Nov 06 '17 at 13:16
  • 3
    "0" is a pretty bad choice in C, since there's no way for the compiler to deduce that you meant a pointer. Use `NULL`, or `(void *) 0`. – unwind Nov 06 '17 at 13:32
  • Could you please show some examples of how you invoke the `LogNew` macro? Will the pointers all be pointers to `char`? – M Oehm Nov 06 '17 at 13:32
  • @unwind: since I'm using a va-list I anyway don't have any information about this. – str0yd Nov 06 '17 at 13:35
  • 3
    Since you're using a va list, the compiler will assume that `0` is the integer `0`. Use `(void *) 0` as suggested. – M Oehm Nov 06 '17 at 13:38
  • @MOehm no, the pointers are not only to char, they are either int*, long*, float* or char*. I'm determining the type of the va_arg over the char after the '%' in the pMsg string. – str0yd Nov 06 '17 at 13:47
  • Borrowing from many of the _unsafe_ (but commonly used) variadic functions in standard C libraries, you can design your prototype to use a format string. As already pointed out, there is no guarantee what a user will _try_ to pass, but you can certainly filter out (or detect) argument types that do not comply with your rules. – ryyker Nov 06 '17 at 13:49
  • @MOehm but it doesen't solve the problem that the user can input LogNew( 0, "Hello %d", 9388593 ) instead of LogNew( 0, "Hello %d", &intVariable ). In this case for example 9388593 would be interpreted as pointer and this could cause to an exception. – str0yd Nov 06 '17 at 13:51
  • @str0yd - the compiler will catch this if the prototype contains `int *` as it's last argument. – ryyker Nov 06 '17 at 13:52
  • @str0yd: the point of these comments is that `int` is not necessarily the same as `void*` in C. Most likely it *has* the same width, but it's not guaranteed. Also, may we ask *why* you want to enforce this rule? You are basically creating a "printf-like" variadic function, which doesn't behave like printf, and this is what will lead to developer mistakes. – vgru Nov 06 '17 at 13:54
  • @ryyker this is exactly what i tried. But there the problems begin: if you pass e.g. LogNew(0, "Something %s", 1) the program will crash. That's why i decided to only allow pointers passing by the user, they never have a wrong adress. And unfortunately you can't test it via format string because in C we don't know if the pointer is a valid address or not. – str0yd Nov 06 '17 at 14:00
  • @Groo Thanks, I see the problem and changed it but this doesn't solves my main problem. – str0yd Nov 06 '17 at 14:02
  • 1
    The flexibility of `printf` comes with a price in run-time safety. You should either accept that (but I would still prefer to have standard `printf` parameter semantics, than having your own set of rules), or skip using variadic functions altogether. – vgru Nov 06 '17 at 14:05
  • 2
    if you use standard printf semantics and have gcc, you can add `__attribute__(( format( printf, 2, 3 ) ))` to make the compiler check the arguments – Ingo Leonhardt Nov 06 '17 at 14:09
  • What is the actual problem you are trying to solve? Variadic functions are almost certainly the wrong solution to another problem than the one you are trying to solve. It _is_ possible to make this 100% type safe in C, but for that we need a finite number of function calls. A sane C design typically just takes one parameter, which is a struct. And then you implement some manner of polymorphism from there. – Lundin Nov 06 '17 at 14:24

2 Answers2

0

You can wrap each argument in an expression that ensures that only a pointer will be allowed (while preserving its value). For example, you can convert a argument elem to:

(true ? (elem) : (void *) 0)

The ternary condition ensures that its second and third arguments are compatible. Note that this will still allow literal zero as an parameter; you can detect that at runtime (the corresponding parameter will be a null pointer); also, for many implementations this will only generate a warning rather than a hard error.

Another option is to apply the &* operators to the argument. The problem here is that this won't work for void pointers, but a _Generic selection macro can help:

&*(_Generic((elem), void *: ((char*) elem), const void *: ((char const*) elem), default: (elem)))

Wrapping each argument is a bit tricky, but there are macro programming libraries which can help. For example, using Boost.Preprocessor you could write:

#include <boost/preprocessor.hpp>

void LogNew(unsigned LogClass, char* pMsg, ... ) {}
#define LOG_NEW_ELEMENT(r,data,elem) \
    , &*(_Generic((elem), void *: ((char*) elem), const void *: ((char const*) elem), default: (elem)))
#define LOG_NEW_IMPL(LogClass,Args) \
    LogNew(LogClass, \
    BOOST_PP_SEQ_HEAD(Args) \
    BOOST_PP_SEQ_FOR_EACH(LOG_NEW_ELEMENT, _, BOOST_PP_SEQ_TAIL(Args)), \
    ((void*) 0))
#define LogNew(LogClass,...) \
    LOG_NEW_IMPL(LogClass, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__))

Example:

int main() {
    char msg[] = "hello";
    LogNew(1u, msg);
    LogNew(1u, msg, "world");
    LogNew(1u, msg, (void *) "ok");
    LogNew(1u, msg, 123);  // error: invalid type argument of unary '*' (have 'int')
}
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • @AndrewHenle sure, why not? Boost.Preprocessor doesn't care whether it's being used with C or C++. – ecatmur Nov 06 '17 at 14:27
  • Unfortunately, lots of broken or non-standard implementations such as gnu11 will allow implicit pointer conversions from integers to pointers. Which means that `(true ? (elem) : (void *) 0)` won't help. – Lundin Nov 06 '17 at 14:28
  • @Lundin good point, I've put up an alternative using `_Generic`. – ecatmur Nov 06 '17 at 14:45
  • Not quite sure how that solves anything. What if `elem` is neither a pointer nor an `int`? – Lundin Nov 06 '17 at 14:57
  • @Lundin in that case you get an error `incompatible type for argument 1 of ‘ErrorMustBeAPointer’` as in the float example above. – ecatmur Nov 06 '17 at 15:28
  • No... `long`, `short`, `char`. And their signed/unsigned flavours. C even allows `bool` as a compatible type to `int`. And then all `enum` types. – Lundin Nov 06 '17 at 15:30
  • @Lundin yeah, tricky. I think `&*` does the trick, with a `_Generic` to deal with `void*` pointers. – ecatmur Nov 10 '17 at 17:08
0

If you want to reduce or prevent errors with wrong formats, your best bet is to use fprintf, in my opinion. It is a variadic function and therefore unsafe, but it is the way to print stuff in C, so programmers are familiar with it and its shortcomings. If your logging function is just a macro, for example:

enum {Fatal, Error, Warning, Info};

int maxlevel = Error;

#define logmsg(L, ...) if (L > maxlevel); else {        \
        if (L == Fatal) fprintf(stderr, "Fatal: ");     \
        if (L == Error) fprintf(stderr, "Error: ");     \
                                                        \
        fprintf(stderr, __VA_ARGS__);                   \
        fprintf(stderr, "\n");                          \
                                                        \
        if (L == Fatal) exit(1);                        \
    }                                                   \

you get the benefit of static analysis, which will warn you about format/type mismatches. in clang/gcc, -Wformat, which is part of -Wall, will do that. In Visual Studio, you can use /analyze. (And you won't generate the va list when logging is suppressed.)

If you roll your own variable-argument function and call vfprintf, you can detect bad format strings with the clang/gcc format attribute or the _Printf_format_string_ anotation in Visual Studio.

That said, if all of your variable arguments are of the same type, you can use an array. If your compiler supports C99's compound literals, you can use a macro to convert the variable portion of your argument list into an array.

Say that all printed arguments should be const char *. (I know that's not what you want, but bear with me.) Then you can achieve a printing function that takes any positive amount of C strings like this:

void put(const char **s)
{
    while (*s) {
        fputs(*s++, stdout);
    }

    fputs("\n", stdout);
}

#define put(...) put((const char *[]){__VA_ARGS__, NULL})

And use it like this:

put("Couldn't open \"", fn, "\".");
put("My name is ", (rand() % 2) ? "Igor" : "Tamara", ".");
put("");

You can extend this technique to const void *. I've hidden the code behind an Ideone link, because using void will make you lose the type information of the arguments. Your code probably won't crash hard, because the pointers should point somewhere, but you will still invoke undefined behaviour and get garbage output when you use the wrong format.

(In comments to the first answer, Lundin points out that converting an integer type to a pointer is legal. That's not a defect of the shown answer, that's a defect of the language. You could also say puts(54) and only get a warning.)

M Oehm
  • 28,726
  • 3
  • 31
  • 42