将缓冲区解释为结构的正确,可移植的方法

我的问题的背景是网络编程。 假设我想通过网络在两个程序之间发送消息。 为简单起见,假设消息看起来像这样,字节顺序不是问题。 我想找到一种正确,可移植且有效的方法来将这些消息定义为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不违反别名规则,因为byte / char指针可以为任何类型设置别名。 但是,反之亦然,因此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) /* ... */ } 

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

相关信息:

什么是严格别名规则?

使用指向结构的别名数组,而不违反标准

何时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 * 。 根据我的理解和这个答案,这肯定是[编辑 – 不是]打破严格别名,因此具有未定义的行为。

我想这就是我一直想避免的,但我终于去了,看看我自己的C99标准 。 这是我发现的(强调添加):
§6.3.2.2无效

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)。

结论

可以(明确定义)转换为来自void* ,但不能在C99中使用void类型的值。 因此,“真实世界的例子”不是未定义的行为。 因此,只要对齐,填充和字节顺序得到处理,显式强制转换方法可以使用以下修改:

 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) /* ... */ } 

正如您所推测的,唯一正确的方法是将char缓冲区中的数据复制到您的结构中。 您的其他选择违反了严格的别名规则或单一成员联合活动规则。

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