包装结构是否便携?
我在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会以不同的方式处理它们。
- 这些结构不会被inheritance,也不会inheritance任何东西。
- 这些结构只包含固定大小的整数字段和其他类似的打包结构。 (我以前曾经被花车烧过)
您不应该在编译域中使用结构,针对内存(硬件寄存器,从文件中读取项目或者在处理器之间传递数据,或者在相同的处理器上使用不同的软件(在应用程序和内核驱动程序之间))。 由于编译器有一些自由selectalignment的空间,所以你要求麻烦,然后用户可以通过使用修饰符来使其变得更糟。
不,没有理由假设您可以跨平台安全地执行此操作,即使您针对不同目标(编译器的不同版本以及目标差异)使用相同的gcc编译器版本也是如此。
为了减less失败的可能性从最大的项目开始(64位然后32位的16位,然后是最后任何8位的项目)理想情况下alignment32最低也许64希望arm和x86做的,但总是可以改变为以及任何从源代码构build编译器的默认情况。
现在,如果这是一个工作安全的事情,那么一定要继续前进,你可以定期维护这个代码,可能需要为每个目标定义每个结构(所以ARM的结构定义的源代码的副本和另一个对于x86,或者最终如果不是立即需要的话)。 然后每一个或几个产品发布你被调用来做的代码工作…好一点的维修时间炸弹熄灭…
如果要在编译域或处理器之间安全地进行相同或不同的体系结构之间的通信,请使用一些大小的数组,一个字节stream,一个半字stream或一串字stream。 显着降低您在路上失败和维护的风险。 不要使用结构来挑选那些能够恢复风险和失败的项目。
人们之所以认为这样做是可以的,因为你使用相同的编译器或者系列来对付同一个目标或者家族(或者编译器从其他的编译器select中派生出来),因为你理解语言的规则,最终会遇到不同的情况,有时需要数十年的时间才能完成,有时需要数周时间……这是“在我的机器上工作”的问题。
考虑到提到的平台,是的,包装结构是完全可以使用的。 x86和x86_64总是支持未alignment的访问,与普遍的看法相反,这些平台上的未alignment访问与alignment访问相比( 几乎 )具有相同的访问速度(不存在未alignment访问速度较慢的情况)。 唯一的缺点是访问可能不是primefaces,但我不认为这个问题很重要。 编译器之间有一个协议,打包结构将使用相同的布局。
GCC / clang支持你提到的语法的打包结构。 MSVC有#pragma pack
,可以像这样使用:
#pragma pack(push, 1) struct Sensor1Telemetry { int16_t temperature; uint32_t timestamp; uint16_t voltageMv; // etc... }; #pragma pack(pop)
可能会出现两个问题:
- 所有平台的字节序必须相同(您的MCU必须使用小端)
- 如果你为一个打包结构体成员指定一个指针,并且你在一个不支持未alignment访问的体系结构上(或者使用具有alignment要求的指令,比如
movaps
或ldrd
),那么你可能会使用该指针海湾合作委员会不警告你这个,但铿锵)。
这是GCC的文档:
packed属性指定variables或结构字段应具有最小可能的alignment方式 – 一个字节用于variables
所以GCC 保证不会使用填充。
MSVC:
打包一个类是将其成员直接放在内存中
所以MSVC 保证不会使用填充。
我发现的唯一“危险”领域是位域的使用。 然后GCC和MSVC之间的布局可能会有所不同。 但是,GCC中有一个选项可以使它们兼容: -mms-bitfields
提示:即使这个解决scheme现在可以正常工作,而且它不太可能停止工作,我build议你在这个解决scheme上保持对你的代码的依赖。
注意:我在这个答案中只考虑了GCC,叮当和MSVC。 有编译器可能,这些事情是不正确的。
如果
- 字节顺序不是问题
- 两个编译器都能正确处理包装
- 两个C实现的types定义是准确的(符合标准)。
那么是的,“ 包装结构 ”是便携式的。
对于我的口味太多“如果”,不要这样做。 出现这种麻烦是不值得的。
你可以做到这一点,或使用更可靠的select。
对于这个系列化狂热分子的核心, CapnProto 。 这给你一个本地结构来处理,并承诺确保当它通过networking传输并轻微的工作时,它仍然是有意义的另一端。 称它为序列化几乎是不准确的; 它的目标是尽可能地对结构的内存中表示进行一些处理。 可能会移植到M4
有Google协议缓冲区,这是二进制的。 更膨胀,但不错。 还有nanob(更适合于微控制器),但它并不是GPB的全部(我不认为它是oneof
)。 不过很多人成功使用它。
一些C asn1运行时足够小,可用于微控制器。 我知道这个适合M0。
如果你想要最大限度的可移植的东西,你可以声明uint8_t[TELEM1_SIZE]
和memcpy()
偏移量,并执行endianness转换,比如htons()
和htonl()
在glib中)。 你可以用C ++中的getter / setter方法将其封装在一个类中,或者在C中使用一个getter-setter函数。
它强烈依赖于什么结构,请记住,在C ++ struct
是一个类默认公开的可见性。
所以你可以inheritance,甚至添加虚拟的,所以这可能会打破你的东西。
如果它是一个纯数据类(以C ++术语来说是一个标准的布局类 ),它应该与packed
结合使用。
另外请记住,如果你开始这样做,你可能会遇到编译器严格别名规则的问题,因为你必须查看内存的字节表示( -fno-strict-aliasing
是你的朋友)。
注意
话虽如此,我强烈build议不要使用序列化。 如果你使用这个工具(即:protobuf,flatbuffers,msgpack,或其他),你会得到很多的function:
- 语言独立
- rpc(远程过程调用)
- 数据规范语言
- 模式/validation
- 版本
这里是一个algorithm的伪代码,可能适合您的需要,以确保使用适当的目标操作系统和平台。
如果使用C
语言,您将无法使用classes
, templates
和其他一些东西,但是您可以使用preprocessor directives
根据OS
创build所需的struct(s)
版本,架构师CPU-GPU-Hardware Controller Manufacturer {Intel, AMD, IBM, Apple, etc.}
, platform x86 - x64 bit
,最后是字节布局的末端。 否则,这里的重点将是对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
工作正常,并将其设置为主类的function。 那么对于那些特殊情况,比如不同的Endian
和字节顺序,或者特定的操作系统版本以不同的方式做某事,或者使用__attribute__((__packed__))
和#pragma pack()
GCC versus MS
编译器可以是有几个专业需要考虑。 您不需要为每个可能的组合指定专门化; 这将是太艰巨和耗时,应该只需要做less数情况下,可以发生,以确保你总是有适当的代码指示目标受众。 也使得enums
非常方便的是,如果将这些作为函数parameter passing,则可以一次设置多个,因为它们被devise为位标志。 所以,如果你想创build一个函数,把这个模板结构作为第一个参数,然后支持OS的第二个参数,那么你可以传入所有可用的OS支持位标志。
这可以帮助确保这组packed structures
按照适当的目标被“打包”和/或正确alignment,并且将始终执行相同的function以维持跨不同平台的可移植性。
现在,您可能需要在不同的支持编译器的预处理器指令之间进行两次专业化。 这样,如果当前的编译器是GCC,因为它以一种方式定义了具有专门化的结构,然后在另一个方法中定义了Clang,或者MSVC,Code Blocks等。因此,初始设置起来会有一点开销,但是它应该可以高度确保在目标机器的指定场景或属性组合中正确使用它。
谈到替代scheme,考虑你的问题打包数据的Tuple-like容器 (我没有足够的声望评论),我build议看看Alex Robenko的CommsChampion项目:
COMMS只是C ++(11)头文件,是独立于平台的库,它使得通信协议的实现变得简单和快速。 它提供了所有必要的types和类来定义自定义消息,以及包装传输数据字段,以作为types和类定义的简单声明性语句。 这些陈述将指出需要实施的内容。 COMMS库内部处理HOW部分。
由于您正在使用Cortex-M4微控制器,因此您可能会感到有趣的是:
COMMS库专门用于包括裸机在内的embedded式系统。 它不使用exception和/或RTTI。 它还最大限度地减less了dynamic内存分配的使用,并提供了在需要时将其完全排除在外的function,这在开发裸机embedded式系统时可能需要。
Alex提供了一本非常好的免费电子书,标题为“在C ++中实现通信协议指南”(用于embedded式系统) ,其中描述了内部。
不总是。 当您将数据发送到不同的架构处理器时,您需要考虑有关Endianness,原始数据types等。更好使用Thrift或Message Pack 。 如果不是,请改为创buildSerialize和DeSerialize方法。