[Introduction]

Unix Incompatibility Notes:
Variadic Functions

Jan Wolter

This page describes portability issues related to variadic functions in C under Unix. Variadic functions are those that do not always take the same number of arguments, like printf() and execl(). Though all modern Unix implementations support the same standard, older ones use a different syntax, and really old ones don't have any formal support for variadic functions at all.

As an example, we will show several implementations of a function called concat() which is passed a pointer to a buffer, and zero or more pointers to null terminated strings followed by a NULL. It concatinates together all the strings, stores them in the buffer (without checking for overflow) and returns the address of the result. So we could do a call like

concat(buf, "Hello", " ", "World", "\n", NULL);
which would store the string "Hello World\n" in buf.

Note that C variadic functions cannot directly tell how many arguments they were called with. You have to do something like pass a NULL as the last argument or pass some kind of literal or implicit argument count so that the function can figure out how many arguments it has.

If your code already requires function prototypes or other features of ANSI C, then can reasonably assume that stdarg.h will be available, and not worry about including support for systems with only varargs.h or prehistoric systems that have neither. People eager to install your software on older systems will have to install gcc or some other ANSI C compiler, and stdarg.h will come with it. Only if you are trying to support pre-ANSI compilers need you worry about coding in support for other variadic function implementations.

Using stdarg.h

All ANSI compliant C compilers support variadic functions using a set of macros defined in the stdarg.h header file. Using this, we would define the concat() function like this:
   #include <stdarg.h>

   char *concat(char *bf, ...)
   {
       va_list ap;
       char *p;

       va_start(ap, bf);	/* bf being the last argument before '...' */

       bf[0]= '\0';
       while ((p= va_arg(ap, char *)) != NULL)
           strcat(bf,p);
   
       va_end(ap);
       return bf;
   }

The stdarg.h header file defines four macros, va_list, va_start(), va_end() and va_arg().

Va_list is a special data type for storing argument lists. In addition to walking through the argument lists with va_arg() you can also pass them to functions like vprintf().

The va_start() and va_end() macros should bracket all va_arg() calls. Many man pages for stdarg say that you can do multiple passes through the argument list by bracketing each pass in its own va_start() and va_end() calls. I believe that this works in all implementations, but am not sure.

Often the va_list structure is passed from a function with variadic arguments to another, like vprintf(). If such a function wants to make multiple passes through the argument list, the trick of doing multiple va_start() and va_end() calls doesn't work. Some implementations define a macro va_copy(ap2,ap1) which creates a copy of the va_list variable ap1 named ap2. This can be used to make multiple passes through a va_list. Just make a copy, walk through that, then walk through the original. However, although the ISO C99 standard requires va_copy(), most older compilers don't have it. Many systems that have va_copy() don't have a manual page for it. Some older gcc systems call it __va_copy() instead (you can check for this with an #ifdef). Many systems have no equivalent to va_copy(). In such cases, doing memcpy(&ap2, &ap1, sizeof(va_list)) is more likely to work than simply doing ap2=ap1, but neither is guaranteed. (If these do work, you should be careful not to call va_end() on ap2, but only on ap1.) So there appears to be no portable way for functions passed va_list type parameters to make multiple passes through the argument list.

Note that the va_start() function must be passed the name of the last fixed argument, the one that appears before the '...' in the function prototype. This is a problem if you have no fixed arguments. One case where this proves to be particularly obnoxious is if you want to write a function that does a little work, then passes its arguments on to something like vprintf().

Note the discussion below of va_arg() types and matching up va_start() and va_end() calls when using varargs.h. I've never seen anything to indicate that these might be concerns with stdarg.h, but to be maximally paranoid, it wouldn't hurt to write your code as if they were.

Using varargs.h

Before ANSI, the prevalent solution to this problem were the very similar macros defined by varargs.h. Most modern Unix systems still have varargs.h for backward compatibility. Using this, the concat() function would be written like this:
   #include <varargs.h>

   char *concat(va_alist)
   va_dcl
   {
       va_list ap;
       char *bf, *p;

       va_start(ap);
       bf= va_arg(ap, char *);

       bf[0]= '\0';
       while ((p= va_arg(ap, char *)) != NULL)
           strcat(bf,p);
   
       va_end(ap);
       return bf;
   }

