我问题的背景是在网络编程中。假设我想在两个程序之间通过网络发送消息。为了简单起见,让我们假设消息看起来是这样的,字节顺序不是一个问题。我希望找到一种正确、可移植和高效的方法来将这些消息定义为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没有违反别名规则,因为字节/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)
/* ... */
}然而,这似乎违反了工会在任何特定时间只包含一名成员的想法。此外,如果源缓冲区在word/半字边界上没有对齐,这似乎会导致对齐问题。
复印:
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是一个更明显的问题。
相关信息:
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 *类型。按照我的理解和这的答案,这是绝对是编辑-没有打破严格的-别名,因此有未定义的行为。
发布于 2013-10-07 23:04:10
我想这就是我一直试图避免的,但我最终还是亲自去看了一下C99标准。下面是我所发现的(强调是另加的):
§6.3.2.2无效
1无效表达式(具有虚空类型的表达式)的(不存在)值不得以任何方式使用,隐式或显式转换(无效除外)不应应用于该表达式。如果将任何其他类型的表达式计算为空表达式,则放弃其值或指示符。(对void表达式的副作用进行评估。)
§6.3.2.3指针
1指向空的指针可以转换为或从指向任何不完整或对象类型的的指针转换。指向任何不完整或对象类型的指针可以再次转换为无效指针和返回指针;结果应与原始指针相比较。
和第3.14节
1对象 执行环境中的数据存储区域,其内容可以表示值。
§6.5
对象的存储值只能由具有下列类型之一的lvalue表达式访问: -与对象的有效类型兼容的类型 -与对象的有效类型兼容的qualifi编辑版本, -与物体的有效类型对应的有符号或无符号类型的类型, -与该对象的有效类型的qualified版本对应的有符号或无符号类型的类型, -集合或联合类型,其中包括上述类型之一 成员(递归地包括小聚合或包含联盟的成员),或 -性格类型。
§6.5
访问其存储值的对象的有效类型是 对象,如果有的话。如果值通过具有非字符类型的类型的lvalue存储到没有声明类型的对象中,则lvalue的类型将成为该访问的对象的有效类型,以及随后不修改存储值的访问。如果值被复制到没有使用memcpy或memmove声明类型的对象中,或者被复制为字符类型数组,则该访问的modified对象的有效类型以及不修改值的后续访问的有效类型是复制值的对象的有效类型(如果它有值的话)。对于没有声明类型的对象的所有其他访问,对象的有效类型只是用于访问的lvalue的类型。
§J.2未定义的行为
-试图使用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)
/* ... */
}发布于 2013-10-03 17:58:21
正如您推测的那样,唯一正确的方法是将数据从char缓冲区复制到结构中。你的其他选择违反了严格的别名规则,或者是一名工会成员的主动规则。
我想再花一点时间提醒您,即使您在单个主机和字节顺序上这样做并不重要,您仍然必须确保用相同的选项构建的连接数组的两端,以及结构的填充方式是相同的,类型大小相同等等。我建议至少花点时间考虑一个真正的序列化实现,这样如果您需要支持更广泛的条件,那么您的面前就不会有很大的更新。
https://stackoverflow.com/questions/19165134
复制相似问题