简体   繁体   English

打包结构是否可移植?

[英]Are packed structs portable?

I have some code on a Cortex-M4 microcontroller and'd like to communicate with a PC using a binary protocol.我在 Cortex-M4 微控制器上有一些代码,并且想使用二进制协议与 PC 进行通信。 Currently, I'm using packed structs using the GCC-specific packed attribute.目前,我正在使用使用 GCC 特定的packed属性的打包结构。

Here is a rough outline:这是一个粗略的概述:

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

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

My question is:我的问题是:

  • Assuming that I use the exact same definition for the TelemetryPacket struct on the MCU and the client app, will the above code be portable accross multiple platforms?假设我对 MCU 和客户端应用程序上的TelemetryPacket结构使用完全相同的定义,上面的代码是否可以跨多个平台移植? (I'm interested in x86 and x86_64, and need it to run on Windows, Linux and OS X.) (我对 x86 和 x86_64 感兴趣,需要它在 Windows、Linux 和 OS X 上运行。)
  • Do other compilers support packed structs with the same memory layout?其他编译器是否支持具有相同内存布局的打包结构? With what syntax?用什么语法?

EDIT :编辑

  • Yes, I know packed structs are non-standard, but they seem useful enough to consider using them.是的,我知道打包结构是非标准的,但它们似乎足够有用,可以考虑使用它们。
  • I'm interested in both C and C++, although I don't think GCC would handle them differently.我对 C 和 C++ 都感兴趣,尽管我认为 GCC 不会以不同的方式处理它们。
  • These structs are not inherited and don't inherit anything.这些结构不是继承的,也不继承任何东西。
  • These structs only contain fixed-size integer fields, and other similar packed structs.这些结构仅包含固定大小的整数字段和其他类似的打包结构。 (I've been burned by floats before...) (我之前被花车烫过……)

Considering the mentioned platforms, yes, packed structs are completely fine to use.考虑到上述平台,是的,打包结构完全可以使用。 x86 and x86_64 always supported unaligned access, and contrary to the common belief, unaligned access on these platforms has ( almost ) the same speed as aligned access for a long time (there's no such thing that unaligned access is much slower). x86 和 x86_64 始终支持非对齐访问,与普遍看法相反,这些平台上的非对齐访问在很长一段时间内( 几乎)与对齐访问的速度相同(没有非对齐访问慢得多的情况)。 The only drawback is that the access may not be atomic, but I don't think it matters in this case.唯一的缺点是访问可能不是原子的,但我认为在这种情况下并不重要。 And there is an agreement between compilers, packed structs will use the same layout.并且编译器之间有一个约定,packed structs 将使用相同的布局。

GCC/clang supports packed structs with the syntax you mentioned. GCC/clang 支持使用您提到的语法打包结构。 MSVC has #pragma pack , which can be used like this: MSVC 有#pragma pack ,可以这样使用:

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

Two issues can arise:可能会出现两个问题:

  1. Endianness must be the same across platforms (your MCU must be using little-endian)跨平台的字节序必须相同(您的 MCU 必须使用小字节序)
  2. If you assign a pointer to a packed struct member, and you're on an architecture which doesn't support unaligned access (or use instructions which have alignment requirements, like movaps or ldrd ), then you may get a crash using that pointer (gcc doesn't warn you about this, but clang does).如果您分配一个指向打包结构成员的指针,并且您使用的架构不支持未对齐访问(或使用具有对齐要求的指令,如movapsldrd ),那么您可能会使用该指针崩溃( gcc 不会就此警告您,但 clang 会警告您)。

Here's the doc from GCC:这是 GCC 的文档:

The packed attribute specifies that a variable or structure field should have the smallest possible alignment—one byte for a variable Packed 属性指定变量或结构体字段应具有尽可能小的对齐方式——一个字节用于变量

So GCC guarantees that no padding will be used.所以 GCC保证不会使用填充。

MSVC: MSVC:

To pack a class is to place its members directly after each other in memory打包一个类就是将它的成员直接放在内存中

So MSVC guarantees that no padding will be used.所以 MSVC保证不会使用填充。

The only "dangerous" area I've found, is the usage of bitfields.我发现的唯一“危险”区域是位域的使用。 Then the layout may differ between GCC and MSVC.那么 GCC 和 MSVC 之间的布局可能会有所不同。 But, there's an option in GCC, which makes them compatible: -mms-bitfields但是,GCC 中有一个选项,使它们兼容: -mms-bitfields


Tip: even, if this solution works now, and it is highly unlikely that it will stop working, I recommend you keep dependency of your code on this solution low.提示:即使这个解决方案现在有效,而且它不太可能停止工作,我建议您保持代码对这个解决方案的依赖程度较低。

Note: I've considered only GCC, clang and MSVC in this answer.注意:我在这个答案中只考虑了 GCC、clang 和 MSVC。 There are compilers maybe, for which these things are not true.也许有编译器,但这些事情并非如此。

If如果

  • endianness is not an issue字节序不是问题
  • both compilers handle packing correctly两个编译器都正确处理打包
  • the type definitions on both C implementations are accurate (Standard compliant).两个 C 实现的类型定义都是准确的(符合标准)。

then yes, " packed structures " are portable.那么是的,“打包结构”是可移植的。

For my taste too many "if"s, do not do this.对于我的口味太多“如果”,请不要这样做。 It's not worth the hassle to arise.出现这种麻烦是不值得的。

You should never use structs across compile domains, against memory (hardware registers, picking apart items read from a file or passing data between processors or the same processor different software (between an app and a kernel driver)).您永远不应该跨编译域、针对内存(硬件寄存器、从文件中读取读取的项目或在处理器或相同处理器的不同软件之间(在应用程序和内核驱动程序之间)之间传递数据)使用结构。 You are asking for trouble as the compiler has somewhat free will to choose alignment and then the user on top of that can make it worse by using modifiers.您是在自找麻烦,因为编译器有一定的自由意志来选择对齐方式,然后在此之上的用户可以通过使用修饰符使情况变得更糟。

No there is no reason to assume you can do this safely across platforms, even if you use the same gcc compiler version for example against different targets (different builds of the compiler as well as the target differences).不,没有理由假设您可以跨平台安全地执行此操作,即使您使用相同的 gcc 编译器版本,例如针对不同的目标(编译器的不同版本以及目标差异)。

To reduce your odds of failure start with the largest items first (64 bit then 32 bit the 16 bit then lastly any 8 bit items) Ideally align on 32 minimum perhaps 64 which one would hope arm and x86 do, but that can always change as well as the default can be modified by whomever builds the compiler from sources.为了降低失败的几率,首先从最大的项目开始(64 位,然后是 32 位,然后是 16 位,最后是任何 8 位项目)理想情况下,在 32 位最小可能是 64 位上对齐,人们希望 arm 和 x86 能做到这一点,但这总是可以改变的以及从源代码构建编译器的任何人都可以修改默认值。

Now if this is a job security thing, sure go ahead, you can do regular maintenance on this code, likely going to need a definition of each structure for each target (so one copy of the source code for the structure definition for ARM and another for x86, or will need this eventually if not immediately).现在,如果这是一个工作安全问题,当然可以继续,您可以对此代码进行定期维护,可能需要为每个目标定义每个结构(因此,ARM 结构定义的源代码副本和另一个对于 x86,或者如果不是立即,最终将需要它)。 And then every or every few product releases you get to be called in to do work on the code...Nice little maintenance time bombs that go off...然后每一个或每隔几个产品发布你都会被调用来处理代码......漂亮的小维护定时炸弹......

If you want to safely communicate between compile domains or processors the same or different architectures, use an array of some size, a stream of bytes a stream of halfwords or a stream of words.如果您想在相同或不同架构的编译域或处理器之间安全地通信,请使用一定大小的数组、字节流、半字流或字流。 Significantly reduces your risk of failure and maintenance down the road.显着降低您在路上发生故障和维护的风险。 Do not use structures to pick apart those items that just restores the risk and failure.不要使用结构来分离那些只会恢复风险和故障的项目。

The reason why folks seem to think this is okay because of using the same compiler or family against the same target or family (or compilers derived from other compilers choices), as you understand the rules of the language and where the implementation defined areas are you will eventually run across a difference, sometimes it takes decades in your career, sometimes it takes weeks...Its the "works on my machine" problem...人们似乎认为这没问题的原因是因为对相同的目标或系列(或从其他编译器选择派生的编译器)使用相同的编译器或系列,因为您了解语言的规则以及实现定义的区域在哪里最终会遇到差异,有时在您的职业生涯中需要数十年,有时需要数周......这是“在我的机器上工作”的问题......

You could do that, or use a more reliable alternative.您可以这样做,或者使用更可靠的替代方法。

For the hard core amongst the serialisation fanatics out there, there's CapnProto .对于那里的序列化狂热分子中的核心,有CapnProto This gives you a native structure to deal with, and undertakes to ensure that when it's transferred across a network and lightly worked on, it'll still make sense the other end.这为您提供了一个本地结构来处理,并保证当它通过网络传输并轻松处理时,它仍然在另一端有意义。 To call it a serialisation is nearly inaccurate;将其称为序列化几乎是不准确的。 it aims to do a little as possible to the in-memmory representation of a structure.它旨在对结构的内存表示做尽可能少的工作。 Might be amenable to porting to an M4可能适合移植到 M4

There's Google Protocol Buffers, that's binary.有 Google Protocol Buffers,它是二进制的。 More bloaty, but pretty good.比较臃肿,但还不错。 There's the accompanying nanopb (more suited to microcontrollers), but it doesn't do the whole of GPB (I don't think it does oneof ).有随附的 nanopb (更适合微控制器),但它并没有完成整个 GPB(我认为它没有oneof )。 Many people use it successfully though.不过,很多人都成功地使用了它。

Some of the C asn1 runtimes are small enough for use on micro controllers.一些 C asn1 运行时足够小,可以在微控制器上使用。 I know this one fits on M0.我知道这个适合 M0。

If you want something maximally portable, you can declare a buffer of uint8_t[TELEM1_SIZE] and memcpy() to and from offsets within it, performing endianness conversions such as htons() and htonl() (or little-endian equivalents such as the ones in glib).如果你想要最大程度的可移植性,你可以声明一个缓冲区uint8_t[TELEM1_SIZE]memcpy()到其中的偏移量,执行字节序转换,如htons()htonl() (或小端等价物,如那些口齿伶俐)。 You could wrap this in a class with getter/setter methods in C++, or a struct with getter-setter functions in C.你可以用 C++ 中的 getter/setter 方法将它包装在一个类中,或者用 C 中的 getter-setter 函数包装一个结构体。

It strongly depends on what struct is, bear in mind that in C++ struct is a class with default visibility public.这在很大程度上取决于结构是什么,请记住,在 C++ 中struct是一个具有默认可见性 public 的类。

So you can inherit and even add virtual to this so this could break things for you.所以你可以继承甚至添加虚拟到它,这样这可能会给你带来麻烦。

If it is a pure data class (in C++ terms a standard layout class ) this should work in combination with packed .如果它是一个纯数据类(在 C++ 术语中是一个标准布局类),这应该与packed .

Also bear in mind, that if you start doing this you might get problems with strict aliasing rules of your compiler, because you will have to look at the byte representation of your memory ( -fno-strict-aliasing is your friend).还要记住,如果您开始这样做,您可能会遇到编译器严格别名规则的问题,因为您必须查看内存的字节表示( -fno-strict-aliasing是您的朋友)。

Note笔记

That being said I would strongly advise against using that for serialization.话虽如此,我强烈建议不要将其用于序列化。 If you use tools for this (ie: protobuf, flatbuffers, msgpack, or others) you get a ton of features:如果您为此使用工具(即:protobuf、flatbuffers、msgpack 或其他),您将获得大量功能:

  • language independence语言独立
  • rpc (remote procedure call) rpc(远程过程调用)
  • data specification languages数据规范语言
  • schemas/validation模式/验证
  • versioning版本控制

Speaking about alternatives and considering your question Tuple-like container for packed data (for which I don't have enough reputation to comment on), I suggest having a look at Alex Robenko's CommsChampion project:谈到替代方案并考虑您的问题Tuple-like container forpacked data (我没有足够的声誉对此发表评论),我建议看看 Alex Robenko 的CommsChampion项目:

COMMS is the C++(11) headers only, platform independent library, which makes the implementation of a communication protocol to be an easy and relatively quick process. COMMS 只是 C++(11) 头文件,独立于平台的库,这使得通信协议的实现成为一个简单且相对快速的过程。 It provides all the necessary types and classes to make the definition of the custom messages, as well as wrapping transport data fields, to be simple declarative statements of type and class definitions.它提供了所有必要的类型和类,使自定义消息的定义以及包装传输数据字段成为类型和类定义的简单声明性语句。 These statements will specify WHAT needs to be implemented.这些声明将指定需要实施的内容。 The COMMS library internals handle the HOW part. COMMS 库内部处理 HOW 部分。

Since you're working on a Cortex-M4 microcontroller, you may also find interesting that:由于您正在研究 Cortex-M4 微控制器,您可能还会发现以下有趣之处:

The COMMS library was specifically developed to be used in embedded systems including bare-metal ones. COMMS 库专门开发用于嵌入式系统,包括裸机系统。 It doesn't use exceptions and/or RTTI.它不使用异常和/或 RTTI。 It also minimises usage of dynamic memory allocation and provides an ability to exclude it altogether if required, which may be needed when developing bare-metal embedded systems.它还最大限度地减少了动态内存分配的使用,并提供了在需要时完全排除它的能力,这在开发裸机嵌入式系统时可能需要。

Alex provides an excellent free ebook titled Guide to Implementing Communication Protocols in C++ (for Embedded Systems) which describes the internals. Alex 提供了一本优秀的免费电子书,标题为Guide to Implementing Communication Protocols in C++ (for Embedded Systems) ,其中描述了内部结构。

Here is pseudo code towards an algorithm that may fit your needs to ensure the use with the proper target OS and platform.这是针对可能适合您的需要的算法的伪代码,以确保在正确的目标操作系统和平台上使用。

If using the C language you will not be able to use classes , templates and a few other things, but you can use preprocessor directives to create the version of your struct(s) you need based on the OS , the architect CPU-GPU-Hardware Controller Manufacturer {Intel, AMD, IBM, Apple, etc.} , platform x86 - x64 bit , and finally the endian of the byte layout.如果使用的是C语言,你将不能够使用classestemplates和其他一些事情,但你可以使用preprocessor directives来创建你的版本struct(s)你需要基于对OS ,建筑师CPU-GPU-Hardware Controller Manufacturer {Intel, AMD, IBM, Apple, etc.}platform x86 - x64 bit ,最后是字节布局的endian Otherwise the focus here would be towards C++ and the use of templates.否则这里的重点将是 C++ 和模板的使用。

Take your struct(s) for example:以您的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__));

You could template these structs as such:您可以这样模板化这些结构:

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

With these enum identifiers you could then use class template specialization to set the up this class to its needs depending on the above combinations.有了这些enum标识符,你可以再使用class template specialization此设置了class依赖于上述组合的需求。 Here I would take all the common cases that would seem to work fine with default class declaration & definition and set that as the main class's functionality.在这里,我将采用似乎可以正常使用default class declaration & definition所有常见情况,并将其设置为主类的功能。 Then for those special cases, such as different Endian with byte order, or specific OS versions doing something in a different way, or GCC versus MS compilers with the use of __attribute__((__packed__)) versus #pragma pack() can then be the few specializations that need to be accounted for.然后对于那些特殊情况,例如具有字节顺序的不同Endian ,或以不同方式做某事的特定操作系统版本,或者使用__attribute__((__packed__))#pragma pack() GCC versus MS编译器可以是需要考虑的几个专业。 You shouldn't need to specify a specialization for every possible combination;您不需要为每个可能的组合指定一个专业化; that would be too daunting and time consuming, should only need to do the few rare case scenarios that can occur to make sure you always have proper code instructions for the target audience.这太令人生畏和耗时,应该只需要执行可能发生的少数罕见情况,以确保您始终为目标受众提供正确的代码说明。 What also makes the enums very handy too is that if you pass these as a function argument, you can set multiple ones at a time as they are designed as bit flags.同样使enums非常方便的是,如果您将它们作为函数参数传递,您可以一次设置多个,因为它们被设计为位标志。 So if you want to create a function that takes this template struct as its first argument, then supported OS's as its second you could then pass in all available OS support as bit flags.因此,如果您想创建一个将这个模板结构作为其第一个参数的函数,然后将支持的操作系统作为其第二个参数,那么您可以将所有可用的操作系统支持作为位标志传入。

This may help to ensure that this set of packed structures is being "packed" and or aligned correctly according to the appropriate target and that it will always perform the same functionality to maintain portability across different platforms.这可能有助于确保这组packed structures根据适当的目标被“打包”和/或正确对齐,并且它将始终执行相同的功能以保持跨不同平台的可移植性。

Now you may have to do this specialization twice between the preprocessor directives for different supporting compilers.现在,您可能需要在不同支持编译器的预处理器指令之间进行两次这种专门化。 Such that if the current compiler is GCC as it defines the struct in one way with its specializations, then Clang in another, or MSVC, Code Blocks etc. So there is a little overhead to get this initially set up, but it should, could highly ensure that it is being properly used in the specified scenario or combination of attributes of the target machine.这样,如果当前编译器是 GCC,因为它以一种方式定义结构及其专业化,然后在另一种方式中定义 Clang,或 MSVC、代码块等。 因此,初始设置有一点开销,但它应该,可以高度确保它在目标机器的指定场景或属性组合中被正确使用。

Not always.不总是。 When you send data to different architect processor, you need to consider about Endianness, primitive data type, etc. Better to use Thrift or Message Pack .当您将数据发送到不同架构处理器时,您需要考虑 Endianness、原始数据类型等。最好使用ThriftMessage Pack If not, create yourself Serialize and DeSerialize methods instead.如果没有,请创建自己的 Serialize 和 DeSerialize 方法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM