C++。不同平台上的结构填充/对齐和布局兼容性的自动检查

C++. Struct padding / alignment on different platforms and atomatic check of layout compatibility

我有嵌入式设备连接到 PC 和一些具有许多字段和自定义类型数组 FixedPoint_t 的大结构 S。 FixedPoint_t 是一个模板化的 POD class,只有一个数据成员,其大小从 char 到 long 不等,具体取决于模板参数。无论如何它通过 static_assert((std::is_pod<FixedPoint_t<0,8,8> >::value == true),"");

如果这个大结构在嵌入式系统和控制 PC 上具有兼容的底层内存表示,那就太好了。这允许将通信协议显着简化为 "set word/byte with offset N to value V" 等命令。假设字节序在两个平台上相同。

我在这里看到 3 个解决方案:

  1. 在两侧使用#pragma 之类的东西。 但是当我将 attribute((packed)) 放入 struct S 声明时,我得到了警告 警告:由于解压缩的非 POD 字段而忽略打包属性。 这是因为 FixedPoint_t 未声明为 packed。 我不想将其声明为打包的,因为这种类型在整个程序中被广泛使用,打包会导致性能下降。

  2. 进行正确的结构序列化。这是不可接受的,因为代码膨胀,额外的 RAM 使用......协议将更加复杂,因为我需要随机访问结构。现在我认为这不是一个选择。

  3. 手动控制填充。我可以添加一些字段,重新排序其他字段......只是为了在两个平台上都没有填充。这会令我满意。但是我需要一个好的方法来编写一个测试来显示我是否存在填充。 我可以将每个字段的 sizeof() 之和与 sizeof(struct) 进行比较。 我可以比较两个平台上的每个结构字段的 offsetof()。 两种变体都够丑了...

你有什么推荐?特别是我对测试中的手动填充控制和自动填充检测感兴趣。

编辑: 在两个平台上比较 sizeof(big struct) 是否足以检测布局兼容性(假设字节顺序相同)?如果填充不同,我认为大小不应该匹配。

编辑2:

//this struct should have padding on 32bit machine
//and has no padding on 8bit
typedef struct
{
    uint8_t f8;
    uint32_t f32;
    uint8_t arr[5];
} serialize_me_t;

//count of members in struct
#define SERTABLE_LEN    3 

//one table entry for each serialize_me_t data member
static const struct {
    size_t width;
    size_t offset;
//    size_t cnt;  //why we need cnt?
} ser_des_table[SERTABLE_LEN] =
    {
        { sizeof(serialize_me_t::f8), offsetof(serialize_me_t, f8)},
        { sizeof(serialize_me_t::f32), offsetof(serialize_me_t, f32)},
        { sizeof(serialize_me_t::arr), offsetof(serialize_me_t, arr)},
    };

void serialize(void* serialize_me_ptr, char* buf)
{
    const char* struct_ptr = (const char*)serialize_me_ptr;
    for(int i=0; i<SERTABLE_LEN; I++)
    {   
        struct_ptr += ser_des_table[i].offset;
        memcpy(buf, struct_ptr, ser_des_table[i].width );        
        buf += ser_des_table[i].width;
    }
}

我强烈建议使用选项 2:

  • 您已保存以备将来更改(新 PCD/ABI、编译器、平台等)
  • 如果考虑周到,代码膨胀可以保持在最低限度。每个方向只需要一个函数。
  • 您可以自动创建所需的 tables/code(半)(我使用 Python)。这样双方就会保持同步。
  • 无论如何,您绝对应该向数据添加 CRC。由于您可能不想在 rx/tx-interrupt 中计算它,因此您无论如何都必须提供一个数组。
  • 直接使用结构很快就会成为维护的噩梦。更糟糕的是,如果其他人必须跟踪此代码。
  • 协议等倾向于重用。如果它是一个具有不同字节顺序的平台,则另一种方法会奏效。

要创建数据结构和 ser/des table,您可以使用 offsetof 获取结构中每种类型的偏移量。如果那个 table 被做成一个包含文件,它就可以在双方使用。您甚至可以创建结构和 table 例如通过 Python 脚本。将其添加到构建过程可确保它始终是最新的,并且您可以避免额外的输入。

例如(在 C 中,只是为了理解):

// protocol.inc

typedef struct {
    uint32_t i;
    uint 16_t s[5];
    uint32_t j;
} ProtocolType;

static const struct {
    size_t width;
    size_t offset;
    size_t cnt;
} ser_des_table[] = {
    { sizeof(ProtocolType.i), offsetof(ProtocolType.i), 1 },
    { sizeof(ProtocolType.s[0]), offsetof(ProtocolType.s), 5 },
    ...
};

如果没有自动创建,我会使用宏来生成数据。可能通过包含文件两次:一次生成结构定义,一次生成 table。这可以通过重新定义中间的宏来实现。

您应该关心有符号整数和浮点数的表示(实现定义,浮点数可能是标准提议的 IEEE754)。

作为 width 字段的替代方法,您可以使用 "type" 代码(例如 char 映射到实现定义的类型。这样您可以添加自定义宽度相同但编码不同的类型(例如 uint32_t 和 IEEE754-float)。这将从物理机器中完全抽象出网络协议编码(最佳解决方案)。注意事项会阻碍您使用通用不会使代码复杂化一点(字面意思)的编码。