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

Emulating a packed structure in portable C

提问人:user16217248 提问时间:6/11/2023 最后编辑:ndimuser16217248 更新时间:6/21/2023 访问量:142

问:

我有以下结构:

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;

为了使在此结构上运行的代码按预期工作,必须紧接在第一个将访问 .每个本质上都是一个控股 8 。使用 GCC,我可以用 和 强制打包结构。但是,这是不可移植的。另一种选择是:node0node((uint16_t *)Octree.node)[-1]Octree.node0Node8unionuint16_t#pragma pack(push)#pragma pack(pop)

  • 假设sizeof(uint64_t *) <= sizeof(uint64_t)
  • 将结构存储为仅 2 个,后跟紧随其后的数据,并且通过按位算术和指针强制转换手动访问成员uint64_tnode

这个选项是非常不切实际的。我还能如何以可移植的方式定义这种“打包”数据结构?还有其他方法吗?

C 数据结构 可移植性

评论

0赞 Eric Postpischil 6/11/2023
为什么在 GCC 中打包结构很重要?除非我数错了,因为有八个字节,否则所有成员都会自然对齐,没有填充,所以打包不会改变任何东西。如果为 4 个字节,则在下一个成员之前将有 4 个字节的填充,因为 和 之间没有填充。您是否担心某些奇怪的平台会插入填充物?另外,您希望打包如何与显式请求交互?#pragma pack(push)uint64_t *uint64_t *alignas(8)node0nodealignas
2赞 Eric Postpischil 6/11/2023
您是否知道,即使成员位于所需位置,C 标准也不能保证访问它时会起作用?某些编译器可能会提供这样的保证,或者根据实际要求,有一些解决方法可能适合您。node((uint16_t *)Octree.node)[-1]
2赞 ndim 6/11/2023
无论以何种方式告诉 C 编译器如何在内存中布局数据结构,您始终可以使用一些 ISO C11 语句来确保内存布局实际上是您所期望的。否则,编译将失败。然后,您可以随时添加更多特定于编译器的内存布局指令。这可能不完全是“可移植的 C”,但它仍然是 C11 加上一些编译器特定的指令,用于编译代码的每个编译器。如果某个类型需要适当的可移植内存布局,则需要另一种编程语言。static_assertoffsetofsizeof
2赞 Eric Postpischil 6/11/2023
Re “还有什么规则不允许?”:指定指针算术的规则 C 2018 6.5.6 8 仅将其定义为数组中的算术(包括最后一个元素之外的结束位置,并将单个对象视为一个元素的数组)。这将创建一个“指针出处”属性;如果具有 C 标准定义的行为,则只能引用数组指向的元素。编译器可能会在优化时使用它来减少指针算术,并且这种减少可能会破坏尝试在实际数组之外使用索引的代码。((uint16_t *)Octree.node)[-1]p[x]p
1赞 Andrew Henle 6/11/2023
“打包结构”和“可移植 C”从根本上是不兼容的概念。

答:

3赞 ndim 6/11/2023 #1

C 语言标准不允许将 的内存布局指定到最后一位。其他语言有(我想到了 Ada 和 Erlang),但 C 没有。struct

因此,如果您想要实际的可移植标准 C,请为数据指定一个 C,并使用指针在特定的内存布局之间进行转换,可能会从大量值组合和分解为大量值以避免字节序问题。编写此类代码容易出错,需要复制内存,并且根据您的用例,它在内存和处理方面都可能相对昂贵。structuint8_t

如果你想通过 C 语言直接访问内存布局,你需要依赖 C 语言规范中没有的编译器功能,因此不是“可移植的 C”。struct

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

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

从 C11 开始,可以使用 和 。static_assertsizeofoffsetof

因此,如果您可以要求 C11(我认为您可以在使用时需要 C11,那么类似以下内容的东西应该可以完成工作,它不是 C99 的一部分,而是 C11 的一部分)。这里的“可移植 C”部分是修复每个平台/编译器的代码,其中编译由于其中一个声明失败而失败。alignasstatic_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_assertstruct

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

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

  • 如问题注释中所述,您需要单独验证未定义的行为是否确实是您在此编译器/平台上所期望的。&(((uint16_t *)octree.node)[-1]) == &octree.node0

    理想情况下,您会找到一种方法将其编写为单独的声明。但是,这样的测试足够快速和简短,您可以在很少但可以保证运行的函数(如全局初始化函数、库初始化函数甚至构造函数)中将此类检查添加到运行时代码中。但是,如果您使用宏进行该检查,请谨慎,因为如果定义了宏,则该运行时检查将变为空操作。static_assertassert()NDEBUG

评论

0赞 anatolyg 6/11/2023
我认为语法是 ,而不是 .但是我用的C不多,所以不能肯定地说。_Static_assertstatic_assert
1赞 ndim 6/11/2023
ISO C11 表示定义 和 宏,并且扩展为 new-in-C11 关键字。assert.hassertstatic_assertstatic_assert_Static_assert