c++ - ¿Las estructuras empaquetadas son portátiles?
gcc packed (9)
Aquí hay un pseudo código para un algoritmo que puede ajustarse a sus necesidades para garantizar el uso con el sistema operativo y la plataforma adecuados.
Si usa el lenguaje C
, no podrá usar classes
, templates
y algunas otras cosas, pero puede usar preprocessor directives
para crear la versión de su struct(s)
que necesita en base al OS
, el arquitecto CPU-GPU-Hardware Controller Manufacturer {Intel, AMD, IBM, Apple, etc.}
, platform x86 - x64 bit
, y finalmente el endian
del diseño de bytes. De lo contrario, el enfoque aquí sería hacia C ++ y el uso de plantillas.
Tome su struct(s)
por ejemplo:
struct Sensor1Telemetry { int16_t temperature; uint32_t timestamp; uint16_t voltageMv; // etc... } __attribute__((__packed__)); struct TelemetryPacket { Sensor1Telemetry tele1; Sensor2Telemetry tele2; // etc... } __attribute__((__packed__));
Puede moldear estas estructuras de la siguiente manera:
enum OS_Type {
// Flag Bits - Windows First 4bits
WINDOWS = 0x01 // 1
WINDOWS_7 = 0x02 // 2
WINDOWS_8 = 0x04, // 4
WINDOWS_10 = 0x08, // 8
// Flag Bits - Linux Second 4bits
LINUX = 0x10, // 16
LINUX_vA = 0x20, // 32
LINUX_vB = 0x40, // 64
LINUX_vC = 0x80, // 128
// Flag Bits - Linux Third Byte
OS = 0x100, // 256
OS_vA = 0x200, // 512
OS_vB = 0x400, // 1024
OS_vC = 0x800 // 2048
//....
};
enum ArchitectureType {
ANDROID = 0x01
AMD = 0x02,
ASUS = 0x04,
NVIDIA = 0x08,
IBM = 0x10,
INTEL = 0x20,
MOTOROALA = 0x40,
//...
};
enum PlatformType {
X86 = 0x01,
X64 = 0x02,
// Legacy - Deprecated Models
X32 = 0x04,
X16 = 0x08,
// ... etc.
};
enum EndianType {
LITTLE = 0x01,
BIG = 0x02,
MIXED = 0x04,
// ....
};
// Struct to hold the target machines properties & attributes: add this to your existing struct.
struct TargetMachine {
unsigned int os_;
unsigned int architecture_;
unsigned char platform_;
unsigned char endian_;
TargetMachine() :
os_(0), architecture_(0),
platform_(0), endian_(0) {
}
TargetMachine( unsigned int os, unsigned int architecture_,
unsigned char platform_, unsigned char endian_ ) :
os_(os), architecture_(architecture),
platform_(platform), endian_(endian) {
}
};
template<unsigned int OS, unsigned int Architecture, unsigned char Platform, unsigned char Endian>
struct Sensor1Telemetry {
int16_t temperature;
uint32_t timestamp;
uint16_t voltageMv;
// etc...
} __attribute__((__packed__));
template<unsigned int OS, unsigned int Architecture, unsigned char Platform, unsigned char Endian>
struct TelemetryPacket {
TargetMachine targetMachine { OS, Architecture, Platform, Endian };
Sensor1Telemetry tele1;
Sensor2Telemetry tele2;
// etc...
} __attribute__((__packed__));
Con estos identificadores enum
, puede usar class template specialization
para configurar la class
según sus necesidades, dependiendo de las combinaciones anteriores. Aquí tomaría todos los casos comunes que parecerían funcionar bien con class declaration & definition
default
y establecer eso como la funcionalidad de la clase principal. Luego, para esos casos especiales, como Endian
diferente con orden de bytes, o versiones específicas de SO que hacen algo de una manera diferente, o GCC versus MS
compiladores de GCC versus MS
con el uso de __attribute__((__packed__))
versus #pragma pack()
puede ser el algunas especializaciones que deben tenerse en cuenta. No debería necesitar especificar una especialización para cada combinación posible; eso sería demasiado desalentador y consumiría mucho tiempo, solo debería hacer los pocos casos de casos excepcionales que puedan ocurrir para garantizar que siempre tenga las instrucciones de código adecuadas para el público objetivo. Lo que también hace que las enums
muy útiles también es que si las pasa como un argumento de función, puede establecer varias a la vez, ya que están diseñadas como indicadores de bits. Por lo tanto, si desea crear una función que tome esta estructura de plantilla como su primer argumento, y luego admitió el sistema operativo como segundo, entonces podría pasar todos los soportes de sistema operativo disponibles como indicadores de bits.
Esto puede ayudar a garantizar que este conjunto de packed structures
se "empaquete" y / o alinee correctamente según el objetivo apropiado y que siempre realizará la misma funcionalidad para mantener la portabilidad en diferentes plataformas.
Ahora puede que tenga que hacer esta especialización dos veces entre las directivas de preprocesador para diferentes compiladores de soporte. Tal que si el compilador actual es GCC, ya que define la estructura de una manera con sus especializaciones, entonces Clang en otro, o MSVC, Bloques de código, etc. Por lo tanto, hay un poco de sobrecarga para configurarlo inicialmente, pero debería, podría asegúrese de que se use correctamente en el escenario o combinación de atributos especificada de la máquina de destino.
Tengo un código en un microcontrolador Cortex-M4 y me gustaría comunicarme con una PC usando un protocolo binario. Actualmente, estoy usando estructuras empaquetadas usando el atributo packed
específico de GCC.
Aquí hay un bosquejo aproximado:
struct Sensor1Telemetry {
int16_t temperature;
uint32_t timestamp;
uint16_t voltageMv;
// etc...
} __attribute__((__packed__));
struct TelemetryPacket {
Sensor1Telemetry tele1;
Sensor2Telemetry tele2;
// etc...
} __attribute__((__packed__));
Mi pregunta es:
- Suponiendo que utilizo la misma definición exacta para la estructura de
TelemetryPacket
en la MCU y la aplicación cliente, ¿el código anterior será portátil en múltiples plataformas? (Estoy interesado en x86 y x86_64, y necesito que se ejecute en Windows, Linux y OS X.) - ¿Los otros compiladores admiten estructuras empaquetadas con el mismo diseño de memoria? Con que sintaxis?
EDITAR :
- Sí, sé que las estructuras empaquetadas no son estándar, pero parecen lo suficientemente útiles como para considerar su uso.
- Estoy interesado en C y C ++, aunque no creo que GCC los maneje de manera diferente.
- Estas estructuras no son heredadas y no heredan nada.
- Estas estructuras solo contienen campos enteros de tamaño fijo y otras estructuras empaquetadas similares. (He sido quemado por carrozas antes ...)
Depende mucho de qué estructura sea, tenga en cuenta que en C ++ struct
es una clase con visibilidad pública por defecto.
Así que puedes heredar e incluso agregar virtual a esto para que esto pueda romper las cosas para ti.
Si se trata de una clase de datos pura (en términos de C ++, una clase de diseño estándar ), esto debería funcionar en combinación con el packed
.
También tenga en cuenta que si comienza a hacer esto, puede tener problemas con las estrictas reglas de aliasing de su compilador, ya que tendrá que mirar la representación de bytes de su memoria ( -fno-strict-aliasing
es su amigo).
Nota
Dicho esto, recomendaría encarecidamente no usar eso para la serialización. Si usa herramientas para esto (es decir: protobuf, flatbuffers, msgpack u otros), obtiene un montón de características:
- independencia del lenguaje
- rpc (llamada de procedimiento remoto)
- lenguajes de especificaciones de datos
- esquemas / validación
- control de versiones
Hablando de alternativas y teniendo en cuenta su pregunta contenedor tipo Tuple para datos empaquetados (para los cuales no tengo suficiente reputación para comentar), sugiero echar un vistazo al proyecto CommsChampion Alex Robenko:
COMMS es la biblioteca independiente de plataforma C ++ (11) solamente, lo que hace que la implementación de un protocolo de comunicación sea un proceso fácil y relativamente rápido. Proporciona todos los tipos y clases necesarios para hacer que la definición de los mensajes personalizados, así como el ajuste de los campos de datos de transporte, sean simples declaraciones declarativas de tipo y definiciones de clase. Estas declaraciones especificarán QUÉ debe implementarse. Los componentes internos de la biblioteca COMMS manejan la parte CÓMO.
Dado que está trabajando en un microcontrolador Cortex-M4, también puede encontrar interesante que:
La biblioteca COMMS fue desarrollada específicamente para ser utilizada en sistemas integrados, incluidos los de metal. No usa excepciones y / o RTTI. También minimiza el uso de la asignación dinámica de memoria y proporciona la capacidad de excluirla por completo si es necesario, lo que puede ser necesario al desarrollar sistemas incrustados de metal.
Alex proporciona un excelente libro electrónico gratuito titulado Guía para la implementación de protocolos de comunicación en C ++ (para sistemas integrados) que describe las partes internas.
No siempre. Cuando envía datos a diferentes procesadores de arquitecto, debe considerar Endianness, tipo de datos primitivos, etc. Es mejor usar Thrift o Message Pack . Si no, cree en su lugar los métodos Serialize y DeSerialize.
Nunca debe usar estructuras en los dominios de compilación, en la memoria (registros de hardware, separar elementos leídos de un archivo o pasar datos entre procesadores o el mismo procesador de software diferente (entre una aplicación y un controlador de núcleo)). Está pidiendo problemas ya que el compilador tiene algo de libre albedrío para elegir la alineación y luego el usuario además de eso puede empeorar el uso de modificadores.
No, no hay ninguna razón para suponer que pueda hacerlo de forma segura en todas las plataformas, incluso si utiliza la misma versión del compilador gcc, por ejemplo, contra diferentes objetivos (diferentes compilaciones del compilador, así como las diferencias de destino).
Para reducir sus probabilidades de falla, comience primero con los artículos más grandes (64 bits, luego 32 bits, los 16 bits, finalmente, los artículos de 8 bits). Idealmente alinee en 32 mínimos quizás 64, lo que uno esperaría hacer con brazo y x86, pero eso siempre puede cambiar como así como el valor por defecto puede ser modificado por quien crea el compilador a partir de las fuentes.
Ahora bien, si esto es una cuestión de seguridad laboral, seguro, puede hacer un mantenimiento regular de este código, probablemente necesite una definición de cada estructura para cada objetivo (por lo tanto, una copia del código fuente para la definición de estructura para ARM y otra para x86, o lo necesitará eventualmente si no de inmediato). Y luego, todos o cada uno de los pocos lanzamientos de productos te llaman para trabajar en el código ... Pequeñas bombas de tiempo de mantenimiento que se apagan ...
Si desea comunicarse de forma segura entre dominios de compilación o procesadores de las mismas o diferentes arquitecturas, utilice una matriz de algún tamaño, una secuencia de bytes, una secuencia de medias palabras o una secuencia de palabras. Reduce significativamente el riesgo de fallas y mantenimiento en el futuro. No use estructuras para separar aquellos elementos que solo restauren el riesgo y el fracaso.
La razón por la cual la gente parece pensar que esto está bien porque usa el mismo compilador o familia contra el mismo objetivo o familia (o compiladores derivados de otras elecciones de compiladores), ya que comprende las reglas del lenguaje y dónde están las áreas definidas de implementación. finalmente encontrará una diferencia, a veces lleva décadas en su carrera, a veces lleva semanas ... Es el problema de "trabaja en mi máquina" ...
Podrías hacer eso, o usar una alternativa más confiable.
Para el núcleo duro entre los fanáticos de la serialización, CapnProto . Esto le proporciona una estructura nativa y se compromete a garantizar que cuando se transfiere a través de una red y se trabaje ligeramente, tenga sentido el otro extremo. Llamarlo una serialización es casi inexacto; tiene como objetivo hacer un poco de lo posible a la representación en memoria de una estructura. Podría ser susceptible de portar a un M4
Hay Buffers de Protocolo de Google, eso es binario. Más robusto, pero bastante bueno. Está el nanopb que lo acompaña (más adecuado para los microcontroladores), pero no hace todo el GPB (no creo que haga uno solo). Mucha gente lo usa con éxito sin embargo.
Algunos de los tiempos de ejecución de C asn1 son lo suficientemente pequeños para su uso en microcontroladores. Sé que este encaja en M0.
Si
- endianness no es un problema
- ambos compiladores manejan el embalaje correctamente
- las definiciones de tipo en ambas implementaciones C son precisas (cumple con las normas).
entonces sí, las " estructuras empaquetadas " son portátiles.
Para mi gusto demasiados "si", no hagas esto. No vale la pena que surja la molestia.
Si quiere algo portátil al máximo, puede declarar un buffer de uint8_t[TELEM1_SIZE]
y memcpy()
desde y hacia compensaciones dentro de él, realizando conversiones de endianness como htons()
y htonl()
(o equivalentes little-endian como los que en glib). Podría envolver esto en una clase con métodos getter / setter en C ++, o una estructura con funciones getter-setter en C.
Teniendo en cuenta las plataformas mencionadas, sí, las estructuras empaquetadas están completamente bien de usar. x86 y x86_64 siempre admiten el acceso no alineado y, contrariamente a la creencia común, el acceso no alineado en estas plataformas tiene ( almost ) la misma velocidad que el acceso alineado durante mucho tiempo (no existe tal cosa que el acceso desalineado sea mucho más lento). El único inconveniente es que el acceso puede no ser atómico, pero no creo que importe en este caso. Y hay un acuerdo entre los compiladores, las estructuras empaquetadas usarán el mismo diseño.
GCC / clang admite estructuras empaquetadas con la sintaxis que mencionaste. MSVC tiene el #pragma pack
, que se puede usar así:
#pragma pack(push, 1)
struct Sensor1Telemetry {
int16_t temperature;
uint32_t timestamp;
uint16_t voltageMv;
// etc...
};
#pragma pack(pop)
Pueden surgir dos problemas:
- La endianidad debe ser la misma en todas las plataformas (su MCU debe estar usando little-endian)
- Si asigna un puntero a un miembro de estructura empaquetado, y está en una arquitectura que no admite acceso desalineado (o usa instrucciones que tienen requisitos de alineación, como
movaps
oldrd
), entonces puede obtener un bloqueo usando ese puntero ( gcc no te advierte sobre esto, pero clang lo hace).
Aquí está el documento de GCC:
El atributo empaquetado especifica que una variable o campo de estructura debe tener la alineación más pequeña posible: un byte para una variable
Entonces GCC garantiza que no se usará relleno.
MSVC:
Para empacar una clase es colocar sus miembros directamente uno después del otro en la memoria
Entonces MSVC garantiza que no se usará relleno.
La única área "peligrosa" que he encontrado, es el uso de bitfields. Entonces el diseño puede diferir entre GCC y MSVC. Pero, hay una opción en GCC, que los hace compatibles: -mms-bitfields
Consejo: incluso si esta solución funciona ahora y es muy poco probable que deje de funcionar, le recomiendo que mantenga baja la dependencia de su código en esta solución.
Nota: He considerado solo GCC, clang y MSVC en esta respuesta. Hay compiladores tal vez, por lo que estas cosas no son ciertas.