next up previous
Next: About this document ...

Writing a log() function using stdarg.h macros

CIS307 Fall 2002

Current and future assignments require you to log output to a file. For the current assignment, much of your log output will look like:

Server 3 has completed work on job 761 at time 4734.
How would we display this? We might use a line of code like:
  fprintf(log_file, "Server %d has completed work on job %d at time %d.\n",
          server_number, j->job_number, current_time);

This is a viable solution, and there are many other viable solutions, some better than others. The best solution would be to have a log() function--then we no longer have to drag log_file around with us. In fact, we can hide it from all functions which don't need it by declaring it in log.c and not exporting it. So what does our log() function look like?

  void log(char *s);
would work. We no longer have to carry a file pointer around, but there is a problem--in order to get the desired format in our output, like above, we need a character array in which we place the appropriate string with a sprintf() call, and which is then passed to log(). This is still not a great solution, and no more concise than the above fprintf() with a well named file pointer.

What we really want is something that looks like printf(), to which we may pass a format string and arguments, and it will do the formatting and send the output to the log file. Call the function something useful, like log(), and we have a concise, readable solution with no extraneous preparatory code.

The prototype for printf() is

  int printf(const char *format, ...);
The ellipsis in this prototype in the C syntax for variable arguments, and if printf(), fprintf(), sprintf(), etc. can be written to take variable arguments, then there is no reason we cannot write our own such function:
  int log(char *fmt, ...);
Notice that in the prototypes for both printf() and log() the ellipsis is last--this must always be the case. Looking at the man page for printf() we see that the number of characters printed are returned, so we declare our log() function to return int, and we will follow this convention for output functions.

ANSI/IOS C defines an interface for variable argument functions in the standard header stdarg.h. By including this file, we gain access to the macros va_start, va_end, va_arg, and the type va_list. The standard also defines another macro va_copy which copies a va_arg src to a va_arg dest, but we will not be using it here.

The defined macros work on a va_list, by convention called ap for argument pointer.

The macro va_start takes two parameters: The first is a va_list, and the second is last, the last parameter before the variable argument list. For printf(), last is format, and for log(), it's fmt. va_start initializes the va_list argument, and so must be called before any other macro can use the va_list.

The va_arg macro takes the initialized va_list and a type, and returns an object of the specified type, and of the value of the item at the front of the list. It also advances the pointer to the next item in the list, so subsequent calls to va_arg with return subsequent items in the va_list. We can use the format string to learn the type of each argument, then a switch statement to assure we make calls to va_arg correctly. As variable arguments are accessed at run time, there is no compile time type checking. Try playing around with printf() with incorrectly corresponding format specifiers--you'll get some very odd output. It is even possible to read type specifiers on input, and then use them to print output!

The va_end macro takes the initialized va_list as it's argument, and does any and all necessary cleanup when we are finished processing the variable arguments. It is important to always call this macro as some implementations may, for example, use dynamic memory which needs returned to the system.

Below are files log.c, log.h, main.c, implementation, specification, and driver respectively for the log() function discussed above, as well as a makefile to build them, and the output of the driver program. There is also a link on my web page to download an archive of these files. You may use this code verbatim in your assignments, but I would like you to explore it a bit--play with it and understand it.

The concepts discussed here are not applicable only to output. I might be impressed with anybody who applies this information to an input function to read an initialization file. A variable arguments function would also be useful in a function to send or receive strings over a network. Furthermore, use of variable argument functions is not limited to I/O. Where else might the concept be useful?

Makefile:

CC = gcc
CFLAGS = -g -Wall

log_test: main.o log.o
        $(CC) -o log_test main.o log.o

main.o: main.c log.h
        $(CC) $(CFLAGS) -c main.c

log.o: log.c log.h
        $(CC) $(CFLAGS) -c log.c

clean:
        rm -f *.o log_test test.log

log.h:

#ifndef LOG_H
# define LOG_H

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

void init_log(char *file, char *mode);
int log(char *fmt, ...);

#endif

log.c:

#include "log.h"

#include <errno.h>
#include <stdlib.h>
#include <ctype.h>

