如何联合数组指针?

How to union an array pointer?

我有以下 struct 定义:

typedef struct mb32_packet_t {
    union {
        struct {
            uint16_t preamble;
            uint8_t  system_id;
            uint8_t  message_id;
            uint8_t  reserved;
            uint32_t paylen;
        };
        uint8_t header[9];
    };
    uint8_t *payload;
    uint16_t checksum;
} __attribute__((packed)) mb32_packet_t;

现在我想要另一个 union,这样我就可以获得一个指向整个数据包对象的 uint8_t body[] 指针。像这样:

typedef struct mb32_packet_t {
    union {
        struct {
            union {
                struct {
                    uint16_t preamble;
                    uint8_t  system_id;
                    uint8_t  message_id;
                    uint8_t  reserved;
                    uint32_t paylen;
                };
                uint8_t header[9];
            };
            uint8_t *payload;
            uint16_t checksum;
        };
        uint8_t body[?];
    };
} __attribute__((packed)) mb32_packet_t;

问题在于 payload 字段大小是在运行时动态确定的。除了使 payload 固定大小外,还有其他方法可以做到这一点吗?


我基本上想通过网络套接字发送这种类型的对象,所以我需要一个 uint8_t 指向这种类型对象的指针。在发送对象的时候,我知道整个对象的字节大小。

我通常这样做:

typedef struct 
{
    size_t payload_size;
    double x;
    char y[45];
    /* another members */
    unsigned char payload[];
}my_packet_t;

或者如果您的编译器不支持 FAM

typedef struct 
{
    size_t payload_size;
    double x;
    char y[45];
    /* another members */
    unsigned char payload[0];
}my_packet_t;

所以有效负载可以在header结构的末尾

这是可能的,但对于结构或联合则不行,因为结构或联合的所有部分都需要有一个已知的大小。您仍然可以为 header.

使用结构

因为 body 在已知位置开始 ,您可以使用一个技巧来访问它,就好像它是结构的一部分一样。您可以声明它根本没有大小(早于标准的"flexible array member") or as 0 bytes (a GCC extension)。编译器不会为它分配任何 space,但它仍然会让您使用该名称来引用结构的末尾。诀窍是你可以在结构结束后 malloc 额外的字节,然后使用 body 来引用它们。

typedef struct mb32_packet_t {
    union {
        struct {
            uint16_t preamble;
            uint8_t  system_id;
            uint8_t  message_id;
            uint8_t  reserved;
            uint32_t paylen;
        };
        uint8_t header[9];
    };
    uint8_t body[]; // flexible array member

} __attribute__((packed)) mb32_packet_t;

// This is not valid. The body is 0 bytes long, so the write is out of bounds.
mb32_packet_t my_packet;
my_packet.body[0] = 1;

// This is valid though!
mb32_packet_t *my_packet2 = malloc(sizeof(*my_packet2) + 50);
my_packet2->body[49] = 1;

// Alternative way to calculate size
mb32_packet_t *my_packet3 = malloc(offsetof(mb32_packet_t, body[50]));
my_packet3->body[49] = 1;

灵活数组成员必须在最后。要访问校验和,您需要分配额外的 2 个字节,并使用指针算法。幸运的是,这只是校验和,而不是整个 header.

mb32_packet_t *my_packet = malloc(sizeof(*my_packet) + body_size + 2);
uint16_t *pchecksum = (uint16_t*)&my_packet.body[body_size];
// or
uint16_t *pchecksum = (uint16_t*)(my_packet.body + body_size);

填好header、body和checksum后,因为它们在内存中是连续的,指向header的指针也是指向整个数据包的指针object.

简介

问题不清楚,所以我将讨论三种明显的可能性。

Fixed-length header 后跟 variable-length 有效载荷

为网络或消息服务定义数据包的典型方法是使用 fixed-length header 后跟 variable-length 负载。在现代 C 中,variable-length 有效载荷可以使用灵活的数组成员定义,它是一个结构末尾没有维度的数组:

typedef struct
{
    uint16_t preamble;
    uint8_t  system_id;
    uint8_t  message_id;
    uint8_t  reserved;
    uint32_t paylen;
    uint8_t payload[];
} mb32_packet_t;

这种结构的内存使用 sizeof 提供的基本大小加上有效负载的额外内存分配:

mb32_packet_t *MyPacket = malloc(sizeof *MyPacket + PayloadLength);

当您将这样的 object 传递给需要 char *uint8_t * 或类似类型作为其参数的例程时,您可以简单地转换指针:

SendMyMessage(…, (uint8_t *) MyPacket,…);

该转换 (uint8_t *) MyPacket 提供指向问题中请求的数据包的第一个字节的指针。无需将另一个成员楔入联合或其他声明的结构或层中。

在 C 1999 中引入灵活的数组成员之前,人们会使用两种变通方法之一来创建具有可变数据量的结构。第一,他们可能只定义一个包含一个元素的成员数组并相应地调整 space 计算:

typedef struct
{
    …
    unsigned char payload[1];
} mb32_packet_t;

mb32_packet_t *MyPacket = malloc(sizeof *MyPacket + PayloadLength - 1);

从技术上讲,这违反了 C 标准,因为该结构只包含一个只有一个元素的数组,尽管为它分配了更多 space。然而,编译器不像现在这样积极地分析程序语义和优化,所以它通常是有效的。所以您可能仍然会看到使用该方法的旧代码。

第二,GCC 有自己的 pre-standard 灵活数组成员实现,只是使用数组维度为零而不是省略维度:

typedef struct
{
    …
    unsigned char payload[0];
} mb32_packet_t;

同样,您可能会看到旧代码使用它,但新代码应该使用标准的灵活数组成员。

Fixed-length header 指向 variable-length payload

上面显示的 payload-after-header 形式是我最希望在消息包中使用的数据包形式,因为它与硬件在通过网络发送字节时必须放在“线路上”的内容相匹配:它写入 header 字节后跟数据字节。所以在记忆中这样排列比较方便

但是,您的代码显示了另一种选择:数据不在数据包中,而是被数据包中的指针指向,uint8_t *payload;。我怀疑这是一个错误,网络或消息服务确实需要一个灵活的数组成员,但你显示它后面跟着另一个成员,uint16_t checksum。灵活的数组成员必须是结构中的最后一个成员,因此在有效负载之后还有另一个成员的事实表明这个带有指针的定义对于您正在使用的消息服务可能是正确的。

然而,如果是这样的话,就不可能获得指向完整数据包object的指针,因为object分为两部分。一个包含 header,另一个位于内存中某个不相关的位置,包含数据。

如上所述,您可以使用 (uint8_t) MyPacket 生成指向数据包开头的 uint8_t * 指针。如果消息传递系统知道结构中的指针,那应该可以工作。如果你弄错了数据包结构必须是什么,它将失败。

Fixed-length header 后跟 fixed-length 负载 space

Code elsewhere on Stack Overflow 显示一个 struct mb32_packet_t,有效负载的固定数量 space:

typedef struct mb32_packet_t {
  uint8_t compid;
  uint8_t servid;
  uint8_t payload[248];
  uint8_t checksum;
} __attribute__((packed)) mb32_packet_s;

在这种形式中,数据包始终是固定大小,尽管用于有效负载的 space 的数量可能会有所不同。同样,您将通过强制转换获得指向数据包的 uint8_t * 指针。不需要特别成员。