打包结构是否可移植?

时间:2017-07-15 08:35:59

标签: c++ c gcc struct packed

我在Cortex-M4微控制器上有一些代码,并希望使用二进制协议与PC通信。目前,我使用的是使用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__));

我的问题是:

  • 假设我对MCU和客户端应用程序上的TelemetryPacket结构使用完全相同的定义,上述代码是否可以跨多个平台移植? (我对x86和x86_64感兴趣,需要它在Windows,Linux和OS X上运行。)
  • 其他编译器是否支持具有相同内存布局的打包结构?用什么语法?

修改

  • 是的,我知道打包的结构是非标准的,但它们看起来很有用,可以考虑使用它们。
  • 我对C和C ++都感兴趣,虽然我不认为GCC会以不同的方式处理它们。
  • 这些结构不是遗传的,也不会遗传任何东西。
  • 这些结构只包含固定大小的整数字段和其他类似的打包结构。 (我之前被浮子烧了......)

9 个答案:

答案 0 :(得分:21)

考虑到所提到的平台,是的,打包结构完全可以使用。 x86和x86_64始终支持未对齐访问,与普遍看法相反,这些平台上的未对齐访问(almost)与对齐访问的速度相同(很长一段时间)(没有未对齐的访问权限)慢得多。唯一的缺点是访问可能不是原子的,但在这种情况下我认为不重要。编译器之间存在协议,打包的结构将使用相同的布局。

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. 如果您指定了一个打包的struct成员的指针,并且您在一个不支持未对齐访问的架构上(或使用具有对齐要求的指令,如movaps或{{ 1}}),然后你可能会使用那个指针崩溃(gcc没有警告你这个,但是clang确实如此)。
  3. 以下是海湾合作委员会的文件:

      

    packed属性指定变量或结构字段   应该具有尽可能小的对齐 - 变量的一个字节

    所以GCC 保证不会使用填充。

    MSVC:

      

    打包一个类是将其成员直接放在彼此之后   存储器

    所以MSVC 保证不会使用填充。

    唯一的"危险"我发现的区域是位域的使用。然后,GCC和MSVC之间的布局可能不同。但是,GCC中有一个选项,它使它们兼容:ldrd

    提示:即使如果此解决方案现在可以正常工作,并且它不太可能停止工作,我建议您将代码依赖于此解决方案。

    注意:我在这个答案中只考虑了GCC,clang和MSVC。可能有编译器,这些都不是真的。

答案 1 :(得分:12)

如果

  • endianness不是问题
  • 两个编译器都正确处理打包
  • 两个C实现的类型定义都是准确的(符合标准)。

然后是,“打包结构”是可移植的。

对于我的口味太多“如果”,不要这样做。麻烦不值得出现。

答案 2 :(得分:8)

你可以这样做,或者使用更可靠的替代方案。

对于序列化狂热分子中的核心,那里有CapnProto。这为您提供了一个本地结构来处理,并承诺确保当它通过网络传输并轻松处理时,它仍然有意义的另一端。称之为序列化几乎是不准确的;它旨在尽可能地对结构的内存表示做一点。可能适合移植到M4

有Google协议缓存,这是二进制文件。更臃肿,但相当不错。随附的nanopb(更适合微控制器),但它不能完成整个GPB(我不认为它oneof)。很多人虽然成功地使用它。

某些C asn1运行时足够小,可用于微控制器。我知道this one符合M0。

答案 3 :(得分:8)

你永远不应该在编译域,内存(硬件寄存器,挑选从文件读取的项目或在处理器之间传递数据或在同一处理器上使用不同的软件(在应用程序和内核驱动程序之间))中使用结构。你要求麻烦,因为编译器有一定的自由意愿来选择对齐,然后用户可以通过使用修饰符使其变得更糟。

没有理由假设您可以跨平台安全地执行此操作,即使您使用相同的gcc编译器版本,例如针对不同的目标(编译器的不同版本以及目标差异)。

为了减少失败的几率首先从最大的项目开始(64位然后是32位,16位,然后是最后的任何8位项目)理想情况下对齐32或者最小可能为64,这可能是希望arm和x86做的,但那可以总是改变,默认可以由任何人从源代码构建编译器来修改。

现在,如果这是一个工作安全问题,请确保继续,您可以对此代码进行定期维护,可能需要为每个目标定义每个结构(因此,结构定义的源代码的一个副本) ARM和x86的另一个,或者如果没有立即就会需要这个)。然后,您可以调用每个或每个产品版本来处理代码......很快就会有很少的维护时间炸弹......

