该计划 - 某种旧式网络消息传递:
// 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.
}
此代码违反了现代GCC中的严格别名(并归结为现代C ++中未指定的行为)。 解决问题的正确方法是什么(使代码不会引发“严格别名”警告)?
附:如果rxBuffer定义为:
union __attribute__((packed)) {
uint8_t[MAX_MSG_SIZE] rawData;
} rxBuffer;
然后我将& rxBuffer 转换为其他指针,它不会引起任何警告。但这是安全,正确和便携的方式吗?
答案 0 :(得分:5)
将rxBuffer
定义为union
uint8_t[MAX_SIZE]
,MsgHeader
,Msg1
以及您计划投射到的任何类型的指针。请注意,这仍然会破坏严格的别名规则,但在GCC中它保证可以作为非标准扩展。
编辑:如果这样的方法会导致过于复杂的声明,那么完全可移植(如果较慢)的方法是将缓冲区保持为简单的uint8_t[]
并将memcpy
保存到适当的消息结构中一旦它被重新解释。这种方法的可行性显然取决于您的性能和效率需求。
编辑2:第三种解决方案(如果您正在使用"正常"体系结构)是使用无效,因为转换为消息类型可能不起作用,请参阅here char
或unsigned char
而不是uint8_t
。这些类型保证为所有内容设置别名。
答案 1 :(得分:2)
通过处理单个字节,可以避免所有指针转换,并消除字节序和对齐的可移植性问题:
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.
}
答案 2 :(得分:1)
与Alberto M写的一样,您可以更改缓冲区的类型以及接收方式:
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) {
// ...
或直接接收到结构中,如果您的收到使用char
s(仅uint8_t
别名uint8_t
而不是char
,这可能总是别名):
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) {
// ...
<击>顺便说一下。通过联合打字是标准,并且不会破坏严格的别名。参见 C99-TC3 6.5(7)并搜索&#34;类型惩罚&#34;。问题是关于C ++,但不是C,所以Alberto M是对的是非标准的,但是GCC扩展。
使用memcpy
的方式与上面的方式相同,但是标准的:基于每个字符复制字节,在访问目标位置时有效地将它们重新解释为结构,就像你一样当你通过工会打字时,会做什么:
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;
}
}
或者像Vaughn Cato写的那样,你可以自己解压缩(并且应该也可以打包)收到和发送的网络缓冲区。它再次符合标准,这样你也可以通过便携方式解决填充和字节顺序问题:
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) {
// ...
注意:struct Msg1
和struct Msg2
在上述代码段中不包含struct MsgHeader
,如下所示:
struct Msg1 {
uint32_t field1;
};
struct Msg2 {
uint16_t field1;
};
答案 3 :(得分:0)
归结为:
((const MsgHeader*)rxBuffer)->msgType
rxBuffer
属于一种类型,但我们希望将其视为另一种类型。我建议使用以下“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
(就像其灵活性较低的表兄memcpy
)有效地表示 在源(rxBuffer
)处可用的位模式将在致电memmove
可在目的地(msg_header_p
)使用。即使类型不同。
您可能认为memmove
没有做任何事情,因为源和目的地是相同的。但这正是重点。 逻辑,它的目的是使msg_header_p
成为rxBuffer
的别名,即使实际上好的编译器会优化它。
(这个答案可能有点争议。我可能会把memmove
推得太远。我猜我的逻辑是:首先,memcpy
到新位置显然可以接受这个问题;第二,memmove
只是memcpy
的更好,更通用(但可能更慢)的版本;第三,如果memcpy
允许您通过不同的方式查看相同的位模式类型,当为什么不应该memmove
允许相同的想法“改变”特定位模式的类型?如果我们memcpy
到临时区域,那么memcpy
回到原来的位置,也可以吗?)
如果你想要建立一个完整的答案,你需要在某个时候再次使用别名,memmove(rxBuffer, msg_header_p, sizeof(MsgHeader));
,但我想我应该首先等待我的“别名演员”的反馈!