C 在不违反严格别名规则的情况下处理数据序列化
在嵌入式编程(但不限于)中,通常需要序列化一些任意的C 在不违反严格别名规则的情况下处理数据序列化,c,strict-aliasing,C,Strict Aliasing,在嵌入式编程(但不限于)中,通常需要序列化一些任意的结构,以便通过某些通信通道发送或写入某些内存 示例 struct { uint32_t a; uint8_t b; uint32_t c; } s; 让我们考虑一个由不同数据类型组成的结构,在 n>代码>对齐内存区域: struct { float a; uint8_t b; uint32_t c; } s; 现在假设我们有一个库函数 void write_to_eeprom(uint32
结构
,以便通过某些通信通道发送或写入某些内存
示例
struct
{
uint32_t a;
uint8_t b;
uint32_t c;
} s;
让我们考虑一个由不同数据类型组成的结构,在<代码> n>代码>对齐内存区域:
struct
{
float a;
uint8_t b;
uint32_t c;
} s;
现在假设我们有一个库函数
void write_to_eeprom(uint32_t *data, uint32_t len);
它将指向要写入的数据的指针作为uint32\u t*
。现在我们想使用此功能将s
写入eeprom。一种天真的方法是做如下事情
write_to_eeprom((uint32_t*)&s, sizeof(s)/4);
但这显然违反了严格的别名规则
第二个示例
struct
{
uint32_t a;
uint8_t b;
uint32_t c;
} s;
在这种情况下,别名(uint32_t*)&s
没有违反规则,因为指针与指向第一个字段类型的指针兼容,这是合法的。但是库函数的实现可以使其执行一些指针算法来迭代输入数据,而这种算法生成的指针与它们所指向的数据不兼容(例如data+1
是uint32\u t*
类型的指针,但它可能指向uint8\u t字段)。据我所知,这再次违反了规则
可能的解决方案?
将有问题的结构与所需类型的数组合并:
union
{
struct_type s;
uint32_t array[sizeof(struct_type) / 4];
} u;
并将u.array
传递给库函数
这样做对吗?这是唯一正确的方法吗?还有什么其他的方法呢?只是一个提示,我不能完全确定,但是将
uint8*
转换为char*
()并不总是安全的
不管怎样,write函数的最后一个参数需要写入的字节数或uint32\u t
元素数是多少?让我们稍后假设,并假设您希望将结构的每个成员写入独立的整数。您可以这样做:
uint32_t dest[4] = {0};
memcpy(buffer, &s.a, sizeof(float));
memcpy(buffer+1, &s.b, sizeof(uint8_t));
memcpy(buffer+2, &s.c, sizeof(uint32_t));
write_to_eeprom(buffer, 3 /* Nr of elements */);
如果要将结构元素连续复制到整数数组,可以先将结构成员连续复制到字节数组,然后将字节数组复制到
uint32\t
数组。并将字节数作为最后一个参数传递,即-sizeof(float)+sizeof(uint8_t)+sizeof(uint32_t)
考虑到写入eeprom通常比写入普通内存慢,有时慢得多,使用中间缓冲很少会影响性能。我意识到这有悖常理,但我觉得它值得考虑,因为它处理了所有其他C关注点
编写一个没有对齐、别名或大小问题的辅助函数
extern void write_to_eeprom(/* I'd expect const */ uint32_t *data, uint32_t len);
// Adjust N per system needs
#define BYTES_TO_EEPROM_N 16
void write_bytes_to_eeprom(const void *ptr, size_t size) {
const unsigned char *byte_ptr = ptr;
union {
uint32_t data32[BYTES_TO_EEPROM_N / sizeof (uint32_t)];
unsigned char data8[BYTES_TO_EEPROM_N];
} u;
while (size >= BYTES_TO_EEPROM_N) {
memcpy(u.data8, byte_ptr, BYTES_TO_EEPROM_N); // **
byte_ptr += BYTES_TO_EEPROM_N;
write_to_eeprom(u.data32, BYTES_TO_EEPROM_N / sizeof (uint32_t));
size -= BYTES_TO_EEPROM_N;
}
if (size > 0) {
memcpy(u.data8, byte_ptr, size);
while (size % sizeof (uint32_t)) {
u.data8[size++] = 0; // zero fill
}
write_to_eeprom(u.data32, (uint32_t) size);
}
}
// usage - very simple
write_bytes_to_eeprom(&s, sizeof s);
**可以使用
memcpy(u.data32,byte_ptr,BYTES_TO_EEPROM_N)代码>处理。非常简单的解决方案:写入eeprom(字符常量*数据,大小长度乘以四)
@KerrekSB正如我所说,写入eeprom
是一个库函数,我们无法控制。而且,将一个float*
转换为char*
也是一种违反,不是吗?@KerrekSB实际上不是,不是。。。任何对象都可以别名为char*
。不,您可以将任何对象解释为字符序列。这显然不是别名冲突。因此,是的,您可以将对象中的字节复制到uint32\uu
数组中,然后复制int。(或者你不必在意,只需将代码限制在一个平台上。)@Eugene Sh:你对这个解决方案有什么看法?我试图避免任何不必要的额外内存操作,因此基本上只寻找类型系统解决方案。谢谢你。@EugeneSh:我认为这一个实际上是安全的。我还提到了另一种方式,即不清楚是否要复制到单独的数组元素或对齐所有值(我回答中的第二个选项)@EugeneSh。一个好的编译器在使用这样的代码时应该避免“不必要的额外内存操作”。您希望在类型系统中找到的转义图案填充在标准C中根本不存在-您的选项是memcpy
,通过无符号字符*
或编译器扩展读取表示。你可能会编写这样的代码来处理填充和后缀,但是ZWOL为什么不考虑<代码>联合<代码>解决方案?