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

У меня есть код на микроконтроллере 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, так и C ++, хотя я не думаю, что GCC будет обращаться с ними по-другому.
  • Эти структуры не наследуются и не наследуют ничего.
  • Эти структуры содержат только целочисленные поля фиксированного размера и другие подобные упакованные структуры. (Я был сожжен поплавками раньше …)

38

Решение

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

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

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

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

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

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

8

Другие решения

Учитывая упомянутые платформы, да, упакованные структуры вполне подходят для использования. 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)

Могут возникнуть две проблемы:

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

Вот документ от GCC:

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

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

MSVC:

Упаковать класс — значит разместить его членов сразу друг за другом в
объем памяти

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

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


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

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

20

Если

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

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

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

12

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

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

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

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

8

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

2

Это сильно зависит от структуры, имейте ввиду, что в C ++ struct это класс с видимостью public по умолчанию.

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

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

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

Заметка

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

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

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

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

Примете ваше 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 очень удобно также то, что если вы передаете их в качестве аргумента функции, вы можете установить несколько единиц одновременно, так как они заданы как битовые флаги. Таким образом, если вы хотите создать функцию, которая принимает эту шаблонную структуру в качестве первого аргумента, а затем поддерживает ОС в качестве второго, вы можете передать всю доступную поддержку ОС в виде битовых флагов.

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

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

1

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

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

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

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

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

1
По вопросам рекламы [email protected]