Подтвердить что ты не робот

Являются ли упакованные структуры переносимыми?

У меня есть код на микроконтроллере Cortex-M4 и я хотел бы общаться с ПК с использованием двоичного протокола. В настоящее время я использую упакованные структуры с использованием специфичного для GCC атрибута packed.

Вот грубая схема:

struct Sensor1Telemetry {
    int16_t temperature;
    uint32_t timestamp;
    uint16_t voltageMv;
    // etc...
} __attribute__((__packed__));

struct TelemetryPacket {
    Sensor1Telemetry tele1;
    Sensor2Telemetry tele2;
    // etc...
} __attribute__((__packed__));

Мой вопрос:

  • Предполагая, что я использую то же определение для структуры TelemetryPacket на MCU и клиентском приложении, будет ли приведенный выше код быть переносимым на нескольких платформах? (Меня интересуют x86 и x86_64, и нужно, чтобы он работал в Windows, Linux и OS X.)
  • Другие компиляторы поддерживают упакованные структуры с одинаковым макетом памяти? С каким синтаксисом?

ИЗМЕНИТЬ

  • Да, я знаю, что упакованные структуры нестандартны, но они кажутся достаточно полезными, чтобы рассмотреть возможность их использования.
  • Мне интересны как C, так и С++, хотя я не думаю, что GCC будет обрабатывать их по-разному.
  • Эти структуры не наследуются и ничего не наследуют.
  • Эти структуры содержат только целочисленные поля фиксированного размера и другие подобные упакованные структуры. (Я был сожжен поплавками раньше...)
4b9b3361

Ответ 1

Вы никогда не должны использовать структуры для компиляции доменов, по сравнению с памятью (аппаратные регистры, выбор предметов, считанных из файла, или передача данных между процессорами или другим программным обеспечением другого процессора (между приложением и драйвером ядра)). Вы просите о проблемах, поскольку у компилятора есть немного свободного выбора, чтобы выбрать выравнивание, а затем пользователь поверх этого может сделать это хуже с помощью модификаторов.

Нет никаких оснований полагать, что вы можете сделать это безопасно на разных платформах, даже если вы используете одну и ту же версию компилятора gcc, например, для разных целей (разные сборки компилятора, а также целевые различия).

Чтобы уменьшить вероятность неудачи, сначала начните с самых больших предметов (64-битных, а затем 32-битных 16 бит, а затем и любых 8-разрядных элементов). Идеально выровняйте по 32, возможно, 64, которые можно было бы ожидать от руки и x86, но это может всегда меняются так же, как и по умолчанию, могут быть изменены тем, кто строит компилятор из источников.

Теперь, если это проблема безопасности работы, обязательно продолжите, вы можете выполнять регулярное обслуживание этого кода, вероятно, потребуется определение каждой структуры для каждой цели (так что одна копия исходного кода для определения структуры для ARM и еще один для x86, или понадобится это, если не сразу). И затем все или несколько выпусков продуктов, к которым вы можете войти, чтобы выполнить работу над кодом... Хорошие небольшие бомбы с техобслуживанием, которые уходят...

Если вы хотите безопасно обмениваться между компилируемыми доменами или процессорами одинаковыми или разными архитектурами, используйте массив некоторого размера, поток байтов - поток полусловов или поток слов. Значительно снижает риск отказа и обслуживания в будущем. Не используйте структуры, чтобы выделять элементы, которые просто восстанавливают риск и сбой.

Причина, по которой люди, похоже, считают, что это нормально, из-за использования одного и того же компилятора или семейства против одной и той же цели или семейства (или компиляторов, полученных из других вариантов компиляторов), поскольку вы понимаете правила языка и где определенная реализация областях, вы в конечном итоге столкнетесь с разницей, иногда это занимает десятилетия в вашей карьере, иногда это занимает недели... Его проблема "работает на моей машине"...

Ответ 2

Учитывая упомянутые платформы, да, упакованные структуры полностью подходят для использования. x86 и x86_64 всегда поддерживали несвязанный доступ, и, вопреки общему мнению, неизменный доступ на этих платформах имеет (почти) ту же скорость, что и выровненный доступ в течение длительного времени (там нет такой вещи, что несвязанный доступ намного медленнее). Единственный недостаток заключается в том, что доступ может быть не атомарным, но я не думаю, что это имеет значение в этом случае. И есть соглашение между компиляторами, упакованные структуры будут использовать один и тот же макет.

