I recently needed a macro with variable arguments that work on all mainstream compilers. I use gcc only on Linux, on the other platforms I want to use the „native“ compiler that belongs to the OS. Let’s start with Linux.
Linux
If you’re inside the gnu/gcc world, things are easy. This is what everybody tries first:
1 2 3 4 5 6 7 8 9 10 11 | #include "stdio.h" /* I use "stdio.h" because my blog software cant't handle angle brackets inside a preformated text blog ... :( */ #define PRINTERROR(x, ...) fprintf(stderr, "ERRORMESSAGE = " f "\n" , __VA_ARGS__); int main(void) { PRINTERROR("Hello %s!", "world"); return 0; } |
The problems start, if you want to use the macro without variable arguments.
9 | PRINTERROR("Hello world!"); |
gcc will not accept the code, because it resolves the macro to
9 | fprintf(stderr, "ERRORMESSAGE = " "Hello world!" "\n" ,); |
(note the comma)
test.c: In function ‘main’: test.c:9: error: expected expression before ‘)’ token |
The gcc workaround for this kind of a situation is:
5 | #define PRINTERROR(x, s...) fprintf(stderr, "ERRORMESSAGE = " f "\n" , ## s); |
If „s“ is null, this erases the comma to the left of the „##“ in the macro definition. The resulting expansion is:
9 | fprintf(stderr, "ERRORMESSAGE = " "Hello world!" "\n" ); |
The comma is gone, the code compiles. Great.
HP-UX
However, this approach fails on HP-UX
cc -o test test.c cc: "test.c", line 9: error 1000: Unexpected symbol: ")". |
I found the solution for this problem in the HP-docs:
you must insert a space to the left of the comma to make it clear to the preprocessor that the comma is the left operand of the „##“ operator.
Yes. A space. Left of the comma. Sigh….
The definition that works on Linux and HP’s cc -AC99
compiler looks like this:
5 | #define PRINTERROR(x, s...) fprintf(stderr, "ERRORMESSAGE = " x "\n" , ## s); |
Solaris
Solaris c89 compiles the above definition with a warning:
"test.c", line 9: warning: argument mismatch |
Solaris c99 compiler is very strict and allows
5 | #define FOO(...) printf(__VA_ARGS__) |
only. The documentation points out, that it is possible to use #__VA_ARGS__
which seems to be similar to GNU’s/HP’s ## Operator:
5 | #define PRINTERROR(x, ...) fprintf(stderr, "ERRORMESSAGE = " x "\n" , # __VA_ARGS__ ); |
Ok, the code compiles, but calling PRINTERROR results in a „argument mismatch“-warning. I found no other solution than disabling the warning. Dirty but it works:
5 6 7 | #define PRINTERROR(x, ...) printf(stderr, "ERRORMESSAGE = " x "\n" , # __VA_ARGS__ ); /* optional: disables "warning: argument mismatch" */ #pragma error_messages (off, E_ARGUEMENT_MISMATCH) |
If course it is bad to hide warnings, but if your code is multi-platform, you can use very strict compiler settings on another platform to make the warnings visible at least there.
AIX
The original AIX-compiler (cc) does not complain, but c89 fails with
"test.c", line 3.16: 1506-1128 (S) The GNU variable argument identifier "s" of macro "FOO" is not permitted in the current langlvl mode. "test.c", line 7.29: 1506-041 (E) The invocation of macro FOO contains fewer arguments than are required by the macro definition. |
There is a langlvl #pragma to switch the compiler language level. All levels that allow implementation-specific language extensions are sufficient. I used extc99
which allows to use the c99 compiler. Simply add this to the beginning of your sourcefile:
#pragma langlvl (extc99) |
The final result
This is the result of some hours browsing the compiler docs. The macro works on all c99-compilers mentioned above and allows zero arguments:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #ifdef _AIX #pragma langlvl (extc99) #endif #include "stdio.h" #ifdef _SOLARIS #define PRINTERROR(x, ...) fprintf(stderr, "ERRORMESSAGE = " x "\n" , # __VA_ARGS__ ); /* optional: disables "warning: argument mismatch" */ #pragma error_messages (off, E_ARGUEMENT_MISMATCH) #else #define PRINTERROR(x, s...) fprintf(stderr, "ERRORMESSAGE = " x "\n" , ## s); #endif int main(void) { PRINTERROR("NO PARAM"); PRINTERROR("Param: %s", "foo"); return 0; } |
Of course you have to use the compiler switch -D_AIX
, -D_SOLARIS
or -D_HPUX
on the apropriate platform. Let me know what do you think and how it could be done better.
Update: André suggests this solution (see his comment):
void PRINTERROR_verbose(const char * file, int line, int level, char *format, ...) { int size; char *cp; va_list args; if (level < = 2) { va_start(args, format); size = vsnprintf(NULL, 0, format, args) + 1; va_end(args); va_start(args, format); cp = (char *)calloc(size, sizeof(char)); if (cp != NULL && vsnprintf(cp, size, format, args) > 0) { /* Ausgabe von cp hier via syslog, stderr, stdout, etc. */ /* oder cp noch mal umformatieren fuer Sonderzeichen und/oder */ /* zusaetzliche Zeitangabe */ /* bei Debuglevel 1 (z.B. ERROR) wird dann noch etwas mehr angezeigt */ if (level < = 1) { fprintf(stderr, "\tFile '%s' in line %i\n", file, line); } } va_end(args); free(cp); } } #define PRINTERROR(...) PRINTERROR_verbose(__FILE__, __LINE__, __VA_ARGS__) |
Ich werd‘ nicht so recht schlau aus deinem Code. Warum definiertst du kein einfaches PRINTERROR(…). Das sollte nach C89 (C99 für vsnprintf) überall funktionieren:
############# schnipp ##############
void PRINTERROR_verbose(const char * file, int line, int level, char *format, …) {
int size;
char *cp;
if (level 1) {
cp = (char *)calloc(size, sizeof(char));
if (cp != NULL && vsnprintf(cp, size, format, args) > 0) {
/* Ausgabe von cp hier via syslog, stderr, stdout, etc. */
/* oder cp noch mal umformatieren für Sonderzeichen */
/* bei Debuglevel 1 (z.B. ERROR) wird dann noch etwas mehr angezeigt */
if (level <= 1) {
fprintf(stderr, "\tFile '%s' in line %i\n", file, line);
}
}
}
}
}
#define PRINTERROR(…) PRINTERROR_verbose(__FILE__, __LINE__, __VA_ARGS__)
############# schnapp ##############
Der erste Parameter für dein PRINTERROR() müsste im obigen Beispiel dann dein Debuglevel sein.
Jaja, das free(cp) fehlt dann noch weiter unten 🙂
(Und die Formatierung im Kommentar ist mir auch nicht gelungen.)
ja, so kann man es machen, allerdings erlaubt C99 _nur_ MACRO(__VA_ARGS__), man muss also immer den relaxten Compilermode verwenden. Ich hatte mir halt in den Kopf gesetzt, das ohne zusätzlichen Funktionsaufruf umzusetzen, aber letztendlich ist eine zusätzliche Wrapperfunktion vermutlich am einfachsten. Sobald ich wieder zugriff auf die Maschinen habe (Januar), teste ich Deinen Vorschlag und werde den Artikel updaten.
/* I use „stdio.h“ because my blog software cant’t handle
angle brackets inside a preformated text blog … 🙁 */
Replace them with < and > and it should show up.