Reputation: 1011
The program - some sort of old-school network messaging:
// Common header for all network messages.
struct __attribute__((packed)) MsgHeader {
uint32_t msgType;
};
// One of network messages.
struct __attribute__((packed)) Msg1 {
MsgHeader header;
uint32_t field1;
};
// Network receive buffer.
uint8_t rxBuffer[MAX_MSG_SIZE];
// Receive handler. The received message is already in the rxBuffer.
void onRxMessage() {
// Detect message type
if ( ((const MsgHeader*)rxBuffer)->msgType == MESSAGE1 ) { // Breaks strict-aliasing!
// Process Msg1 message.
const Msg1* msg1 = (const Msg1*)rxBuffer;
if ( msg1->field1 == 0 ) { // Breaks strict-aliasing!
// Some code here;
}
return;
}
// Process other message types.
}
This code violates strict-aliasing in modern GCC (and falls down to unspecified behaviour in modern C++). What is the correct way to solve the problem (to make the code that doesn't throw the "strict-aliasing" warning)?
P.S. If rxBuffer is defined as:
union __attribute__((packed)) {
uint8_t[MAX_MSG_SIZE] rawData;
} rxBuffer;
and then I cast &rxBuffer to other pointers it doesn't cause any warnings. But is it safe, right and portable way?
Upvotes: 7
Views: 1757
Reputation: 27133
It boils down to this:
((const MsgHeader*)rxBuffer)->msgType
rxBuffer
is of one type, but we wish to treat is as if it was of another type. I suggest the following "alias-cast":
const MsgHeader * msg_header_p = (const MsgHeader *) rxBuffer;
memmove(msg_header_p, rxBuffer, sizeof(MsgHeader));
auto msg_type = msg_header_p -> msgType;
memmove
(like its less flexible cousin memcpy
) effectively says that the bit pattern that was available at the source (rxBuffer
) will, after the call to memmove
be available at the destination (msg_header_p
). Even if the types are different.
You might argue that memmove
does "nothing", because the source and destination are identical. But that's exactly the point. Logically, it serves the purpose of making msg_header_p
an alias for rxBuffer
, even though in practice a good compiler will optimize it out.
(This answer is potentially a bit controversial. I may be pushing memmove
too far. I guess my logic is: First, memcpy
to a new location is clearly acceptable to answer this question; second, memmove
is just a better, more general (but maybe slower), version of memcpy
; third, if memcpy
allows you to look at the same bit pattern via a different type, when why shouldn't memmove
allow the same idea to "change" the type of a particular bit pattern? If we memcpy
to a temporary area, then memcpy
back to the original position, would be OK also? )
If you want to build a full answer out of this, you'll need to alias-cast back again at some point, memmove(rxBuffer, msg_header_p, sizeof(MsgHeader));
, but I guess I should await feedback on my "alias cast" first!
Upvotes: 0
Reputation: 396
Like Alberto M wrote, you can change the type of your buffer and how you receive into it:
union {
uint8_t rawData[MAX_MSG_SIZE];
struct MsgHeader msgHeader;
struct {
struct MsgHeader dummy;
struct Msg1 msg;
} msg1;
} rxBuffer;
receiveBuffer(&rxBuffer.rawData);
if (rxBuffer.msgHeader.msgType == MESSAGE1) {
if (rxBuffer.msg1.msg.field1) {
// ...
or directly receive into the struct, if your receive uses char
s (uint8_t
only aliases uint8_t
unlike char
, which may always alias):
struct {
struct MsgHeader msgHeader;
union {
struct Msg1 msg1;
struct Msg2 msg2;
} msg;
} rxBuffer;
recv(fd, (char *)&rxBuffer, MAX_MSG_SIZE, 0);
// handle errors and insufficient recv length
if (rxBuffer.msgHeader.msgType == MESSAGE1) {
// ...
Btw. type punning through a union is standard and doesn't break strict aliasing. See C99-TC3 6.5 (7) and also search for "type punning". The question is about C++, but not C, so Alberto M is right about it being non-standard, but a GCC extension.
Using memcpy
for this works kind of in the same manner like above, but is standard: bytes are copied on per character basis, effectively reinterpreting them as a struct when accessing the destination location, like you would do when you're type punning through a union:
struct MsgHeader msgHeader;
memcpy(&msgHeader, rxBuffer, sizeof(msgHeader));
if (msg_header.msgType == MESSAGE1) {
struct Msg1 msg;
memcpy(&msg, rxBuffer + sizeof(msgHeader), sizeof(msg));
if (msg.field1 == 0) {
// Some code here;
}
}
Or like Vaughn Cato wrote, you can unpack (and should then probably also pack) the received and sent network buffers yourself. Again it's standard compliant and this way you also work around padding and byte order in a portable way:
uint8_t *buf= rxBuffer;
struct MsgHeader msgHeader;
msgHeader.msgType = (buf[3]<<0) | (buf[2]<<8) | (buf[1]<<16) | (buf[0]<<24); // read uint32_t in big endian
if (msgHeader.msgType == MESSAGE2) {
struct Msg2 msg;
buf += sizeof(MsgHeader);
msg.field1 = (buf[1]<<0) | (buf[0]<<8); // read uint16_t in big endian
if (msg.field1 == 0) {
// ...
Note: struct Msg1
and struct Msg2
don't contain a struct MsgHeader
in the above snippets and are like this:
struct Msg1 {
uint32_t field1;
};
struct Msg2 {
uint16_t field1;
};
Upvotes: 1
Reputation: 1077
Define rxBuffer
as a pointer to a union
of uint8_t[MAX_SIZE]
, MsgHeader
, Msg1
and whatever type you plan to cast to. Note that this would still break the strict aliasing rules, but in GCC it it guaranteed to work as non-standard extension.
EDIT: if such a method would lead to a too complicated declaration, a fully portable (if slower) way is to keep the buffer as a simple uint8_t[]
and memcpy
it to the opportune message struct as soon as it has to be reinterpreted. The feasability of this method obviously depends on your performance and efficiency needs.
EDIT 2: a third solution (if you are working on "normal" architectures) is to use Not valid because the conversion to the message type might not work, see herechar
or unsigned char
instead of uint8_t
. Such types are guaranteed to alias everything.
Upvotes: 5
Reputation: 64308
By working with the individual bytes, you can avoid all pointer casting and eliminate portability issues with endianness and alignment:
uint32_t decodeUInt32(uint8_t *p) {
// Decode big-endian, which is network byte order.
return (uint32_t(p[0])<<24) |
(uint32_t(p[1])<<16) |
(uint32_t(p[2])<< 8) |
(uint32_t(p[3]) );
}
void onRxMessage() {
// Detect message type
if ( decodeUInt32(rxBuffer) == MESSAGE1 ) {
// Process Msg1 message.
if ( decodeUInt32(rxBuffer+4) == 0 ) {
// Some code here;
}
return;
}
// Process other message types.
}
Upvotes: 2