There are multiple questions into one here:
Casting from Non-Variadic to Variadic
((void (*)(const char *, ...))memcpy)(ptr1, ptr2, 10); // Assuming ptr1 and ptr2 are valid, still invokes undefined behavior
memcpy
's signature is void *memcpy(void*, const void*, size_t)
. This means that as first and second arguments are only valid pointers and as third a size_t
.
Meanwhile the type void (*)(const char*, ...)
allows to pass any kind of objects past the first argument.
No matter how you look at it, this should not work. What is variadic_memcpy(p1, p2, 42.5, bigStruct, gmtime(&spec.tv_sec));
supposed to do?
And indeed, the System V ABI for AMD64 processors specify that different object types are passed in different locations:
- INTEGER objects are allocated in, in order, RDI, RSI, RDX, RCX, R8, and R9,
- SSE objects (such as floats) are allocated from XMM0 to XMM7
- MEMORY and X87 objects, as well as all others when no more of the previously mentioned registers are available, are allocated on the stack.
Those are essentially three different stacks, so you cannot determine the arguments order just from looking at them. (printf
's format argument itself specifies the arguments' types, so the routine actually iterates through registers and memory independently).
Furthermore, AL is reserved by routines calling variadic routines to specify an upper bound of vector registers used by passed SSE arguments.
Implementation-wise, on SysV-compliant systems, the target routine will look for arguments where you did not put them. And the ABI says it's your fault.
And theory-wise, such a use of variadic functions is exactly the same as passing the wrong argument type to parameters with compiler checks disabled - also called Dynamic Typing - and in the case of C, with dynamic checks disabled too, for added fun.
Casting from Variadic to Non-Variadic
A variadic function is not statically checked, which allows you to cast it to a non-variadic one by simply translating from a checked ABI to an unchecked one.
Eg. Casting printf
to int (*)(const char*)
can be achieved by generating a routine that forwards all arguments, doing an ABI translation if necessary (eg. register to stack). If the translation is wrong, C says that it's on you anyway.
Such translation in the opposite direction is not possible. For all you know, a variadic call will pass less arguments than a target routine expects, and a compiler would be supposed to accept such nonsensical code.
C Variadic ABIs
i386
Compilers for i386 essentially do whatever they want, and most follow a pretty simple pattern: everything passed to ...
, and the argument preceding it, are allocated on the stack.
This means finding the first variadic argument is a matter of ¶m + sizeof(param)
.
Fun fact: this design is why C requires variadic routines to have at least one non-variadic parameter, and why the va_start
macro takes that parameter as second argument.
And how to know how many arguments are passed you ask? Ha! As if compilers care. lol you're the programmer, you're supposed to know how computers work, so use your brain and figure it out yourself.
This is not a joke. This is what C compilers tell you to do.
And remember the C Standard says you are in charge and responsible of everything.
So how one typically writes a variadic routine is by having a parameter that specifies how many variadic arguments there are and what their types are, and by gently asking that routine's user to get everything right.
printf
for instance deduces both arguments' count and types from the format string's specifiers.
[As mentioned by @TobySpeight's comment: Another approach is by requiring a terminating sentinel value, as is used by execl
and XtVaSetValues
.]
Naturally, this is incompatible with both UNIX's and Win32's ABIs (_cdecl
, _stdcall
and friends), which do pass things in registers.
System V for AMD64
SysV actually works similarly to the i386 approach described above, but with the added benefit of being one standardised ABI.
The main difference is that a variadic function sees some arguments passed in registers, which requires it to save all the relevant registers on the stack, with the following exceptions:
- RAX should indicate how many XMM registers are used by the caller,
- and only those registers should be saved (ie. if
RAX
is 2, then XMM0
and XMM1
are to be saved).
Note that RAX is reserved both for this use and to be used by returning routines, meaning it does not actually constitute a difference between variadic and non-variadic routines.
Then that function must iterate through each of the used stacks as described earlier. This means va_list
is not just a pointer, but a struct containing multiple pointers which are independently incremented.
For example, va_arg(list, int)
looks first in RDI's stack copy, then RSI's copy, etc.., while va_arg(list, float)
looks first in XMM0's copy then XMM1's copy, etc..
This is actually trivial.
Here is va_list
:
typedef struct {
unsigned int gp_offset;
///The element holds the offset in bytes from reg_save_area
///to the place where the next available general purpose
///argument register is saved.
///In case all argument registers have been exhausted, it is
///set to the value 48 (6 ∗ 8).
unsigned int fp_offset;
///The element holds the offset in bytes from reg_save_area
///to the place where the next available floating point
///argument register is saved.
///In case all argument registers have been exhausted, it is
///set to the value 304 (6 ∗ 8 + 16 ∗ 16).
void *overflow_arg_area;
///This pointer is used to fetch arguments passed on the stack.
///It is initialized with the address of the first argument
///passed on the stack, if any, and then always updated to
///point to the start of the next argument on the stack.
void *reg_save_area;
///The element points to the start of the register save area.
} va_list[1];
Where the register save area is the memory where the registers were saved.
Here is va_arg
's algorithm:
- Determine whether
type
may be passed in the registers. If not go to step 7.
- Compute
num_gp
to hold the number of general purpose registers needed to pass type
and num_fp
to hold the number of floating point
registers needed.
- Verify whether arguments fit into registers. In the case:
l->gp_offset > 48 − num_gp ∗ 8
or
l->fp_offset > 304 − num_fp ∗ 16
go to step 7.
- Fetch type from
l->reg_save_area
with an offset of l->gp_offset
and/or l->fp_offset
. This may require copying to a temporary loca-
tion in case the parameter is passed in different register classes or
requires an alignment greater than 8 for general purpose registers and
16 for XMM registers.
- Set:
l->gp_offset = l->gp_offset + num_gp ∗ 8
l->fp_offset = l->fp_offset + num_fp ∗ 16
.
- Return the fetched
type
.
- Align
l->overflow_arg_area
upwards to a 16 byte boundary if alignment needed by type
exceeds 8 byte boundary.
- Fetch type from
l->overflow_arg_area
.
- Set
l->overflow_arg_area
to:
l->overflow_arg_area + sizeof(type)
- Align
l->overflow_arg_area
upwards to an 8 byte boundary.
- Return the fetched
type
.
From §3.5.7.The va_arg macro.
What this all means is that System V guarantees variadic routines to be ABI-compatible with non-variadic routines on AMD64!
And this all happens without passing extra parameters! va_list
is computed on demand after dumping registers on the stack.
Now however, how to figure out how many arguments there are and what their types are still is up to the user.
Win32 & Win64
TODO: Get interest in Windows.
D Variadic ABIs
D, unlike C, has two type-safe variadic calling conventions.
D-Style Variadic Functions
This is a simple dynamically-typed implementation:
- the variadic arguments are allocated on the stack, and
- a variadic function has as first parameter a compiler-supplied
TypeInfo[] _arguments
object allocated on the stack, which is a slice object with a .length
property and containing pointers to the TypeInfo
instances of each respective argument.
As implementations must supply _arguments
, a variadic function then can take its address and compute the address of each argument, as well as dynamically typecheck each one.
Typesafe Variadic Functions
Another approach that D uses is that of requiring all variadic arguments to be of the same type, allowing implementations to pack them in stack-allocated arrays, and to just pass that.
In fact D goes further in that a void f(int[] xs...)
function can be used both with n-arity (f(42, 1917, 1922)
) or as expecting an array directly (f([42, 1917, 1922])
), and then generalises the idea by allowing the n-arity use to pass arguments to class or struct constructors (eg. calling f(LinkedList!string xs...)
as f("h", "Ü")
is the same as f(new LinkedList!string("h", "Ü"))
).
Cheating with Class Hierarchies
In fact we can also generalise such statically-type-safe variadic functions as described above by using class hierarchies.
If everything descends from Object
, then a variadic function void f(Object[] xs...)
accepts arguments of any type to be matched over by the callee, by simply passing an array, using the same ABI as non-variadic functions.
Add in either autoboxing or some way to dynamically and seamlessly differentiate reference from value types, such as pointer tagging, and all types are covered by the single ABI.