#define STRING_LENGTH 240                    /* something 'safe'             */

FILE *log_file;

void init_log(char *file, char *mode)
{
  char error[STRING_LENGTH];

  if (!(log_file = fopen(file, mode))) {     /* open file for mode           */
    sprintf(error, "init_log() failed to open %s.\n", file);
    perror(error);
    exit(666);                               /* message and exit on fail     */
  }
}

int log(char *fmt, ...)
{
  va_list ap;                                /* special type for variable    */
  char format[STRING_LENGTH];                /* argument lists               */
  int count = 0;
  int i, j;                                  /* Need all these to store      */
  char c;                                    /* values below in switch       */
  double d;
  unsigned u;
  char *s;
  void *v;

  va_start(ap, fmt);                         /* must be called before work   */
  while (*fmt) {
    for (j = 0; fmt[j] && fmt[j] != '%'; j++)
      format[j] = fmt[j];                    /* not a format string          */
    if (j) {
      format[j] = '\0';
      count += fprintf(log_file, format);    /* log it verbatim              */
      fmt += j;
    } else {
      for (j = 0; !isalpha(fmt[j]); j++) {   /* find end of format specifier */
        format[j] = fmt[j];
        if (j && fmt[j] == '%')              /* special case printing '%'    */
          break;
      }
      format[j] = fmt[j];                    /* finish writing specifier     */
      format[j + 1] = '\0';                  /* don't forget NULL terminator */
      fmt += j + 1;
      
      switch (format[j]) {                   /* cases for all specifiers     */
      case 'd':
      case 'i':                              /* many use identical actions   */
        i = va_arg(ap, int);                 /* process the argument         */
        count += fprintf(log_file, format, i); /* and log it                 */
        break;
      case 'o':
      case 'x':
      case 'X':
      case 'u':
        u = va_arg(ap, unsigned);
        count += fprintf(log_file, format, u);
        break;
      case 'c':
        c = (char) va_arg(ap, int);          /* must cast!                   */
        count += fprintf(log_file, format, c);
        break;
      case 's':
        s = va_arg(ap, char *);
        count += fprintf(log_file, format, s);
        break;
      case 'f':
      case 'e':
      case 'E':
      case 'g':
      case 'G':
        d = va_arg(ap, double);
        count += fprintf(log_file, format, d);
        break;
      case 'p':
        v = va_arg(ap, void *);
        count += fprintf(log_file, format, v);
        break;
      case 'n':
        count += fprintf(log_file, "%d", count);
        break;
      case '%':
        count += fprintf(log_file, "%%");
        break;
      default:
        fprintf(stderr, "Invalid format specifier in log().\n");
      }
    }
  }
  
  va_end(ap);                                /* clean up                     */

  return count;
}

main.c:

#include "log.h"

#define LOG_FILE "test.log"

int main(int argc, char **argv)
{
  init_log(LOG_FILE, "w");

  log("This is a test\n");
  log("This is the %dnd test.\n", 2);
  log("The two smallest primes are %d and %i\n", 2, 3);
  log("The speed of light in a vacuum in octal: %c = %om/s\n", 'c', 299792458);
  log("This is all logged to %s\n", LOG_FILE);
  log("Of course, we can use further formatting--pi is close to %2.2f\n", 3.14159);
  log("Avogadro's number is %e, or if you prefer %G\n", 6.02257e23, 6.02257e23);
  log("This function resides at %p\n", log);
  log("This symbol starts format specifiers: %%\n");
  log("And the %%n specifier is tough to fit into a sentence. %n\n");

  return 0;
}

output:

This is a test
This is the 2nd test.
The two smallest primes are 2 and 3
The speed of light in a vacuum in octal: c = 2167474112m/s
This is all logged to test.log
Of course, we can use further formatting--pi is close to 3.14
Avogadro's number is 6.022570e+23, or if you prefer 6.02257E+23
This function resides at 0x8048700
This symbol starts format specifiers: %
And the %n specifier is tough to fit into a sentence. 54




next up previous
Next: About this document ...
Jeremy W. Sheaffer 2002-09-16