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为什么不考虑<代码>联合<代码>解决方案?