Manera correcta y portátil de interpretar buffer como una estructura


El contexto de mi problema está en la programación de redes. Digamos que quiero enviar mensajes a través de la red entre dos programas. Por simplicidad, digamos que los mensajes se ven así, y el orden de bytes no es una preocupación. Quiero encontrar una manera correcta, portátil y eficiente de definir estos mensajes como estructuras C. Conozco cuatro enfoques para esto: casting explícito, casting a través de una unión, copiado y marshaling.

struct message {
    uint16_t logical_id;
    uint16_t command;
};

Casting explícito:

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

Mi entendimiento es que send_message no viola las reglas de aliasing, porque un puntero byte/char puede alias de cualquier tipo. Sin embargo, lo contrario no es cierto, por lo que receive_message viola las reglas de aliasing y por lo tanto tiene un comportamiento indefinido.

Fundición a través de una Unión:

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

Sin embargo, esto parece violar la idea de que un sindicato solo contiene a uno de sus miembros en un momento dado. Además, esto parece que podría conducir a problemas de alineación si el búfer de origen no está alineado en una palabra / media palabra límite.

Copiando:

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

Esto parece garantizado para producir el resultado correcto, pero por supuesto que preferiría mucho no tener que copiar los datos.

Marshaling

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

Todavía hay que copiar, pero ahora desacoplado de la representación de la estructura. Pero ahora necesitamos ser explícitos con la posición y el tamaño de cada miembro, y la endianidad es un tema mucho más obvio.

Información relacionada:

¿Cuál es el alias estricto la regla?

Arreglo de aliasing con puntero a estructura sin violar el estándar

¿Cuándo es seguro char* para el alias estricto del puntero?

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

Ejemplo del Mundo Real

He estado buscando ejemplos de código de red para ver cómo se maneja esta situación en otros lugares. El ip ligero tiene algunos casos similares. En el udp.c el archivo contiene el siguiente código:

/**
 * 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;

  /* ... */
}

Donde struct udp_hdr es una representación empaquetada de una cabecera udp y p->payload es de tipo void *. Siguiendo mi entendimiento y esta respuesta, esto es definitivamente [editar - no] rompiendo el estricto aliasing y por lo tanto tiene un comportamiento indefinido.

Author: Community, 2013-10-03

2 answers

Supongo que esto es lo que he estado tratando de evitar, pero finalmente fui y eché un vistazo al estándar C99 yo mismo. Esto es lo que he encontrado (énfasis añadido):
§6.3.2.2 nulo

1 El valor (inexistente) de una expresión void (una expresión que tiene tipo void) no ser utilizado de cualquier manera, y las conversiones implícitas o explícitas (excepto para anular) no serán aplicado a tal expresión. Si una expresión de cualquier otro tipo es evaluada como un void expresión, su valor o designador se descarta. (Una expresión vacía se evalúa para su efectos secundarios.)

§6.3.2.3 Punteros

1 Un puntero a void se puede convertir a o desde un puntero a cualquier objeto o incompleto tipo. Un puntero a cualquier tipo incompleto o de objeto se puede convertir en un puntero a void y de nuevo; el resultado se comparará igual al puntero original.

Y §3.14

1 objeto
región de almacenamiento de datos en el entorno de ejecución, cuyo contenido puede representar valores

§6.5

Un objeto tendrá acceso a su valor almacenado solo por una expresión lvalue que tenga uno de los siguientes tipos:
- un tipo compatible con el tipo efectivo del objeto,
- una versión cualificada de un tipo compatible con el tipo efectivo del objeto,
- un tipo que es el tipo firmado o sin firmar correspondiente al tipo efectivo de la objeto,
- un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada del tipo efectivo del objeto,
- un tipo agregado o de unión que incluya uno de los tipos antes mencionados entre sus
miembros (incluyendo, recursivamente, un miembro de un subagregado o sindicato contenido), o
- un tipo de carácter.

§6.5

El tipo efectivo de un objeto para un acceso a su valor almacenado es el tipo declarado del
objeto, si cualquier. Si un valor se almacena en un objeto que no tiene lvalue que tiene un tipo que no es un tipo de carácter, entonces el tipo del lvalue se convierte en el tipo efectivo del objeto para ese acceso y para accesos posteriores que no modifican el valor almacenado . Si un valor se copia en un objeto que no tiene un tipo declarado usando memcpy o memmove, o se copia como una matriz de tipo de carácter, a continuación, el tipo efectivo del objeto modificado para ese acceso y para accesos posteriores que no modifican el value es el tipo efectivo del objeto del que se copia el valor, si lo tiene. Para todos los demás accesos a un objeto sin tipo declarado, el tipo efectivo del objeto es simplemente el tipo de lvalue utilizado para el acceso.

§J. 2 Comportamiento indefinido

- Se intenta utilizar el valor de una expresión void, o de una expresión implícita o explícita la conversión (excepto a void) se aplica a una expresión void (6.3.2.2).

Conclusión

Está bien (bien definido) convertir a-y-desde un void*, pero no está bien usar un valor de tipo void en C99. Por lo tanto, el "ejemplo del mundo real" no es un comportamiento indefinido. Por lo tanto, el método explicit casting se puede usar con la siguiente modificación, siempre que se cuide la alineación, el relleno y el orden de bytes:

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)
        /* ... */
}
 6
Author: croyd,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-10-08 16:52:13

La única forma correcta es, como supones, copiar los datos del búfer char en tu estructura. Sus otras alternativas violan las estrictas reglas de alias, o la regla de un miembro activo de la unión.

Quiero tomarme un momento más para recordarle que incluso si lo hace en un solo host y el orden de bytes no importa, aún tiene que asegurarse de que ambos extremos de la conexión arae se construyan con las mismas opciones y que la estructura esté acolchada de la misma manera, los tipos son mismo tamaño, etc. Sugiero tomar al menos una pequeña cantidad de tiempo considerando una implementación de serialización real para que si alguna vez necesita soportar una gama más amplia de condiciones, no tenga una gran actualización frente a usted.

 4
Author: Mark B,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-10-03 17:58:21