if you look in the libc source this is the implementation for printf:
int
__printf (const char *format, ...)
{
va_list arg;
int done;
va_start (arg, format);
done = vfprintf (stdout, format, arg);
va_end (arg);
return done;
}
that doesn't tell you much... it is a variadic function taking at least one argument, the format... it makes a va_list with va_start and hands it to vfprintf which does the hard work.
it does this so it can share that hard work between all of the printf stream functions, but here is vfprintf in a source version of libc from git://sourceware.org/git/glibc.git, but the file is pretty hard to grok, because it is implemented with lots of macros.
but you can write you own variadic by scanning a format (here we wont scan a format but just trust an argument number passed in) function and acting on the args
#include <stdio.h>
#include <stdarg.h>
void printNStrings( int n, ...)
{
va_list l;
va_start(l,n);
for (int i=0; i< n; i++)
{
char * arg = va_arg(l, char *);
puts(arg);
}
va_end(l);
}
int main(void)
{
printNStrings(2,"hello", "world");
printNStrings(3, "how", "are", "you");
return 0;
}
It should be noted that this is a dangerous proposition... because va_arg, just pulls an arg off of the stack and doesn't have any state that can store information about bounds so: printNStrings(4, "how", "are", "you"); gets into stack overflow and UB territory quickly. The compiler's themselves have bounds checking for printf family functions... and you can invoke them on popular compilers with a declaration like: void mylogger(const char *format, ...) __attribute__((format(printf, 1, 2))); which will turn on warnings for the types passed into your logger.
I am still looking for the actual implimentation of va_list in glibc to use as a reference.