Правильный, портативный способ интерпретации буфера как структуры
контекст моей проблемы заключается в сетевом программировании. Скажем, я хочу отправлять сообщения по сети между двумя программами. Для простоты предположим, что сообщения выглядят так, и порядок байтов не вызывает беспокойства. Я хочу найти правильный, портативный и эффективный способ, чтобы определить эти сообщения как 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)
/* ... */
}
мой понимание - вот что!--6--> не нарушает правила псевдонимов, поскольку указатель 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)
/* ... */
}
все еще нужно скопировать, но теперь отделено от представления структуры. Но теперь нам нужно быть ясными с позицией и размером каждого члена, и эндианство-гораздо более очевидный вопрос.
обзоры info:
что такое строгое правило антиалиасинга?
сглаживание массива с указателем на структуру без нарушения стандарта
когда char * безопасно для строгого сглаживания указателя?
http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
Пример Реального Мира
Я ищу примеры сетевого кода, чтобы увидеть, как это ситуация решается в другом месте. The легкий 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] нарушение строгого сглаживания и, следовательно, имеет неопределенное поведение.
2 ответов
Я думаю, это то, чего я пытался избежать, но я, наконец, пошел и посмотрел на стандарт C99 сам. Вот что я нашел (Курсив мой):
§6.3.2.2 void
1 (несуществующее) значение выражения void (выражение с типом void) не должно неявные или явные преобразования (за исключением void) не должны использоваться каким-либо образом применительно к такому выражению. Если выражение любого другого типа оценивается как пустота выражение, его значение или обозначение отбрасывается. (Выражение void вычисляется для его побочные явления.)
§6.3.2.3 указатели
1 указатель на void может быть преобразован в или из указателя на любой неполный или объект тип. Указатель на любой неполный тип или тип объекта может быть преобразован в указатель на void и обратно; результат должен сравниваться равный исходному указатель.
и §3.14
1 объект
область хранения данных в среде выполнения, содержимое которой может представлять значения
§6.5
объект должен иметь свое сохраненное значение, доступ только к выражению lvalue, которое имеет одно из следующие типы:
- тип, совместимый с действующим тип объекта,
- квалифицированная версия типа, совместимого с эффективным типом объект,
- тип, который является подписанным или неподписанным типом, соответствующим действующему типу объекта,
- тип, который является подписанным или неподписанным типом, соответствующим квалифицированной версии действующего типа объекта,
- тип агрегата или объединения, который включает один из вышеупомянутых типов среди его
члены (включая, рекурсивно, член субагрегата или содержащегося объединения), или
- характер тип.
§6.5
эффективным типом объекта для доступа к его сохраненному значению является объявленный тип
возражаю, если есть. если значение хранится в объекте, не имея объявленного типа через lvalue, имеющий тип, который не является типом символа, тогда тип lvalue становится эффективный тип объекта для этого доступа и для последующих обращений, которые не изменяют сохраненное значение. Если значение скопировано в объект, не имеющий объявленного типа memcpy или memmove, или копируется как массив символьного типа, а затем эффективный тип измененного объекта для доступа и для последующих обращений, которые не изменяют value-эффективный тип объекта, из которого копируется значение, если оно имеет значение. Для все остальные обращения к объекту, не имеющему объявленного типа, эффективный тип объекта просто тип lvalue используемый для доступ.
§J. 2 Неопределенное Поведение
- делается попытка использовать значение выражения void, или неявное или явное преобразование (кроме void) применяется к выражению void (6.3.2.2).
вывод
это нормально (хорошо определено), чтобы бросить в-и-из void*
, но не ОК, чтобы использовать значение типа void
на C99. Поэтому "пример реального мира" не является неопределенным поведением. Поэтому явный метод литья может использоваться со следующей модификацией, если выполняется выравнивание, заполнение и порядок байтов:
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
буфер в вашу структуру. Другие альтернативы нарушают строгие правила псевдонимов или правило "один член профсоюза".
Я хочу взять еще один момент, чтобы напомнить вам, что даже если вы сделаете это на одном хосте и порядке байтов, вы все равно должны убедиться, что оба конца соединения arae построены с теми же параметрами и что структура дополнена таким же образом, типы одинаковый размер и т. д. Я предлагаю взять хотя бы небольшое количество времени, учитывая реальную реализацию сериализации, чтобы, если вам когда-либо понадобится поддерживать более широкий спектр условий, у вас не будет большого обновления перед вами.