GCC/clang поддерживает упакованные структуры с указанным вами синтаксисом. MSVC имеет #pragma pack, который можно использовать следующим образом:

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

Возможны два вопроса:

  • Endianness должна быть одинаковой на всех платформах (ваш MCU должен использовать мало-endian)
  • Если вы назначаете указатель на упакованный элемент структуры, и вы находитесь в архитектуре, которая не поддерживает неровный доступ (или использует инструкции с требованиями к выравниванию, например movaps или ldrd), тогда вы можете получить сбой с помощью этого указателя (gcc не предупреждает вас об этом, но clang делает).

Здесь документ из GCC:

Упакованный атрибут указывает, что поле переменной или структуры должен иметь наименьшее возможное выравнивание - один байт для переменной

Таким образом, GCC гарантирует, что никакие дополнения не будут использоваться.

MSVC:

Чтобы упаковать класс, нужно поместить его элементы непосредственно друг за другом в память

Таким образом, MSVC гарантирует, что никакие дополнения не будут использоваться.

Единственная "опасная" область, которую я нашел, - это использование битполей. Тогда макет может отличаться между GCC и MSVC. Но в GCC есть опция, которая делает их совместимыми: -mms-bitfields


Совет: даже если это решение работает сейчас, и маловероятно, что он перестанет работать, я рекомендую вам поддерживать зависимость вашего кода от этого решения.

Примечание. В этом ответе я рассматривал только GCC, clang и MSVC. Возможно, есть компиляторы, для которых это не так.

Ответ 3

Если

  • endianness не проблема
  • оба компилятора правильно обрабатывают упаковку
  • определения типов на обеих реализациях C являются точными (стандартное соответствие).

то да, "упакованные структуры" переносимы.

На мой вкус слишком много "если", не делайте этого. Это не стоит хлопот возникнуть.

Ответ 4

Вы можете сделать это или использовать более надежную альтернативу.

Для жесткого ядра среди фанатиков сериализации там CapnProto. Это дает вам родную структуру, с которой нужно иметь дело, и берется за то, чтобы, когда она передавалась по сети и слегка работала, все равно будет смысл в другом конце. Называть его сериализацией почти неточно; он направлен на то, чтобы сделать как можно меньше для внутримоментного представления структуры. Возможно, будет перенесен на M4

Там буферы протокола Google, которые являются двоичными. Больше раздумий, но довольно хорошо. Там сопутствующий nanopb (более подходит для микроконтроллеров), но он не делает весь GPB (я не думаю, что он делает oneof). Многие люди используют его успешно, хотя.

Некоторые из временных циклов C asn1 достаточно малы для использования на микроконтроллерах. Я знаю этот подходит для M0.

Ответ 5

Если вы хотите, чтобы что-то максимально переносимое, вы можете объявить буфер uint8_t[TELEM1_SIZE] и memcpy() к ним и от смещений внутри него, выполняя преобразования endianness, такие как htons() и htonl() (или эквиваленты с маленьким концом, такие как те, которые в glib). Вы можете обернуть это в класс с помощью методов getter/setter в С++ или struct с функциями getter-setter в C.

Ответ 6

Вот псевдокод к алгоритму, который может соответствовать вашим потребностям, чтобы обеспечить использование с надлежащей целевой ОС и платформой.

Если вы используете язык C, вы не сможете использовать classes, templates и несколько других вещей, но вы можете использовать preprocessor directives для создания версии вашего struct(s), который вам нужен на основе OS, архитектор CPU-GPU-Hardware Controller Manufacturer {Intel, AMD, IBM, Apple, etc.}, platform x86 - x64 bit и, наконец, endian макета байта. В противном случае основное внимание будет уделено С++ и использованию шаблонов.

Возьмите struct(s), например:

struct Sensor1Telemetry {
    int16_t temperature;
    uint32_t timestamp;
    uint16_t voltageMv;
    // etc...
} __attribute__((__packed__));

struct TelemetryPacket {
    Sensor1Telemetry tele1;
    Sensor2Telemetry tele2;
    // etc...
} __attribute__((__packed__));

Вы можете создать эти структуры как таковые:

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__));

С помощью этих enum идентификаторов вы можете использовать class template specialization для настройки этого class к его потребностям в зависимости от вышеуказанных комбинаций. Здесь я бы взял все общие случаи, которые, казалось бы, отлично работали с default class declaration & definition и устанавливали это как функциональность основного класса. Затем для этих особых случаев, таких как разные endian с порядком байта, или конкретные версии ОС, которые делают что-то по-другому, или компиляторы GCC versus MS с использованием __attribute__((__packed__)) по сравнению с #pragma pack(), могут быть несколькими специализациями, которые необходимо учитывать. Вам не нужно указывать специализацию для каждой возможной комбинации; что было бы слишком сложным и трудоемким, нужно только делать несколько редких сценариев, которые могут произойти, чтобы убедиться, что у вас всегда есть правильные коды для целевой аудитории. То, что также делает enums очень удобным, заключается в том, что если вы передадите их как аргумент функции, вы можете установить несколько одновременно, поскольку они разработаны как битовые флаги. Поэтому, если вы хотите создать функцию, которая принимает эту структуру шаблона в качестве первого аргумента, а затем поддерживает OS в качестве второй, вы можете передать всю доступную поддержку ОС в виде битовых флагов.

Это может помочь обеспечить, чтобы этот набор packed structures был "упакован" или правильно выровнен в соответствии с соответствующей целью и что он всегда будет выполнять те же функции для поддержания переносимости на разных платформах.

Теперь вам может понадобиться сделать эту специализацию дважды между директивами препроцессора для разных поддерживающих компиляторов. Таким образом, если текущий компилятор является GCC, так как он определяет структуру одним способом с ее специализациями, то Clang в другом, или MSVC, блоками кода и т.д. Таким образом, для первоначальной настройки требуется немного накладных расходов, но это должно высоко убедитесь, что он правильно используется в указанном сценарии или комбинации атрибутов целевой машины.

Ответ 7

Это сильно зависит от того, что такое структура, помните, что в С++ struct есть класс с общедоступной видимостью по умолчанию.

Таким образом, вы можете наследовать и даже добавлять виртуальные, чтобы это могло нарушить для вас вещи.

Если это чистый класс данных (в терминах С++ стандартный класс макета), он должен работать в комбинации с packed.

Также имейте в виду, что если вы начнете это делать, у вас могут возникнуть проблемы со строгими правилами псевдонимов вашего компилятора, потому что вам нужно будет посмотреть представление байта вашей памяти (-fno-strict-aliasing - ваш друг).

Примечание

При этом я настоятельно рекомендую использовать это для сериализации. Если вы используете инструменты для этого (то есть: protobuf, flatbuffers, msgpack или другие), вы получаете массу возможностей:

  • независимость языка
  • rpc (вызов удаленной процедуры)
  • языки спецификации данных
  • Схемы/проверки
  • версионирование

Ответ 8

Говоря об альтернативах и рассматривая ваш вопрос Контейнер с контейнером для упакованных данных (для которого у меня недостаточно репутации для комментариев), я предлагаю посмотрите на Алекс Робенко Проект CommsChampion:

COMMS - это только С++ (11) заголовки, независимая от платформы библиотека, что делает реализацию протокола связи простым и относительно быстрым процессом. Он предоставляет все необходимые типы и классы, чтобы сделать определение пользовательских сообщений, а также обертывание полей транспортных данных, простыми декларативными утверждениями определений типов и классов. В этих заявлениях указывается, ЧТО должно быть реализовано. Внутренние библиотеки COMMS обрабатывают часть HOW.

Поскольку вы работаете над микроконтроллером Cortex-M4, вам также может показаться интересным, что:

Библиотека COMMS была специально разработана для использования во встроенных системах, включая bare-metal. Он не использует исключения и/или RTTI. Он также минимизирует использование динамического распределения памяти и дает возможность полностью исключить его, если это необходимо, что может потребоваться при разработке встроенных систем с открытым металлом.

Алекс предоставляет отличную бесплатную книгу под названием Руководство по реализации коммуникационных протоколов в С++ (для встроенных систем), в котором описываются внутренности.

Ответ 9

Не всегда. Когда вы отправляете данные на другой процессор архитекторов, вам нужно учитывать информацию о Endianness, примитивном типе данных и т.д. Лучше использовать Thrift или Message Pack. Если нет, создайте методы Serialize и DeSerialize.