【问题标题】:Dealing with data serialization without violating the strict aliasing rule在不违反严格的别名规则的情况下处理数据序列化
【发布时间】:2026-02-20 06:45:02
【问题描述】:

通常在嵌入式编程中(但不限于)需要序列化一些任意的struct,以便通过某个通信通道发送它或写入某个内存。

示例

让我们考虑一个由N对齐的内存区域中不同数据类型组成的结构:

struct
{
    float a;
    uint8_t b;
    uint32_t c;
} s; 

现在假设我们有一个库函数

void write_to_eeprom(uint32_t *data, uint32_t len);

它将指向要写入的数据的指针作为uint32_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+1uint32_t* 类型的指针,但是它可能指向 uint8_t 字段)。据我了解,这又违反了规则。

可能的解决方案?

用所需类型的数组将有问题的结构包装在一个联合中:

union 
{
    struct_type s;
    uint32_t array[sizeof(struct_type) / 4];
} u;

并将u.array 传递给库函数。

这是正确的方法吗?这是唯一正确的方法吗?还有什么其他方法?

【问题讨论】:

  • 非常简单的解决方案:write_to_eeprom(char const* data, size_t len_times_four).
  • @KerrekSB 正如我所说,write_to_eeprom 是一个库函数,这是我们无法控制的。而且,将float* 转换为char* 是违规行为,不是吗?。
  • @KerrekSB 其实不,不是……任何东西都可以别名为char*
  • 不,您可以将任何对象解释为字符序列。这明确不是别名违规。
  • 是的,您可以将对象中的字节复制到uint32_ 的数组中,然后复制整数。 (或者您可能根本不在乎,只是将您的代码限制在您的一个平台上。)

标签: c strict-aliasing


【解决方案1】:

只是一个注释,我不完全确定,但将uint8_t* 转换为char*(here) 可能并不总是安全的。

无论如何,您的 write 函数的最后一个参数想要什么,要写入的字节数 - 或 uint32_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)

【讨论】:

  • @Eugene Sh.:您对此解决方案有何看法?
  • 我试图避免任何不必要的额外内存操作,所以基本上寻找类型系统唯一的解决方案。谢谢。
  • @EugeneSh.:我认为这个实际上是安全的。我还提到了其他方式,不清楚您要复制到单独的数组元素或对齐所有值(我的回答中的第二个选项)
  • @EugeneSh。当出现这样的代码时,一个好的编译器应该避免“不必要的额外内存操作”。您希望在类型系统中找到的逃生舱口在标准 C 中根本不存在 - 您的选项是 memcpy,通过 unsigned char * 或编译器扩展读取表示。无论如何,您可能最终会编写这样的代码来处理填充和字节序。
  • @zwol 为什么不考虑union 解决方案?
【解决方案2】:

考虑到写入 eeprom 的速度通常比写入普通内存要慢,有时甚至慢很多,因此使用中间缓冲区很少会拖累性能。我意识到这违背了this comment,但我觉得它值得考虑,因为它处理了所有其他 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); 处理@zwol issue

【讨论】: