Les structures emballées sont-elles portables?

J’ai du code sur un microcontrôleur Cortex-M4 et je souhaite communiquer avec un PC en utilisant un protocole binary. Actuellement, j’utilise des structures compressées utilisant l’atsortingbut packed spécifique à GCC.

Voici un aperçu:

 struct Sensor1Telemetry { int16_t temperature; uint32_t timestamp; uint16_t voltageMv; // etc... } __atsortingbute__((__packed__)); struct TelemetryPacket { Sensor1Telemetry tele1; Sensor2Telemetry tele2; // etc... } __atsortingbute__((__packed__)); 

Ma question est:

  • En supposant que j’utilise exactement la même définition pour la structure TelemetryPacket sur le MCU et l’application client, le code ci-dessus sera-t-il portable sur plusieurs plates-formes? (Je suis intéressé par x86 et x86_64, et j’ai besoin de l’exécuter sous Windows, Linux et OS X.)
  • Les autres compilateurs prennent-ils en charge les structures compressées avec la même disposition de mémoire? Avec quelle syntaxe?

EDIT :

  • Oui, je sais que les structures compactes sont non standard, mais elles semblent assez utiles pour les utiliser.
  • Je suis intéressé par C et C ++, même si je ne pense pas que GCC les traiterait différemment.
  • Ces structures ne sont pas héritées et n’héritent de rien.
  • Ces structures ne contiennent que des champs entiers de taille fixe et d’autres structures compressées similaires. (J’ai été brûlé par des flotteurs avant …)

Vous ne devez jamais utiliser de structures sur des domaines de compilation, contre de la mémoire (registres de matériel, sélection d’éléments lus dans un fichier ou transmission de données entre processeurs ou logiciels différents (entre une application et un pilote de kernel)). Vous demandez des problèmes car le compilateur a un peu de libre volonté de choisir l’alignement, puis l’utilisateur peut aggraver la situation en utilisant des modificateurs.

Non, il n’y a aucune raison de supposer que vous pouvez le faire en toute sécurité sur toutes les plates-formes, même si vous utilisez la même version du compilateur gcc, par exemple, avec différentes cibles (différentes versions du compilateur et les différences cibles).

Pour réduire vos chances de réussite, commencez par les éléments les plus importants (64 bits puis 32 bits, puis 16 bits, puis tous les éléments 8 bits). Idéalement, alignez sur 32 minimum, peut-être 64, ainsi que la valeur par défaut peut être modifiée par quiconque construit le compilateur à partir de sources.

Maintenant, s’il s’agit d’un travail de sécurité, allez-y, vous pouvez effectuer une maintenance régulière de ce code, nécessitant probablement une définition de chaque structure pour chaque cible (une copie du code source pour la définition de structure pour ARM et une autre pour pour x86, ou auront besoin de cela si pas immédiatement). Et puis chaque fois que vous sortez un ou plusieurs produits, vous êtes appelé à travailler sur le code … Jolies petites bombes de maintenance qui se déclenchent …

Si vous souhaitez communiquer en toute sécurité entre des domaines de compilation ou des processeurs de même ou de différentes architectures, utilisez un tableau d’une certaine taille, un stream d’octets, un stream de demi-mots ou un stream de mots. Réduit de manière significative votre risque de défaillance et d’entretien sur la route. N’utilisez pas de structures pour séparer les éléments qui ne font que restaurer le risque et l’échec.

La raison pour laquelle les gens semblent penser que c’est correct en utilisant le même compilateur ou la même famille contre la même cible ou la même famille (ou les compilateurs dérivés d’autres choix de compilateurs), car vous comprenez les règles du langage et finira par se heurter à une différence, parfois ça prend des décennies dans votre carrière, parfois ça prend des semaines … C’est le problème “ça marche sur ma machine” …

Considérant les plates-formes mentionnées, oui, les structures emballées sont parfaitement adaptées à l’utilisation. x86 et x86_64 prenaient toujours en charge les access non alignés, et contrairement à la croyance commune, les access non alignés sur ces plates-formes ont ( presque ) la même vitesse que les access alignés pendant longtemps (l’access non aligné est beaucoup plus lent). Le seul inconvénient est que l’access peut ne pas être atomique, mais je ne pense pas que cela compte dans ce cas. Et il existe un accord entre les compilateurs, les structures compressées utiliseront la même disposition.

GCC / clang prend en charge les structures compressées avec la syntaxe que vous avez mentionnée. MSVC a #pragma pack , qui peut être utilisé comme ceci:

 #pragma pack(push, 1) struct Sensor1Telemetry { int16_t temperature; uint32_t timestamp; uint16_t voltageMv; // etc... }; #pragma pack(pop) 

Deux problèmes peuvent survenir:

  1. Endianness doit être identique sur toutes les plates-formes (votre MCU doit utiliser little-endian)
  2. Si vous atsortingbuez un pointeur à un membre struct, et que vous êtes sur une architecture qui ne prend pas en charge les access non alignés (ou utilisez des instructions ayant des exigences d’alignement, telles que movaps ou ldrd ), vous risquez de tomber en panne ( gcc ne vous avertit pas à ce sujet, mais clang le fait).

Voici le document de GCC:

L’atsortingbut Pack indique qu’une variable ou un champ de structure doit avoir le plus petit alignement possible – un octet pour une variable

Donc GCC garantit qu’aucun remplissage ne sera utilisé.

MSVC:

Emballer une classe, c’est placer ses membres directement les uns après les autres en mémoire

Donc, MSVC garantit qu’aucun remplissage ne sera utilisé.

La seule zone “dangereuse” que j’ai trouvée est l’utilisation de champs de bits. Ensuite, la mise en page peut différer entre GCC et MSVC. Mais, il y a une option dans GCC, qui les rend compatibles: -mms-bitfields


Astuce: même si cette solution fonctionne maintenant et qu’il est très improbable qu’elle ne fonctionne plus, je vous recommande de ne pas trop dépendre de votre code sur cette solution.

Note: Je n’ai considéré que GCC, clang et MSVC dans cette réponse. Il y a peut-être des compilateurs pour lesquels ces choses ne sont pas vraies.

Si

  • l’endianness n’est pas un problème
  • les deux compilateurs gèrent correctement l’emballage
  • les définitions de type sur les deux implémentations C sont précises (conforme aux normes).

alors oui, les ” structures emballées ” sont portables.

A mon gout trop de “si” s, ne le fais pas. Cela ne vaut pas la peine de se poser.

Vous pouvez le faire ou utiliser une alternative plus fiable.

Pour le kernel dur des fanatiques de sérialisation, il y a CapnProto . Cela vous donne une structure native avec laquelle vous devez vous occuper et vous permet de vous assurer que, lorsqu’il est transféré sur un réseau et qu’il est légèrement mis en œuvre, il rest logique à l’autre bout. L’appeler une sérialisation est presque inexact; il vise à faire un peu de choses à la représentation en mémoire d’une structure. Peut être porté sur un M4

Il y a des tampons de protocole Google, c’est binary. Plus gonflable, mais plutôt bon. Il y a la nanopb d’accompagnement (plus adaptée aux microcontrôleurs), mais elle ne fait pas tout le GPB (je ne pense pas que ce soit le cas). Beaucoup de gens l’utilisent avec succès cependant.

Certains des temps d’exécution C asn1 sont suffisamment petits pour être utilisés sur des microcontrôleurs. Je sais que celui-ci correspond à M0.

Si vous voulez quelque chose de portable au maximum, vous pouvez déclarer un tampon de uint8_t[TELEM1_SIZE] et memcpy() vers et depuis les décalages, en effectuant des conversions d’endianness telles que htons() et htonl() ou des équivalents little-endian tels que en glib). Vous pouvez envelopper ceci dans une classe avec des méthodes getter / setter en C ++, ou une structure avec des fonctions getter-setter dans C.

