在不破坏严格别名规则的情况下,在现代GCC / C ++中使用网络缓冲区的正确方法

时间:2015-06-05 12:45:58

标签: c++ c++11 gcc strict-aliasing

该计划 - 某种旧式网络消息传递:

// 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 转换为其他指针,它不会引起任何警告。但这是安全,正确和便携的方式吗?

4 个答案:

答案 0 :(得分:5)

rxBuffer定义为union uint8_t[MAX_SIZE]MsgHeaderMsg1以及您计划投射到的任何类型的指针。请注意,这仍然会破坏严格的别名规则,但在GCC中它保证可以作为非标准扩展。

编辑:如果这样的方法会导致过于复杂的声明,那么完全可移植(如果较慢)的方法是将缓冲区保持为简单的uint8_t[]并将memcpy保存到适当的消息结构中一旦它被重新解释。这种方法的可行性显然取决于您的性能和效率需求。

编辑2:第三种解决方案(如果您正在使用"正常"体系结构)是使用charunsigned char而不是uint8_t。这些类型保证为所有内容设置别名。无效,因为转换为消息类型可能不起作用,请参阅here

答案 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)

  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扩展。

  2. 使用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;
            }
    }
    
  3. 或者像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) {
                    // ...
    
  4. 注意:struct Msg1struct 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));,但我想我应该首先等待我的“别名演员”的反馈!