【发布时间】:2013-10-10 12:03:11
【问题描述】:
我的问题的背景是网络编程。假设我想通过网络在两个程序之间发送消息。为简单起见,假设消息看起来像这样,字节顺序不是问题。我想找到一种正确、可移植且有效的方法来将这些消息定义为 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)
/* ... */
}
仍然需要复制,但现在与结构的表示分离。但是现在我们需要明确每个成员的位置和大小,字节序是一个更明显的问题。
相关信息:
What is the strict aliasing rule?
Aliasing array with pointer-to-struct without violating the standard
When is char* safe for strict pointer aliasing?
http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
现实世界的例子
我一直在寻找网络代码的示例,以了解其他地方如何处理这种情况。 light-weight 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 * 类型。根据我的理解和this 的回答,这肯定 [edit-not] 破坏了严格的混叠,因此具有未定义的行为。
【问题讨论】:
-
我想最正确(不正确)的方法是分配一个新结构,分别读取值并一一填写。
-
@Kninnug 是的,但我也在寻找效率和优雅。对于像这样的简单示例,手动执行它不会有问题,但是当有更多字段和不匹配的字节顺序时,它很快就会变得丑陋。
-
由于字节序、填充等可能存在差异,因此不会有一种光滑、单行、优雅的方式来执行此操作。我认为@Kninnug 关于哪种方法最可靠是正确的。您可以进行联合或内存复制,但随后您需要对幕后发生的事情做出一些强有力的假设,而且它的可移植性会更差。
-
请为 C 和 C++ 输入单独的问题,并从该问题中删除其中一个标签。对于不同的语言,答案是不同的。例如,C 1999 允许访问除最后一个存储的联合成员之外的联合成员(字节被重新解释为新类型),但 C++ 不允许。
-
如果您不考虑字节顺序、成员大小、填充和对齐等问题,则很难将结果标记为“可移植”。
标签: c networking portability strict-aliasing