1
votes

I have a use for a macro that displays a message on stderr showing the current filename (from __FILE__) and line number (from __LINE__), but also allows the use of an optional format and variadic argument list, for custom messages. If a custom message is specified, the compiler must do a fairly decent job of warning about incorrect printf arguments.

It can be called in the following ways, with this arbitrary error code:

LogError(ErrorCode_TypeMismatch);                        // prints "[filename:lineno] Type Mismatch:"
LogError(ErrorCode_TypeMismatch, "type was %d", type);   // prints "[filename:lineno] Type Mismatch: type was 0"

The filename printed is always the basename (i.e. path is stripped).

In addition, it returns the error code value passed in, so that it can be used in the following manner:

result = LogError(ErrorCode_TypeMismatch);                     // result will be ErrorCode_TypeMismatch
result = LogError(ErrorCode_InvalidName, "name is %s", name);  // result will be ErrorCode_InvalidName

At the moment, I have this solution:

#include <stdio.h>
#include <stdarg.h>

// used to make strings from numeric macros
#define XSTRINGY(a) STRINGY(a)
#define STRINGY(a) #a

typedef enum {
    ErrorCode_TypeMismatch,
    ErrorCode_InvalidName,
    // ...
} ErrorCode;

#define LogError(code, ...) Log2(code, __VA_ARGS__)

// a function to simply return the value provided, used below
static inline ErrorCode ReturnErrorCode(ErrorCode code) { return code; }

#define Log2(code, format, ...) \
ReturnErrorCode(code); /* return the code */ \
{ \
    const char * fileName = __FILE__; \
    const char * lineNum = XSTRINGY(__LINE__); \
    const char * shortFileName = strrchr(fileName, '/'); \
    const char * errorString = ErrorCode_ToString(code); /* returns string representation of error code, implementation not shown here */ \
    DoLog("[%s:%s] %s: " format "\n", shortFileName ? shortFileName + 1 : fileName, lineNum, errorString, ##__VA_ARGS__); \
}

void DoLog(const char * format, ...) __attribute__ ((format (printf, 1, 2)));
void DoLog(const char * format, ...)
{
    va_list argp;
    va_start(argp, format);
    vfprintf(stderr, format, argp);
    va_end(argp);
}

This works well, and meets the requirements mentioned previously.

However I'd like to introduce a new requirement - that the LogError macro can be used with a return statement, for example:

return LogError(ErrorCode_TypeMismatch);
// or
return LogError(ErrorCode_TypeMismatch, "The values are %d, %d", value0, value1);

Unfortunately, due to the way the Log2 macro is written (to return the passed-in value), the processor will execute the return, and the remaining code to format the filename/line number and print it will not be executed. Therefore no output is generated (although the correct value is returned).

I have investigated numerous ideas concerning the comma operator and GNU statement expressions (which I'd like to avoid for portability reasons, although ##__VA_ARGS__ is acceptable) and whereas I have found some useful resources relating to returning values from a macro ("function" macros), I'm not able to find a solution that works for a variadic macro of this kind.

Alternatively, is there a way to detect that the macro has been used with a return statement and automatically stop the build? The compiler warning of unreachable code doesn't seem to trigger with this macro, and I'm not sure why that is.

1
Just an unrelated question, but why print the line (__LINE__) as a string instead of as a decimal number using "%d" format?Some programmer dude
Also, you have a serious flaw in your current Log2 macro, what if it's part of an if statement body without braces, e.g. if (some_condition) LogError(...); Only the ReturnErrorCode call (which is really a no-op here) will be in the condition.Some programmer dude
Ditch the variables, use __FILE__ et al directly in DoLog arguments.n. 1.8e9-where's-my-share m.
@JoachimPileborg yes, I think that flaw is part of the problem and why return LogError(...) is also problematic. There was a reason why __LINE__ was converted to a string, but it's lost in time. I can see how printing it with %d would suffice.davidA
@n.m. I did have a variant with this change, and you're right, it's a reasonable simplification to make, thank you. However it doesn't solve the return value problem.davidA

1 Answers

1
votes

You can solve it by not using statements in the Log2 macro, only expressions. That means you can't declare variable, but have to pass all the expressions as is to the DoLog function and not use temporary variables.

Maybe something like this

#define Log2(code, format, ...)                                          \
    (DoLog("[%s:%d] %s: " format "\n",                                   \
         strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__, \
         __LINE__,                                                       \
         ErrorCode_ToString(code),                                       \
         ##__VA_ARGS__                                                   \
     ),                                                                  \
     code /* Will be the result of the expression */                     \
    )

If you use it in a return statement, like

return LogError(ErrorCode_TypeMismatch);

the macro will be expanded as

return (DoLog(...), ErrorCode_TypeMismatch);

Which will evaluate the DoLog call, and thanks to the comma expression return the error code.