访问联合原始字节的灵活数组成员

Flexible Array Member to access union raw bytes

在嵌入式平台上。

假设我通过串行线路从从属设备接收字节,其中数据被正确序列化和反序列化以确保字节顺序和数据大小正确。

我真正想要实现的是让 struct test 具有可变大小以允许将来扩展数组成员。


#include <stdio.h>
#include <stdint.h>

struct test
{
    uint32_t a;
    uint32_t b;
    uint32_t c[];
};

union test1
{
    struct test A;
    uint8_t B[256];
};

int main(void)
{
    union test1 test2;

    for (uint32_t i=0; i<256; i++)
    {
        test2.B[i] = i;
    }

    for (size_t i=0; i<(sizeof(test2.B)/sizeof(uint32_t))-2; i++)
        printf("Test: 0x%08X\n", test2.A.c[i]);
}

在 C89 和 C99 的原始出版物中,写入 union 的一个成员并从另一个成员读取具有实现定义的行为。在 TC1 到 C99 中,它被更改为 unspecified 行为。无论哪种方式,实际含义都是相同的:您 可以 写信给工会的一个成员,然后从另一个成员那里读回,而不用担心恶魔从你的鼻子里飞出来;该标准不会告诉您您将获得什么,但是根据实施知识,它应该是可预测的。

话虽如此,您很可能 运行 遇到 struct test 中的填充、字节顺序不一致等问题。其中一些问题可以通过使用 stdint.h 固定宽度类型而不是 int 并尽可能使用无符号类型来缓解。我还强烈建议您根据外部协议所处的任何明确的字节序编写一个显式转换函数,例如

static int32_t
be32_to_cpu(const unsigned char *p)
{
    uint32_t x = 0;
    x |= ((uint32_t)p[0]) << 24;
    x |= ((uint32_t)p[1]) << 16;
    x |= ((uint32_t)p[2]) <<  8;
    x |= ((uint32_t)p[3]) <<  0;
    return (int32_t)x;
}

并从具有手动计算偏移量的 unsigned char 缓冲区手动复制,例如

struct test
{
    int32_t a;
    int32_t b;
    int32_t c[62];
}

void convert_block(struct test *restrict out,
                   const unsigned char *restrict buf)
{
    out->a = be32_to_cpu(&buf[0]);
    out->b = be32_to_cpu(&buf[4]);
    for (int i = 0; i < 62; i++)
        out->c[i] = be32_to_cpu(&buf[4 * (i+2)]);
}

现代编译器将识别 be32_to_cpu 中的习语并生成最佳代码。对于little-endian,只需颠倒移位顺序即可。请注意,您必须 assemble 无符号变量中的值,然后转换为有符号变量,因为移入符号位具有未定义的行为。

如果您的有线协议发送可变大小的数据包,那么大概有一个大小字段,您将需要使用它来了解何时停止读取以及缓冲区的大小:

struct test
{
    uint32_t size;
    int32_t b;
    int32_t c[]; /* SIZE/4 - 2 values */
};

struct test *
read_block(int fd)
{
    char b1[4];
    if (read(fd, b1, 4) < 4) abort();
    uint32_t size = be32u_to_cpu(b1);

    char b2[size - 4];
    if (read(fd, b2, size - 4) < size - 4) abort();

    struct test *out = malloc(size);
    out->size = size;
    out->b = be32s_to_cpu(&b2[0]);
    for (int i = 0; i < size/4 - 2; i++)
        out->c[i] = be32s_to_cpu(&b2[(i+1)*4]);

    return out;
}

正确处理错误和短读留作练习。

需要考虑的一些事项:

  • 对齐。您不能可移植地假设结构或联合没有填充字节。理论上,一些系统对 int 大小的对齐要求不明确,可能会导致结构内部填充字节。

    由于该场景主要是理论上的,您可以通过添加

    来确保它不会发生
    _Static_assert(sizeof(struct test) == sizeof(int)+sizeof(int),
               "Padding detected!");
    
  • Endianess,如问题中所述,是一个真正的问题,必须在某个地方处理。

  • Signed int 在联合中或嵌入式系统中的任何其他地方可能没有任何意义。这些可能会以多种方式造成破坏,但不会在发布的任何代码中造成破坏。它们应该替换为 stdint.h.
  • 中的确定性大小和符号类型
  • 输入双关语。类型双关很好,尽管是实现定义的。我不明白为什么您的代码会在常规二进制补码系统上引起问题。但是从理论上讲,您可能会遇到不使用二进制补码但实现填充位、陷阱位等的奇异系统的可移植性问题。我不会过分担心这种几乎不存在的系统的可移植性。
  • C 标准。您显然无法将灵活的数组成员代码移植到 C90 系统。它可能会在那里编译但会调用未定义的行为。此外,标准委员会的一些弱智分支在 C11 中将 stdint.h 设为可选。不过我不会担心这个。

总的来说,只要您在某处处理字节顺序并摆脱 int.

,代码就可以很好地移植到所有有用的系统。