Cela dépend fortement de la structure, gardez à l’esprit que dans C ++, struct est une classe avec une visibilité par défaut publique.

Vous pouvez donc hériter et même append du virtuel à ceci afin que cela puisse casser des choses pour vous.

S’il s’agit d’une classe de données pure (en termes de C ++, une classe de mise en page standard ), cela devrait fonctionner en combinaison avec les données packed .

Gardez également à l’esprit que si vous commencez à faire cela, vous risquez d’avoir des problèmes avec les règles d’alias ssortingctes de votre compilateur, car vous devrez regarder la représentation en octets de votre mémoire ( -fno-ssortingct-aliasing est votre ami).

Remarque

Cela étant dit, je vous déconseille fortement d’utiliser cette fonction pour la sérialisation. Si vous utilisez des outils pour cela (par exemple, protobuf, flatbuffers, msgpack ou autres), vous obtenez une tonne de fonctionnalités:

  • indépendance linguistique
  • RPC (appel de procédure à distance)
  • langages de spécification de données
  • schémas / validation
  • versioning

Voici un pseudo-code vers un algorithme qui peut répondre à vos besoins pour garantir l’utilisation avec le système d’exploitation et la plate-forme cibles appropriés.

Si vous utilisez le langage C , vous ne pourrez pas utiliser les classes , les templates et quelques autres choses, mais vous pouvez utiliser les preprocessor directives pour créer la version de vos struct(s) fonction du OS , l’architecte CPU-GPU-Hardware Controller Manufacturer {Intel, AMD, IBM, Apple, etc.} , platform x86 - x64 bitplatform x86 - x64 bit , et enfin le endian de la disposition des octets. Sinon, l’accent serait mis sur le C ++ et l’utilisation de modèles.

Prenez vos struct(s) par exemple:

 struct Sensor1Telemetry { int16_t temperature; uint32_t timestamp; uint16_t voltageMv; // etc... } __atsortingbute__((__packed__)); struct TelemetryPacket { Sensor1Telemetry tele1; Sensor2Telemetry tele2; // etc... } __atsortingbute__((__packed__)); 

Vous pouvez modéliser ces structures en tant que telles:

 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 & atsortingbutes: 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 struct Sensor1Telemetry { int16_t temperature; uint32_t timestamp; uint16_t voltageMv; // etc... } __atsortingbute__((__packed__)); template struct TelemetryPacket { TargetMachine targetMachine { OS, Architecture, Platform, Endian }; Sensor1Telemetry tele1; Sensor2Telemetry tele2; // etc... } __atsortingbute__((__packed__)); 

Avec ces identifiants d’ enum vous pouvez utiliser la class template specialization pour définir cette class à ses besoins en fonction des combinaisons ci-dessus. Ici, je prendrais tous les cas courants qui sembleraient bien fonctionner avec la class declaration & definition default et définir cela comme la fonctionnalité principale de la classe. Alors, pour les cas spéciaux, tels que Endian différent avec ordre des octets, ou des versions de système d’exploitation spécifiques faisant quelque chose de différent, ou GCC versus MS compilateurs GCC versus MS avec l’utilisation de __atsortingbute__((__packed__)) par rapport à #pragma pack() peu de spécialisations à prendre en compte. Vous ne devriez pas avoir besoin de spécifier une spécialisation pour chaque combinaison possible; cela serait trop compliqué et prendrait beaucoup de temps, il vous suffira de faire les rares scénarios qui peuvent se produire pour vous assurer de toujours avoir des instructions de code appropriées pour le public cible. Ce qui rend également les enums très pratiques, c’est que si vous les transmettez en tant qu’argument de fonction, vous pouvez en définir plusieurs à la fois car elles sont conçues comme des indicateurs de bits. Donc, si vous voulez créer une fonction qui prend ce template struct comme premier argument, alors les systèmes d’exploitation pris en charge comme second argument pourraient alors transmettre tous les supports de système d’exploitation disponibles sous forme d’indicateurs de bits.

Cela peut aider à garantir que cet ensemble de packed structures est “compressé” ou correctement aligné sur la cible appropriée et qu’il exécutera toujours les mêmes fonctionnalités pour maintenir la portabilité sur différentes plates-formes.

Maintenant, vous devrez peut-être effectuer cette spécialisation deux fois entre les directives du préprocesseur pour différents compilateurs de support. De telle sorte que si le compilateur actuel est GCC, car il définit la structure d’une manière avec ses spécialisations, alors Clang dans un autre, ou MSVC, blocs de code, etc. Assurez-vous qu’il est correctement utilisé dans le scénario ou la combinaison d’atsortingbuts spécifiés de la machine cible.

En parlant d’alternatives et en considérant votre question Conteneur semblable à Tuple pour des données emballées (pour lesquelles je n’ai pas assez de réputation pour commenter), je suggère de jeter un coup d’œil au projet CommsChampion d’ Alex Robenko:

COMMS est la bibliothèque indépendante des en-têtes C ++ (11) uniquement, ce qui simplifie et accélère la mise en œuvre d’un protocole de communication. Il fournit tous les types et toutes les classes nécessaires pour que la définition des messages personnalisés, ainsi que des champs de données de transport, soit des déclarations déclaratives simples de définitions de type et de classe. Ces déclarations spécifieront ce qui doit être implémenté. Les internes de la bibliothèque COMMS gèrent la partie HOW.

Puisque vous travaillez sur un microcontrôleur Cortex-M4, vous pouvez également trouver intéressant que:

La bibliothèque COMMS a été spécifiquement développée pour être utilisée dans des systèmes embarqués, y compris ceux à nu. Il n’utilise pas les exceptions et / ou RTTI. Il minimise également l’utilisation de l’allocation dynamic de mémoire et permet de l’exclure complètement si nécessaire, ce qui peut être nécessaire lors du développement de systèmes embarqués à nu.

Alex fournit un excellent livre électronique gratuit intitulé Guide pour la mise en œuvre de protocoles de communication en C ++ (pour les systèmes intégrés) qui décrit les composants internes.

Pas toujours. Lorsque vous envoyez des données à différents processeurs d’architecte, vous devez prendre en compte l’Endianness, le type de données primitif, etc. Mieux vaut utiliser Thrift ou Message Pack . Sinon, créez vous-même les méthodes Serialize et DeSerialize à la place.