在可移植 C 中模拟打包结构

2024-04-26

我有以下结构:

typedef struct Octree {
    uint64_t *data;
    uint8_t alignas(8) alloc;
    uint8_t dataalloc;
    uint16_t size, datasize, node0;
    // Node8 is a union type with of size 16 omitted for brevity
    Node8 alignas(16) node[]; 
} Octree;

为了使在此结构上运行的代码按预期工作,有必要node0 紧接着之前首先node这样((uint16_t *)Octree.node)[-1]将访问Octree.node0. Each Node8本质上是一个union持有8uint16_t。通过 GCC,我可以强制打包结构#pragma pack(push) and #pragma pack(pop)。然而,这是不可移植的。另一种选择是:

  • Assume sizeof(uint64_t *) <= sizeof(uint64_t)
  • 将结构存储为 2uint64_t紧接着是node数据,并且通过按位算术和指针强制转换手动访问成员

这个选项非常不切实际。我还能如何定义这个'packed'以可移植的方式进行数据结构?还有其他方法吗?


C 语言标准不允许您指定struct的内存布局一直到最后一位。其他语言可以(例如 Ada 和 Erlang),但 C 不行。

因此,如果您想要实际的便携式标准 C,您可以指定 Cstruct为您的数据,并使用指针从特定的内存布局转换为特定的内存布局,可能由许多组成和分解为uint8_t值以避免字节顺序问题。编写此类代码很容易出错,需要复制内存,并且根据您的用例,它在内存和处理方面可能相对昂贵。

如果您想通过struct在 C 中,您需要依赖 C 语言规范中没有的编译器功能,因此不是“可移植 C”。

因此,下一个最好的办法是使 C 代码尽可能可移植,同时防止为不兼容的平台编译该代码。您定义struct并为每个受支持的平台和编译器组合提供特定于平台/编译器的代码,以及使用struct在每个平台/编译器上都可以是相同的。

现在,您需要确保不可能意外地针对内存布局不完全是您的代码和外部接口所需的平台/编译器进行编译。

从 C11 开始,可以使用static_assert, sizeof and offsetof.

因此,如果您可以要求 C11,类似以下的内容应该可以完成工作(我认为您可以要求 C11,因为您正在使用alignas它不是 C99 的一部分,而是 C11 的一部分)。这里的“可移植 C”部分是修复每个平台/编译器的代码,其中编译由于以下原因之一而失败static_assert声明失败。

#include <assert.h>
#include <stdalign.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

typedef uint16_t Node8[8];

typedef struct Octree {
    uint64_t *data;
    uint8_t alignas(8) alloc;
    uint8_t dataalloc;
    uint16_t size, datasize, node0;
    Node8 alignas(16) node[];
} Octree;

static_assert(0x10 == sizeof(Octree),              "Octree size error");
static_assert(0x00 == offsetof(Octree, data),      "Octree data position error");
static_assert(0x08 == offsetof(Octree, alloc),     "Octree alloc position error");
static_assert(0x09 == offsetof(Octree, dataalloc), "Octree dataalloc position error");
static_assert(0x0a == offsetof(Octree, size),      "Octree size position error");
static_assert(0x0c == offsetof(Octree, datasize),  "Octree datasize position error");
static_assert(0x0e == offsetof(Octree, node0),     "Octree node0 position error");
static_assert(0x10 == offsetof(Octree, node),      "Octree node[] position error");

该系列的static_assert使用字符串化的预处理器宏,可以更简洁地编写声明,减少错误消息的冗余源代码类型struct名称、成员名称,可能还有大小/偏移值。

现在我们已经确定了结构体成员的大小和结构体中的偏移量,但仍有两个方面需要检查。

  • 您的代码期望的整数字节顺序与您的内存结构包含的字节顺序相同。如果字节顺序恰好是“本机”,则您无需检查或处理转换。如果字节序是“大字节序”或“小字节序”,则需要添加一些检查和/或进行转换。

  • 正如问题评论中所述,您需要单独验证未定义的行为&(((uint16_t *)octree.node)[-1]) == &octree.node0实际上就是您期望它在这个编译器/平台上的样子。

    理想情况下,您会找到一种方法将其写为单独的static_assert宣言。但是,这样的测试足够快且短,您可以在很少但保证运行的函数(例如全局初始化函数、库初始化函数,甚至构造函数)中向运行时代码添加这样的检查。如果您使用的话请务必小心assert()该检查的宏,因为如果运行时检查将变成无操作NDEBUG宏已定义。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

在可移植 C 中模拟打包结构 的相关文章

随机推荐