tags:

views:

81

answers:

6

Hello Folks!

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?

+1  A: 

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 byte (or sometimes even 128 byte) boundary. That is, for the pointer value p, p % 64 (p modulus 64) 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.)

JosephH
Hmm ... passing the va_list to a special callback-function would be an option. Then, I have to declare it asva_list get_value( va_list l, char* buf )where buf is the memory where to write/copy the data to.But why does my second approach not work - copying byte-per-byte into a memory buffer?
Jan
I've updated my answer to try and explain why byte-per-byte doesn't work.
JosephH
+1  A: 

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.

Digikata
How would you go about serializing the data definition?
Shiftbit
I want the data object creation as simple as possible:OBJ_DESC obj_desc[] ={ sizeof( int ), sizeof( char* ), sizeof( double ), sizeof( int* )};ander later I create a object withcreate( obj_desc, 1, "Hello World", 566.2, everything behind obj_desc is taken from obj_desc itself. Creating an array of parameters first would be too much overhead.
Jan
I'm guessing that the reason you have a data defined type system is that creation/manipulation needs to be data-driven - otherwise why not use plain structs. So, how often are you going to be creating objects in literal code vs whatever data-driven method the system will use?
Digikata
To serialize the data def: Unless my usage was very simple, I'd probably look over the many available formats and choose one that fits my needs.
Digikata
@Digikata, not to sound thick, but what formats are available?
Shiftbit
Google Protocol Buffers, YAML, ASN.1, and Boost serialization come to mind... ah here, there's even a StackOverflow on it http://stackoverflow.com/questions/321619/c-serialization-performance
Digikata
+1  A: 

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.).

R..
So the va_list-modification callback is my only option to get a user-specific value?
Jan
+1  A: 

A non scientific test.

  • AIX 5.3 with GCC 4.2 - works
  • HP-UX 11.23 with aCC 5.56 - doesn't
  • Linux (SUSE 10.2) with GCC 4.1 - doesn't
  • Solaris 10 with CC 5.9 - doesn't

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.

Dummy00001
Nice testing! I was surprised to see it even failed under linux gcc. Was that running on a 64 bit system?
JosephH
@JosephH: All test were 32bit, except Linux. IOW, the default compile mode. Actually I first tested on AIX (because I already had a terminal open) and thought wow it really works. Only few moments later to find that well AIX is an outlier even in this test...
Dummy00001
A: 

May I suggest replacing variable arguments with an array of void pointers?

Seva Alekseyev
well, but this is another solution with too much overhead (not that I already thought about it). I only want to pass my parameters to one function call, not always declare an array to pass for each call. The use of the function should be as simple as possible.
Jan
A: 

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:

  • it places the number of arguments to the first parameter of myRealFunc
  • it creates a temporary array (compound literal) of the correct size and initializes it with the arguments (all cast to uintmax_t)
  • if 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 once

Now 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.

Jens Gustedt