灵活的数组成员,不必是最后一个

Flexible array member without having to be the last one

我想弄清楚在 C 中是否有一种变通方法可以在结构中拥有一个灵活的数组成员,这不是最后一个。例如,这会产生编译错误:

typedef struct __attribute__((__packed__))
{
    uint8_t         slaveAddr;      /*!< The slave address byte */

    uint8_t         data[];         /*!< Modbus frame data (Flexible Array
                                    Member) */
    
    uint16_t        crc;            /*!< Error check value */

} rtuHead_t;

这不会产生错误:

typedef struct __attribute__((__packed__))
{
    uint8_t         slaveAddr;      /*!< The slave address byte */

    uint8_t         data[];         /*!< Modbus frame data (Flexible Array
                                    Member) */

} rtuHead_t;

typedef struct __attribute__((__packed__))
{
    rtuHead_t       head;           /*!< RTU Slave addr + data */

    uint16_t        crc;            /*!< Error check value */

} rtu_t;

但是不起作用。如果我有一个字节数组:data[6] = {1, 2, 3, 4, 5, 6}; 并将其转换为 rtu_t,那么 crc 成员将等于 0x0302,而不是 0x0605

有没有办法在结构(或结构中的结构)中间使用灵活的数组成员?

灵活数组成员必须是结构的最后一个成员,包含灵活数组成员的结构不能是数组或其他结构的成员。

这种结构的预期用途是动态分配它,为其他成员留出足够的 space 以及灵活成员的 0 个或更多元素。

您试图做的是将一个结构覆盖到一个内存缓冲区上,该缓冲区包含您希望通过访问成员来简单解析的数据包数据。在这种情况下这是不可能的,而且由于对齐和填充问题,通常这样做不是一个好主意。

做你想做的事情的正确方法是编写一个函数,一次反序列化数据包的一个字段,并将结果放在用户定义的结构中。

它不能在 ISO C 中完成。但是...

GCC 有一个扩展,允许在结构中定义可变修改类型。所以你可以这样定义:

#include <stddef.h>
#include <stdio.h>

int main() {
    int n = 8, m = 20;
    struct A {
        int a;
        char data1[n];
        int b;
        float data2[m];
        int c;
    } p;

    printf("offset(a) = %zi\n", offsetof(struct A, a));
    printf("offset(data1) = %zi\n", offsetof(struct A, data1));
    printf("offset(b) = %zi\n", offsetof(struct A, b));
    printf("offset(data2) = %zi\n", offsetof(struct A, data2));
    printf("offset(c) = %zi\n", offsetof(struct A, c));
    return 0;
}

除了一些关于使用非 ISO 功能的警告外,它编译正常并产生预期的输出。

offset(a) = 0
offset(data1) = 4
offset(b) = 12
offset(data2) = 16
offset(c) = 96

问题是此类型只能在 block 范围内定义,因此不能用于将参数传递给其他函数。

但是,它可以传递给嵌套函数,这是 GCC 的另一个扩展。示例:

int main() {
   ... same as above

    // nested function
    int fun(struct A *a) {
        return a->c;
    }
    return fun(&p);
}

灵活的数组成员只能放在结构的末尾。这正是 C 标准 6.7.2.1 对它们的定义:

As a special case, the last element of a structure with more than one named member may have an incomplete array type; this is called a flexible array member.

但对于具体情况,它们也是对错题的错解。错误的问题是“如何在 C 结构中存储可变大小的 Modbus 数据协议帧”? struct 通常最好首先避免。不幸的是,我们的 C 程序员几乎被洗脑了,在每一种情况下都使用 struct,以至于我们不假思索地声明一个。

结构存在各种问题,最显着的是 alignment/padding 问题,它只能通过 gcc __attribute__((__packed__))#pragma pack(1) 等非标准扩展来解决。但即使你使用它们,你最终也会得到一个编译器可能仍然访问未对齐的块——你只告诉它删除填充“我知道我在做什么”。但是,如果您继续访问该内存,则可能是未对齐的访问。

然后是可变大小协议的问题。根据接收到的数据量一遍又一遍地调整内存块的大小实际上除了膨胀和程序执行开销之外并没有太大的效果。这样做节省了多少内存?大约 10 到 100 个字节?即使在低端 MCU 中,这也不算什么。由于您只需要同时在 RAM 中保留几帧。

事实证明,您将不得不分配足够的内存来存储出现过的最大帧,因为您的程序必须处理最坏的情况。然后你也可以静态地分配那么多内存。更快、更安全、更确定。

还有一个问题您似乎没有解决,即网络字节序。 Modbus 使用big endian 并且CRC 是在big endian 中计算的。所以结构末尾的 uint16_t 成员只是坐在那里制造问题。即使您决定使用一些非标准的 GNU VLA 扩展来调整每个帧的大小。

我建议你忘记所有这些结构。

快速、便携且安全的解决方案是简单地使用 uint8_t frame [MAX];,其中 MAX 是一个帧可以拥有的最大字节数。使用一个结构只是为了给帧中的一个特定字节赋予一个变量名,实际上它本身并没有添加任何东西。您真正想要的是拥有可读代码,轻松解释每个字节的作用,而不是原始数据的匿名缓冲区。

这也可以在访问此 uint8_t 数组时使用命名索引(例如 enum)来完成。结构版本 frame.slave_addr = x; 和数组版本 frame[slave_addr] = x; 之间的可读性、用途或生成的机器代码没有区别。 (除了前者可能导致机器代码中的未对齐访问。)

无论如何,您都需要逐字节访问 CRC,因为您首先需要使用 CPU 字节序计算它,然后将其转换为网络字节序。例如:

frame[fcs_high] = checksum >> 8; 
frame[fcs_low]  = checksum & 0xFF;

与结构不同,此代码不依赖于 CPU 字节顺序,它只能在大端 CPUs 上按预期工作。