ttemple
ttemple

Reputation: 882

Enforce two structs to have same size at compile time?

I have defined two data structures that must remain the same size as each other for the application to function properly. The struct's are used to communicate between a PC and a DSP. The DSP code is in 'C', the PC side in C++.

for example:

struct inbound_data{
    int header[5];
    float val1;
    float val2;
    int trailer[3];
};

struct outbound_data{
    int header[5];
    int reply1;
    int reply2;
    float dat1;
    float dat2;
    int filler[1];
}

later I will do something like:

int tx_block[sizeof(outbound_data)];
int rx_block[sizeof(inbound_data)];

These arrays will be passed to the communication peripherals to transmit and receive between the devices.

Because of how the hardware works, it is essential that the size of the two structs match, so that the buffers are of equal size. This is easy enough to assure with proper care, but occasionally through the design cycle, the data structures get modified. If one is not extremely careful, and aware of the requirement that the structures stay the same size (and be reflected in the PC side code as well), chaos ensues.

I would like to find a compile time way to have the code not build if one of the structures gets modified so that it does not match the size of the other structure.

Is this possible somehow in 'standard' C to check the sizes at compile time and fail if they are different? (I think my compiler is at least C99, maybe not 11).

Upvotes: 13

Views: 1829

Answers (5)

Lundin
Lundin

Reputation: 213892

Enforce two structs have same size at compile time?

There is no standard way to enforce this in C. There are only ways to protect it from happening, such as static_assert - which prevents buggy code from compiling but doesn't solve the actual problem.

In your case there are several problems:

  • Your struct is using the naive default types of C. These aren't portable and can have any size. This can easily be fixed by swapping int for int32_t etc.
  • Endianess might make the code non-portable regardless of integer type. That's a separate issue that I won't address here, but it needs to be considered, especially for exotic DSPs.
  • Any struct can contain padding bytes anywhere, to sate system-specific alignment requirements. The root of the problem being that alignment works differently on different systems. This is the hard one to solve.

The dirty fix to avoid padding is to use static_assert together with some non-standard solution to ensure that the struct has the expected size. Such as #pragma pack(1) or gcc __attribute__ ((__packed__)) etc. These are not standard nor are they portable. Furthermore, skipping padding can be problematic on many systems and you can get issues with misaligned access - padding is there for a reason. So this can potentially create more problems than it solves.

So unfortunately we end up with the realisation that struct is unsuitable for portable code. Particularly for things like data protocol specifications.

If you need truly portable, rugged code, it leaves you with only one option, namely to use a raw data array of uint8_t. In case you need to translate this array into structs, you will have to write serialization/de-serialization code. Which will cost run-time overhead. But there's no other way around it, if you want truly portable structs.

Upvotes: 6

If you must use C99, then I too like Swordfish would suggest a macro. The way to make one which can appear anywhere, and would not introduce any objects for the optimizer to remove, is to put the invalid array in a typedef. So a more general purpose static assertion would look this:

#define CONCAT_(A,B) A##B
#define CONCAT(A,B) CONCAT_(A,B)
#define MY_STATIC_ASSERT(p, msg) typedef char CONCAT(dummy__,__LINE__) [(p) ? 1 : -1]

It's designed to mimic _Static_assert. The message is passed in with the hopes of a compiler diagnostic showing it. An example for its usage is here.

Which produces:

main.cpp:4:54: error: size of array 'dummy__13' is negative
 #define MY_STATIC_ASSERT(p, msg) typedef char CONCAT(dummy__,__LINE__) [(p) ? 1 : -1]
                                                      ^~~~~~~
main.cpp:2:22: note: in definition of macro 'CONCAT_'
 #define CONCAT_(A,B) A##B
                      ^
main.cpp:4:47: note: in expansion of macro 'CONCAT'
 #define MY_STATIC_ASSERT(p, msg) typedef char CONCAT(dummy__,__LINE__) [(p) ? 1 : -1]
                                               ^~~~~~
main.cpp:13:1: note: in expansion of macro 'MY_STATIC_ASSERT'
 MY_STATIC_ASSERT(sizeof(struct foo) == sizeof(struct baz), "Do not match!");

And all the way down there you can see the static assertion with the message.


As an afterthought, you can change dummy__ to please_check_line_ will will produce the more descriptive please_check_line_13 above.

Upvotes: 8

0___________
0___________

Reputation: 67546

you can add padding to equalize the size

struct inbound_data;
struct outbound_data;

struct _inbound_data{
    int header[5];
    float val1;
    float val2;
    int trailer[3];
};

struct _outbound_data{
    int header[5];
    int reply1;
    int reply2;
    float dat1;
    float dat2;
    int filler[1];
};

struct inbound_data{
    int header[5];
    float val1;
    float val2;
    int trailer[3];
    char padding[sizeof(struct _inbound_data) < sizeof(struct _outbound_data) ? sizeof(struct _outbound_data) - sizeof(struct _inbound_data) : 0];
};

struct outbound_data{
    int header[5];
    int reply1;
    int reply2;
    float dat1;
    float dat2;
    int filler[1];
    char padding[sizeof(struct _outbound_data) < sizeof(struct _inbound_data) ? sizeof(struct _inbound_data) - sizeof(struct _outbound_data) : 0];
};

I it can be of course written shorter way without the struct members duplication - but I did it intentionally to show the idea.

struct inbound_data1 __attribute__((packed){
    struct _inbound_data id;
    char padding[sizeof(struct _inbound_data) < sizeof(struct _outbound_data) ? sizeof(struct _outbound_data) - sizeof(struct _inbound_data) : 0];
};

struct outbound_data1 __attribute__((packed){
    struct _outbound_data od;
    char padding[sizeof(struct _outbound_data) < sizeof(struct _inbound_data) ? sizeof(struct _inbound_data) - sizeof(struct _outbound_data) : 0];
};

Upvotes: 1

Swordfish
Swordfish

Reputation: 13134

For C99 you could use something like

#define C_ASSERT(x, y) { int dummy[(x) == (y) ? 1 : -1]; (void*)dummy; }

struct foo {
    int f;
};

struct bar {
    int b1;
    //int b2;
};

int main()
{
    C_ASSERT(sizeof(struct foo), sizeof(struct bar));
}

Upvotes: 3

Vittorio Romeo
Vittorio Romeo

Reputation: 93294

The C11 Standard added a new keyword _Static_assert. You can use it to test a predicate at compile-time, and produce an error if it is false:

_Static_assert(sizeof(outbound_data) == sizeof(inbound_data), "sizes must match");

Upvotes: 7

Related Questions