ccastingvoid-pointerspack

Is it legal to type cast a void** to a void*?


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?


Solution

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