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 个解决方案:
在两侧使用#pragma 之类的东西。
但是当我将 attribute((packed)) 放入 struct S 声明时,我得到了警告
警告:由于解压缩的非 POD 字段而忽略打包属性。
这是因为 FixedPoint_t 未声明为 packed。
我不想将其声明为打包的,因为这种类型在整个程序中被广泛使用,打包会导致性能下降。
进行正确的结构序列化。这是不可接受的,因为代码膨胀,额外的 RAM 使用......协议将更加复杂,因为我需要随机访问结构。现在我认为这不是一个选择。
手动控制填充。我可以添加一些字段,重新排序其他字段......只是为了在两个平台上都没有填充。这会令我满意。但是我需要一个好的方法来编写一个测试来显示我是否存在填充。
我可以将每个字段的 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
)。这将从物理机器中完全抽象出网络协议编码(最佳解决方案)。注意事项会阻碍您使用通用不会使代码复杂化一点(字面意思)的编码。
我有嵌入式设备连接到 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 个解决方案:
在两侧使用#pragma 之类的东西。 但是当我将 attribute((packed)) 放入 struct S 声明时,我得到了警告 警告:由于解压缩的非 POD 字段而忽略打包属性。 这是因为 FixedPoint_t 未声明为 packed。 我不想将其声明为打包的,因为这种类型在整个程序中被广泛使用,打包会导致性能下降。
进行正确的结构序列化。这是不可接受的,因为代码膨胀,额外的 RAM 使用......协议将更加复杂,因为我需要随机访问结构。现在我认为这不是一个选择。
手动控制填充。我可以添加一些字段,重新排序其他字段......只是为了在两个平台上都没有填充。这会令我满意。但是我需要一个好的方法来编写一个测试来显示我是否存在填充。 我可以将每个字段的 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
)。这将从物理机器中完全抽象出网络协议编码(最佳解决方案)。注意事项会阻碍您使用通用不会使代码复杂化一点(字面意思)的编码。