Note that va_start() does not require the last fixed argument to be passed in, which is good because the function declaration can contain no fixed arguments. All arguments are always variable. We have to set up the variables for our fixed arguments ourselves and we have no function prototype, but we gain the ability to write functions with no fixed arguments.

As with stdarg.h, some but not all implementations define a va_copy macro, and some but not all manual pages say you can do multiple passes by bracketing each pass with va_start() and va_end().

The type given as the second argument of the va_arg() macro should generally not be char, short or float. That's because if these types are passed to the function, they will be promoted to int or double before they are passed in. There can be portability problems if you give the original types instead of the promoted types.

There may be some systems where va_start() includes an opening bracket that is closed by a matching bracket in va_end(), so these macros should be used on the same level, not with the va_end() inside an if statement or something.

The Age of the Dinosaurs

The C language started life with the curious defect of not having any portable way to implement functions like printf(). There are various kludges that will work for most systems in limited situations. For example:
   char *concat(bf, s1, s2, s3, s4, s5, s6)
   char *bf, *s1, *s2, *s3, *s4, *s5, *s6;
   {
       bf[0]= '\0';
       if (s1 == NULL) return bf;
       strcat(bf,s1);
       if (s2 == NULL) return bf;
       strcat(bf,s2);
       if (s3 == NULL) return bf;
       strcat(bf,s3);
       if (s4 == NULL) return bf;
       strcat(bf,s4);
       if (s5 == NULL) return bf;
       strcat(bf,s5);
       if (s6 == NULL) return bf;
       strcat(bf,s6);
       return bf;
   }

Since function prototypes haven't been invented, the compiler will happily allow calls to this function with any number of arguments. If more than six strings are passed in, of course, then only the first six will be concatinated.

This becomes more problematic if the arguments aren't all of the same type. If they are all of the same size then you might be able to get away by declaring them as one type and typecasting them into another, but this isn't likely to be very portable.

One trick that is somewhat more likely to be portable is to write wrappers around printf() style functions like this:

   errf(fmt, arg1, arg2, arg3, arg4, arg5, arg6)
   char *fmt;
   long arg1, arg2, arg3, arg4, arg5, arg6;
   {
      printf("Error: ");
      printf(fmt, arg1, arg2, arg3, arg4, arg5, arg6);
   }
The arguments actually passed in are unlikely to be all long integers, so the contents of the arg variables are likely to be gibberish. But when we push them all back on the stack again in the same order to call printf() then we are presumably putting the stack back the way it was before we were called, so the call should work fine. Of course, if the total size of the actual arguments exceeds the total size of the six long integers, then we are probably hosed. Given a more modern system, we'd just use vprintf() instead and be home free.

Variadic Macros

In compilers conforming to the ISO C99 standard, you can define variadic macros, like
   #define debug(fmt, ...) fprintf(stderr, fmt , __VA_ARGS__)
The set of tokens matching the '...' in the macro declaration is substutited in for __VA_ARGS__. Note that invoking this macro as debug("Hello") won't work. Most implementations will require that you do debug("Hello",). The comma must be given, but the argument may be a null string. However, even then the macro would expand to fprintf(stderr,"Hello",) which is not legal because of the extra comma. The work-around in this case is to do
   #define debug(...) fprintf(stderr, __VA_ARGS__)
Since C99 is a fairly new standard don't count on most compilers supporting this. You should probably do some test like
   #if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
before using it. If the compiler isn't ISO C99 compliant, you can just define debug() as a function.

Gnu's gcc compiler has supported variadic macros with a slightly different syntax for a long time (recent versions support both this and the ISO C99 version). They look like:

   #define debug(fmt, args...) fprintf(stderr, fmt , args)
Here args will be replaced by the list of arguments. To solve the comma problem, you can do
   #define debug(fmt, args...) fprintf(stderr, fmt , ## args)
Here the ## causes the preceding group of non-space characters (in older versions of gcc) or the preceding comma (in newer versions) to be deleted if args is empty. For maximum portability, the comma should be surrounded by white space.
Jan Wolter (E-Mail)
Mon Sep 3 10:19:29 EDT 2001 - Original Release.