I've implementated something like this in my testing scripts recently, when messing with standard C, and I got curious about it. This is no homework, its just some hobby of my own.
#include <stdio.h>
typedef struct Struct0 {
int x;
int y;
} Struct0;
typedef struct Struct1 {
char x;
double y;
char z;
} Struct1;
typedef struct Struct2 {
double x;
char y;
int z;
char w;
} Struct2;
void my_test_callback(void *pack) {
// Type cast back to void**
void **params = (void **) pack;
// Unpack data
Struct0* s0 = (Struct0 *) params[0];
Struct1* s1 = (Struct1 *) params[1];
Struct2* s2 = (Struct2 *) params[2];
// Use the data
printf("s0: %i %i\n", s0->x, s0->y);
printf("s1: %c %.2f %c\n", s1->x, s1->y, s1->z);
printf("s2: %.2f %c %i %c\n", s2->x, s2->y, s2->z, s2->w);
}
int main(int argc, char **argv) {
// Imagine we have two or more variables that need to be passed
// to a single function call, as a single parameter.
Struct0 a;
Struct1 b;
Struct2 c;
// Initializing Struct 0
a.x = 1;
a.y = 2;
// Initializing Struct 1
b.x = 'a';
b.y = 3.0;
b.z = 'b';
// Initializing Struct 2
c.x = 4.0;
c.y = 'c';
c.z = 5;
c.w = 'd';
// So, instead of doing something like this (which is the common way):
struct packed {
Struct0 _a;
Struct1 _b;
Struct2 _c;
} pack_data;
pack_data._a = a;
pack_data._b = b;
pack_data._c = c;
// I would like to do something much more generic, such as:
void* generic_pack[3];
generic_pack[0] = (void *) &a; // This way, I don't really need to
generic_pack[1] = (void *) &b; // know the variables types, and I
generic_pack[2] = (void *) &c; // can simply type cast them to void*
// Is it legal to do this? Will it always work?
my_test_callback((void *) generic_pack);
// Output from my console:
// s0: 1 2
// s1: a 3.00 b
// s2: 4.00 c 5 d
return 0;
}
The code compiles with no problems, created and tested on windows 10, gcc.exe (Rev6, Built by MSYS2 project) 12.2.0 , and it works without any segmentation faults.
Will converting gen_pack
(which is a void **
pointer) to a void *
type be always guaranteed to work everytime we run the script? Or did we just got lucky a segmentation fault never occured?
The conversion itself is OK as far as C goes. C allows pretty much any form of wild and crazy pointer conversions - and in case of void*
specifically, they can be carried out without casting. The problems only start to appear when you de-reference the pointer through the wrong type.
generic_pack[0] = (void *) &a;
This is OK and you don't even need the cast.
my_test_callback((void *) generic_pack);
This is fishy but the cast itself is valid. What you are doing here is converting a "decayed" array pointer (decayed from void* [3]
to void**
) into a void*
. And again, you don't need the cast when converting to void*
.
void **params = (void **) pack;
Since the caller coded had array decay, this is ok. But again we don't need the cast.
If the caller code had been (void *) &generic_pack
then it would be wrong and problematic.
Similarly, had you declared the parameter as void* pack[3]
then the parameter would have "decayed" into a void**
and there would be no problems either.
Struct0* s0 = (Struct0 *) params[0];
You de-reference a void**
and perform pointer arithmetic on it. It is pointing at an array of void*
so it is ok. But again, no need to cast.
Other than that, it is fine to cast a member of a void* [3]
array into a struct pointer, since that's what you stored there in the first place.
Note that not declaring the void*
array as a middle step would have been much more problematic in case you decided to point at a struct
array with a void**
. Because only the void*
is the special generic pointer type, it does not apply to void**
.
The summary of this is that void*
are unsafe and dangerous, so it is wise to avoid them. The correct way to implement your code would have been to invent a 4th struct
containing the 3 others:
typedef struct
{
Struct0 s0;
Struct1 s1;
Struct2 s2;
} everything;
void my_test_callback(const everything* pack)
Or alternatively that struct could have contained pointers to structs allocated elsewhere. The best option by far would of course have been to rewrite the function to accept 3 parameters.