Okay, I figured out a workaround:
#define ASSERT_LIST_TYPE(t, l...) do{t _t[] = {l};}while(0)
void _Log(char* what, ...);
#define Log(what, args...) do{\
ASSERT_LIST_TYPE(char*, args);\
_Log(what, args);\
}while(0)
That at least generates a warning if the args aren't of the right type, because the dummy array initialization isn't of the right type.
I plan on #ifdef'ing the ASSERT_LIST_TYPE out if this isn't a debug build just in case..
** Edit **
Based on the feedback in here, I changed the code to look like this:
void _Log(char* what, const char** args, size_t args_len);
#define Log(what, args...) do{\
const char* a[] = {args};\
_Log(what, a, (sizeof a)/sizeof(char*));
}while(0)
This appears to work even if args is empty, and it catches somebody passing an obj-c literal string instead (@"..." instead of "...") which is what bit me before.