3

If one wants to write a function in C that passes a variable argument list through to printf one has to use the vprintf version. How can I implement this mechanism for a custom function?

In other words, how is the essence of what sets vprintf apart from printf implemented in standard conforming C?

levzettelin
  • 2,600
  • 19
  • 32

3 Answers3

20

If you want to write a function which takes a va_list as an argument, the way vprintf does, then you just do that. You can extract arguments from the va_list with va_arg in the normal way.

Don't call va_start or va_end on the va_list: that's the responsibility of the caller. Since you can't restart the va_list in the normal way, if you need to scan it more than once, you'll need to va_copy it.

Here's a quick example, just for illustration (i.e. it's not meant to be the best possible implementation).

These two functions just join a bunch of strings using a provided delimiter string. The first one is the "v" version (like vsprintf), which implements the logic. The second one is the varargs version which packages up the va_list and passes it to the implementation.

The inner function runs through the arguments twice; the first time it adds the sizes of the strings. Both functions return a newly-malloc'd string which will need to be free'd by the caller.

The argument list must be terminated with a NULL.

char* vjoin(const char* delim, va_list ap) {
  va_list aq;
  va_copy(aq, ap);
  size_t dlen = strlen(delim);

  /* First pass. Use the copied va_list */
  size_t needed = 1; /* NUL terminator */
  const char* s = va_arg(aq, const char*);
  if (s) {
    needed += strlen(s);
    while ((s = va_arg(aq, const char*)))
      needed += dlen + strlen(s);
  }
  va_end(aq);

  /* Second pass. Use the original va_list */
  char* rv = malloc(needed);
  size_t offset = 0;
  *rv = 0;
  s = va_arg(ap, const char*);
  if (s) {
    strcpy(rv, s);
    offset = strlen(s);
    while ((s = va_arg(ap, const char*))) {
      strcpy(rv + offset, delim);
      strcpy(rv + offset + dlen, s);
      offset += dlen + strlen(s);
    }
  }
  return rv;
}

char* join(const char* delim, ...) {
  va_list ap;
  va_start(ap, delim);
  char* rv = vjoin(delim, ap);
  va_end(ap);
  return rv;
}
rici
  • 234,347
  • 28
  • 237
  • 341
  • I like it, it's the only posts i found which demonistration how to write a function with va_list argument. – tangxinfa Jul 20 '18 at 11:42
  • I thought this was great as first, too - this might be asking for undefined behavior (UB) on 64 bit systems/any systems for which `va_list` is an array type. In this case, before any call of `vjoin`, C99 tells us that `ap` will be converted into a `typeof(*ap) *`. `va_copy` expects a `va_list` argument, i.e. an array type. Why does this matter here? On function entry, machine code will cause a conversion for each `va_list` argument (i.e. `va_list` (which is an array type) into `typeof(*ap) *`). This conversion yields UB for `ap` inside `vjoin`, since `ap` is `typeof(*ap) *`, already. – polynomial_donut Jul 25 '18 at 20:08
  • One might be inclined to quickly say "But `vprintf` takes a `va_list` argument, and how else would it work then?". If you try to look into, say the `gcc` implementation though, you will see that `vprintf` isn't written as a C function - it's probably assembler - and is tied in into the entire compiler implementation. – polynomial_donut Jul 25 '18 at 20:11
  • @polynomial_donut: As far as I know, `vprintf` isn't in `gcc` at all; it's part of `glibc` and there it is definitely written in C: https://sourceware.org/git/?p=glibc.git;a=blob;f=stdio-common/vfprintf.c;h=ae412e4b8444aea22a691d9f62ecc07cf02a3389;hb=HEAD . The gcc distro includes a version of vprintf in libiberty, but that's also written in C and I don't think it is normally used. If you have some other information, I'd love to see it. – rici Jul 25 '18 at 20:35
  • Ah I was wrong there :) But I also doubt the code breaks down into something like the example you gave here, at least not on x86-64. About `va_list` being an array type: [see here] (https://stackoverflow.com/a/8048892/2956948) [or here](https://stackoverflow.com/a/4958507/2956948). UB is already possible whenever you call `va_copy` inside `vjoin`, because C99 tells us that on entry into `vjoin`, `ap` is converted into a type `typeof(*ap) *`. Now, any function inside `va_copy` that expects a `va_list` argument and gets `ap` instead, will apply said conversion to `ap` -> UB. – polynomial_donut Jul 25 '18 at 21:11
  • @polynomial: I think you are missing the fact that C makes it impossible for a function to have an argument which is an array type. See 6.7.6.3 para 7: "A declaration of a parameter as `array of type’` shall be adjusted to `qualified pointer to type`." That precisely matches the decay of the argument array to a pointer to the first element. The fact that a typedef is used changes nothing. Your first link is not relevant to my code since I don't attempt to pass by reference. – rici Jul 25 '18 at 21:47
  • 1
    @polynomial: as for your doubt, perhaps it would be assauged by an inspection of the actual implementation of `fprintf` in terms of `vfprintf` in glibc: https://sourceware.org/git/?p=glibc.git;a=blob;f=stdio-common/fprintf.c;h=2bbf14bf5d177881d0437ef5bb91aded63bcb98d;hb=HEAD Anyway, the C standard makes it pretty clear that the usage in my example is legit: "If access to the varying arguments is desired, the called function shall declare an object (generally referred to as ap in this subclause) having type va_list. The object ap may be passed as an argument to another function;" (7.16p). – rici Jul 25 '18 at 21:55
2

The big difference is, of course, that printf takes a variable number of optional arguments after the format string, while vprintf takes a single non-optional argument that "points" to all arguments.

Most implementations of printf creates this single data structure and in turn calls vprintf.

Read any tutorial about variable number of arguments (also known as variadic arguments) to see how it works. This printf (and family) reference might also come in handy, as well as this variable arguments reference (which has an example).

Some programmer dude
  • 400,186
  • 35
  • 402
  • 621
1

The trick is to realize that vprintf (or more likely vfprintf) is the underlying core function that's implemented. printf is likely to be just a wrapper that calls va_start, vprintf, then va_end.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711