I highly recommend that you avoid variadic functions and use pointer arrays and variadic macros instead (with a terminator object).
Your function would have looked like this when using this approach:
void printline(const char *str) { printf("%s\n", str); }
int printlines(char **lines) {
if (!lines)
return -1;
while (*lines)
printline(*(lines++));
return 0;
}
#define printlines(...) printlines((char *[]){__VA_ARGS__, NULL})
Not only are variadic functions sometimes difficult to code, but the ABI for variadic functions is problematic to the point that different languages might treat it differently and C bindings between different languages might break your code.
Besides, when using this approach, things can become much more fun and interesting as well, allowing for easy type detection and multi-type arguments... this code from the facil.io CSTL library provides a good example for what I mean.
The function accepts an array of structs:
/** An information type for reporting the string's state. */
typedef struct fio_str_info_s {
/** The string's length, if any. */
size_t len;
/** The string's buffer (pointer to first byte) or NULL on error. */
char *buf;
/** The buffer's capacity. Zero (0) indicates the buffer is read-only. */
size_t capa;
} fio_str_info_s;
/** memory reallocation callback. */
typedef int (*fio_string_realloc_fn)(fio_str_info_s *dest, size_t len);
/** !!!Argument type used by fio_string_write2!!! */
typedef struct {
size_t klass; /* type detection */
union {. /* supported types */
struct {
size_t len;
const char *buf;
} str;
double f;
int64_t i;
uint64_t u;
} info;
} fio_string_write_s;
int fio_string_write2(fio_str_info_s *restrict dest,
fio_string_realloc_fn reallocate, /* nullable */
const fio_string_write_s srcs[]);
Then a macro makes sure the array's last element is a terminator element:
/* Helper macro for fio_string_write2 */
#define fio_string_write2(dest, reallocate, ...) \
fio_string_write2((dest), \
(reallocate), \
(fio_string_write_s[]){__VA_ARGS__, {0}})
Additional helper macros were provided to make the fio_string_write_s
structs easier to construct. i.e.:
/** A macro to add a String with known length to `fio_string_write2`. */
#define FIO_STRING_WRITE_STR2(str_, len_) \
((fio_string_write_s){.klass = 1, .info.str = {.len = (len_), .buf = (str_)}})
/** A macro to add a signed number to `fio_string_write2`. */
#define FIO_STRING_WRITE_NUM(num) \
((fio_string_write_s){.klass = 2, .info.i = (int64_t)(num)})
And the function used the terminator element to detect the number of arguments received by the macro:
int fio_string_write2 (fio_str_info_s *restrict dest,
fio_string_realloc_fn reallocate, /* nullable */
const fio_string_write_s srcs[]) {
int r = 0;
const fio_string_write_s *pos = srcs;
size_t len = 0;
while (pos->klass) {
switch (pos->klass) { /* ... */ }
/* ... counts total length */
++pos;
}
/* ... allocates memory, if required and possible ... */
pos = srcs;
while (pos->klass) {
switch (pos->klass) { /* ... */ }
/* ... prints data to string ... */
++pos;
}
/* ... house-keeping + return error value ... */
}
Example use (from the source code comments):
fio_str_info_s str = {0};
fio_string_write2(&str, my_reallocate,
FIO_STRING_WRITE_STR1("The answer is: "),
FIO_STRING_WRITE_NUM(42),
FIO_STRING_WRITE_STR2("(0x", 3),
FIO_STRING_WRITE_HEX(42),
FIO_STRING_WRITE_STR2(")", 1));
This both simplifies the code and circumvents a lot of the issues with variadic functions. This also allows C bindings from other languages to work better and the struct array to be constructed in a way that is more idiomatic for the specific target.