Reputation: 51
I'm having a C programming question: I want to write a function with variable argument lists, where the specific types of each argument is not know - only its size in bytes. That means, if I want to get an int-Parameter, I (somewhere before) define: There will be a parameter with sizeof( int ), that is handled with a callback-function xyz.
My variable argument function should now collect all information from its call, the real data-type specific operations (which also can be user-defined data types) are processed only via callback-functions.
At the standard va_arg-functions, it is not possible to say "get me a value of X bytes from my parameter-list", so I thought to do it this way. My data-type is double in this case, but it can be any other number of bytes (and even variable ones).
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
int fn( int anz, ... )
{
char* p;
int i;
int j;
va_list args;
char* val;
char* valp;
int size = sizeof( double );
va_start( args, anz );
val = malloc( size );
for( i = 0; i < anz; i++ )
{
memcpy( val, args, size );
args += size;
printf( "%lf\n", *( (double*)val ) );
}
va_end( args );
}
int main()
{
fn( 1, (double)234.2 );
fn( 3, (double)1234.567, (double)8910.111213, (double)1415.161718 );
return 0;
}
It works for me, under Linux (gcc). But my question is now: Is this really portable? Or will it fail under other systems and compilers?
My alternative approach was to replace
memcpy( val, args, size );
args += size;
with
for( j = 0; j < size; j++ )
val[j] = va_arg( args, char );
but then, my values went wrong.
Any ideas or help on this?
Upvotes: 5
Views: 9129
Reputation: 37495
It is not portable, sorry. The format of va_list is compiler/platform dependent.
You have to use va_arg() to access va_list, and you must pass the correct type of the argument to va_list.
However, I believe it's possible that if you pass a type of the correct size to va_arg, that would work. ie. the type is not usually relevant, only it's size. However, even this is not guaranteed to work across all systems.
I think I'd suggest relooking at your design and seeing if you can find an alternative design - are there more details on why you are trying to do this that you can share? Can you pass the va_list to the callbacks instead?
Update
The reason the byte-by-byte approach doesn't work is probably quite involved. As far as the C standard goes, the reason it doesn't work is because it's not allowed - you can only use va_arg to access the identical types that were passed to the function.
But I suspect you'd like to know what's going on behind the scenes :)
The first reason is that when you read pass a "char" to a function, it's actually automatically promoted to an int, so is stored into the va_arg as an int. So when you read a char, you're reading an "int"s worth of memory, not a "char"s - so you're not actually reading a byte at a time.
A further reason has to do with alignment - on some architectures (one example would be very recent ARM processors), a "double" must be aligned to a 64 bit (or sometimes even 128 bit) boundary. That is, for the pointer value p, p % 16 (p modulus 16, in bytes - ie. 128 bit) must equal 0. So when these are packed on the va_arg, the compiler will probably be ensuring that any double values have space (padding) added so they only occur with the correct alignment - but you're not taking acount of it when you read the entries a byte at a time.
(There may be other reasons too - I'm not intimately familiar with the inner works of va_arg.)
Upvotes: 1
Reputation: 78903
For C99, if I may assume that all your arguments are integer types, but may just be of different width or signedness you can get away with a macro. By that you may even transform your argument list into an array without pain for the user that calls this.
#define myFunc(...) myRealFunc(NARGS(__VA_ARGS__), (uintmax_t const[]){ __VA_ARGS__})
where
void myRealFunc(size_t len, uintmax_t const param*);
and where NARGS
is a macro that gives you the length of the __VA_ARGS__
parameter. Such a thing then can be called just like a function that would receive va_list
.
To explain a bit what the macro does:
myRealFunc
uintmax_t
)NARGS
is done correctly, this
evaluates your argument list only at
preprocessing time, as tokens, and
not at run time. So your parameters
are not run-time evaluated more than
onceNow your callback functions would be called by myRealFunc
by using whatever magic you would like to place there. Since when called they'd need a parameter of a different integer type, the uintmax_t
parameter param[i]
would be cast back into that type.
Upvotes: 0
Reputation: 61341
May I suggest replacing variable arguments with an array of void pointers?
Upvotes: 1
Reputation: 17420
A non scientific test.
All Linux, Solaris and HP-UX complained about the args += size;
line.
Otherwise, it is quite obvious that va_arg()
was included for a reason. E.g. on SPARCs stack is used completely differently.
Upvotes: 1
Reputation: 215193
Performing arithmetic on a va_list
is on the extreme end of nonportable. You should use va_arg
normally with a type of the same size as the argument and it will probably work anywhere. For the sake of being "closer to portable" you should use unsigned integer types for this purpose (uint32_t
etc.).
Upvotes: 1
Reputation: 1959
In this case avoiding va_args would likely be cleaner because you still end up bound to a specific number of arguments in code at the calling point. I'd go with passing arrays of arguments.
struct arg
{
void* vptr;
size_t len;
};
void fn( struct arg* args, int nargs );
For that matter, I'd also carry the data definition too, either an int as mentioned earlier in the comments or a pointer to a struct if it's a more complex data def.
Upvotes: 0