如果要在编译域或处理器之间安全地进行相同或不同的体系结构通信,请使用某种大小的数组,字节流,半字流或单词流。显着降低您的失败和维护风险。不要使用结构来挑选那些只能恢复风险和失败的物品。

人们似乎认为这是可以的,因为对相同的目标或系列(或从其他编译器选择派生的编译器)使用相同的编译器或系列,因为您了解语言的规则和实现定义的位置这些领域你最终会遇到差异,有时候你的职业生涯需要几十年,有时需要数周才能完成......它可以在我的机器上运行"问题...

答案 4 :(得分:2)

如果您想要最具可移植性的内容,可以向其中的偏移量声明uint8_t[TELEM1_SIZE]memcpy()的缓冲区,执行字符结转换,例如htons()htonl() (或者像glib中那样的小端等价物)。您可以将它包含在C ++中使用getter / setter方法的类中,或者使用C语言中具有getter-setter函数的结构。

答案 5 :(得分:1)

这是针对算法的伪代码,可以满足您的需求,以确保使用适当的目标操作系统和平台。

如果使用C语言,您将无法使用classestemplates和其他一些内容,但您可以使用preprocessor directives创建根据{{​​1}},架构师struct(s)OS以及最后的CPU-GPU-Hardware Controller Manufacturer {Intel, AMD, IBM, Apple, etc.}字节布局,您需要platform x86 - x64 bit。否则,这里的重点将放在C ++和模板的使用上。

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,或以不同方式执行某些操作的特定操作系统版本,或使用EndianGCC versus MS的{​​{1}}编译器然后可以是需要考虑的少数专业化。您不需要为每种可能的组合指定专门化;这将是太令人生畏和耗时,应该只需要做一些可能发生的罕见案例场景,以确保您始终有目标受众的正确代码说明。同样使__attribute__((__packed__))非常方便的是,如果将这些作为函数参数传递,则可以一次设置多个,因为它们被设计为位标志。因此,如果您想创建一个以此模板结构作为其第一个参数的函数,那么支持OS作为其第二个,您可以将所有可用的OS支持作为位标记传递。

这可能有助于确保这组#pragma pack()根据适当的目标被“打包”和/或正确对齐,并且它将始终执行相同的功能以保持跨不同平台的可移植性。

现在,您可能需要在预处理程序指令之间对不同的支持编译器执行两次此专业化。这样,如果当前编译器是GCC,因为它以一种方式定义结构及其特化,那么Clang在另一个,或MSVC,代码块等等。所以初始设置这个有一点开销,但它应该,可以高度确保在目标机器的指定方案或属性组合中正确使用它。

答案 6 :(得分:1)

这在很大程度上取决于结构是什么,请记住在C ++ struct是一个默认可见性公开的类。

所以你可以继承甚至添加虚拟,这样就可以为你破坏。

如果它是纯数据类(用C ++术语表示标准布局类),则应与packed结合使用。

另外请记住,如果你开始这样做,你可能会遇到编译器严格别名规则的问题,因为你必须查看内存的字节表示(-fno-strict-aliasing是你的朋友)。

注意

话虽如此,我强烈建议不要将其用于序列化。如果您使用工具(即:protobuf,flatbuffers,msgpack或其他),您将获得大量功能:

  • 语言独立
  • rpc(远程过程调用)
  • 数据规范语言
  • 架构/验证
  • 版本

答案 7 :(得分:1)

谈到替代方案并考虑你的问题Tuple-like container for packed data(我没有足够的声誉来评论),我建议看一下Alex Robenko的CommsChampion项目:

  

COMMS是仅限C ++(11)标头,独立于平台的库,它使通信协议的实现变得简单且相对快速。它提供了所有必需的类型和类,使自定义消息的定义以及包装传输数据字段成为类型和类定义的简单声明性语句。这些陈述将指定需要实施的内容。 COMMS库内部处理HOW部分。

由于您正在使用Cortex-M4微控制器,您可能还会感兴趣:

  

COMMS库专门开发用于嵌入式系统,包括裸机系统。它没有使用异常和/或RTTI。它还最大限度地减少了动态内存分配的使用,并提供了在需要时完全排除它的能力,这在开发裸机嵌入式系统时可能是必需的。

亚历克斯提供了一本优秀的免费电子书,名为Guide to Implementing Communication Protocols in C++ (for Embedded Systems),描述了内部。

答案 8 :(得分:-1)

并非总是如此。当您将数据发送到不同的架构师处理器时,您需要考虑Endianness,原始数据类型等。更好地使用ThriftMessage Pack。如果没有,请改为创建自己的Serialize和DeSerialize方法。