32

我的问题的背景是网络编程。假设我想通过网络在两个程序之间发送消息。为简单起见,假设消息看起来像这样,字节顺序不是问题。我想找到一种正确、可移植且有效的方法来将这些消息定义为 C 结构。我知道有四种方法:显式强制转换、通过联合强制转换、复制和编组。

struct message {
    uint16_t logical_id;
    uint16_t command;
};

显式铸造:

void send_message(struct message *msg) {
    uint8_t *bytes = (uint8_t *) msg;
    /* call to write/send/sendto here */
}

void receive_message(uint8_t *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    struct message *msg = (struct message*) bytes;
    /* And now use the message */
    if (msg->command == SELF_DESTRUCT)
        /* ... */
}

我的理解是这send_message不违反别名规则,因为字节/字符指针可以别名任何类型。但是,反之亦然,因此receive_message违反了别名规则,因此具有未定义的行为。

通过联合铸造:

union message_u {
    struct message m;
    uint8_t bytes[sizeof(struct message)];
};

void receive_message_union(uint8_t *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    union message_u *msgu = bytes;
    /* And now use the message */
    if (msgu->m.command == SELF_DESTRUCT)
        /* ... */
}

然而,这似乎违反了工会在任何给定时间只包含其成员之一的想法。此外,如果源缓冲区未在字/半字边界上对齐,这似乎会导致对齐问题。

复制:

void receive_message_copy(uint8_t *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    struct message msg;
    memcpy(&msg, bytes, sizeof msg);
    /* And now use the message */
    if (msg.command == SELF_DESTRUCT)
        /* ... */
}

这似乎可以保证产生正确的结果,但我当然更愿意不必复制数据。

编组

void send_message(struct message *msg) {
    uint8_t bytes[4];
    bytes[0] = msg.logical_id >> 8;
    bytes[1] = msg.logical_id & 0xff;
    bytes[2] = msg.command >> 8;
    bytes[3] = msg.command & 0xff;
    /* call to write/send/sendto here */
}

void receive_message_marshal(uint8_t *bytes, size_t len) {
    /* No longer relying on the size of the struct being meaningful */
    assert(len >= 4);    
    struct message msg;
    msg.logical_id = (bytes[0] << 8) | bytes[1];    /* Big-endian */
    msg.command = (bytes[2] << 8) | bytes[3];
    /* And now use the message */
    if (msg.command == SELF_DESTRUCT)
        /* ... */
}

仍然必须复制,但现在与结构的表示分离。但是现在我们需要明确每个成员的位置和大小,字节序是一个更明显的问题。

相关资料:

什么是严格的别名规则?

在不违反标准的情况下使用指向结构的指针对数组进行别名

对于严格的指针别名,char* 何时安全?

http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html

现实世界的例子

我一直在寻找网络代码的示例,以了解如何在其他地方处理这种情况。轻量级的ip也有几个类似的案例。udp.c文件中包含以下代码:

/**
 * Process an incoming UDP datagram.
 *
 * Given an incoming UDP datagram (as a chain of pbufs) this function
 * finds a corresponding UDP PCB and hands over the pbuf to the pcbs
 * recv function. If no pcb is found or the datagram is incorrect, the
 * pbuf is freed.
 *
 * @param p pbuf to be demultiplexed to a UDP PCB (p->payload pointing to the UDP header)
 * @param inp network interface on which the datagram was received.
 *
 */
void
udp_input(struct pbuf *p, struct netif *inp)
{
  struct udp_hdr *udphdr;

  /* ... */

  udphdr = (struct udp_hdr *)p->payload;

  /* ... */
}

其中struct udp_hdr是 udp 标头的打包表示,p->payload类型为void *. 根据我的理解和这个答案,这绝对是[edit-not] 破坏严格混叠,因此具有未定义的行为。

4

2 回答 2

9

我想这是我一直试图避免的,但我终于亲自去看看C99 标准。这是我发现的(强调添加):
§6.3.2.2 void

1 void 表达式(具有 void 类型的表达式)的(不存在的)值不应以任何方式使用,并且不应将隐式或显式转换(除了 void)应用于此类表达式。如果任何其他类型的表达式被评估为 void 表达式,则其值或指示符将被丢弃。(评估 void 表达式的副作用。)

§6.3.2.3 指针

1指向 void 的指针可以转换为指向任何不完整或对象类型的指针或从指针转换。指向任何不完整或对象类型的指针可以转换为指向 void 的指针并再次返回;结果应与原始指针比较。

和§3.14

1 执行环境中数据存储的对象
区域,其内容可以表示值

§6.5

对象的存储值只能由具有以下类型之一的左值表达式访问:
与对象的有效类型兼容的类型,
— 与对象的有效类型兼容的类型的限定版本,
—对应于对象有效类型的有符号或无符号类型,
— 对应于对象有效类型的限定版本的有符号或无符号类型,
— 聚合或联合类型,包括一个其
成员中的上述类型(递归地,包括子聚合或包含联合的成员),或者
- 字符类型。

§6.5

访问其存储值的对象的有效类型是对象的声明类型(
如果有)。如果通过具有非字符类型类型的左值将值存储到没有声明类型的对象中,则左值的类型将成为该访问的对象的有效类型以及不修改该类型的后续访问储值. 如果使用 memcpy 或 memmove 将值复制到没有声明类型的对象中,或者复制为字符类型的数组,则该访问和不修改该值的后续访问的修改对象的有效类型是从中复制值的对象的有效类型(如果有的话)。对于没有声明类型的对象的所有其他访问,对象的有效类型只是用于访问的左值的类型。

§J.2 未定义行为

— 尝试使用 void 表达式的值,或者将隐式或显式转换(除了 void)应用于 void 表达式(6.3.2.2)。

结论

可以(明确定义)来往 avoid*转换,但不能voidC99中使用类型值。因此,“现实世界的例子”不是未定义的行为。因此,只要注意对齐、填充和字节顺序,就可以使用显式转换方法进行以下修改:

void receive_message(void *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    struct message *msg = (struct message*) bytes;
    /* And now use the message */
    if (msg->command == SELF_DESTRUCT)
        /* ... */
}
于 2013-10-07T23:04:10.747 回答
5

正如您所猜测的,唯一正确的方法是将数据从char缓冲区复制到您的结构中。您的其他替代方案违反了严格的别名规则或 one-member-of-union-active 规则。

我确实想再花一点时间提醒您,即使您在单个主机上执行此操作并且字节顺序无关紧要,您仍然必须确保使用相同的选项构建连接的两端并且结构以相同的方式填充,类型的大小相同,等等。我建议至少花一点时间考虑真正的序列化实现,这样如果你需要支持更广泛的条件,你就没有那么大更新就在你面前。

于 2013-10-03T17:58:21.913 回答