提问人:user16217248 提问时间:6/11/2023 最后编辑:ndimuser16217248 更新时间:6/21/2023 访问量:142
在可移植 C 语言中模拟打包结构
Emulating a packed structure in portable C
问:
我有以下结构:
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,我可以用 和 强制打包结构。但是,这是不可移植的。另一种选择是:node0
node
((uint16_t *)Octree.node)[-1]
Octree.node0
Node8
union
uint16_t
#pragma pack(push)
#pragma pack(pop)
- 假设
sizeof(uint64_t *) <= sizeof(uint64_t)
- 将结构存储为仅 2 个,后跟紧随其后的数据,并且通过按位算术和指针强制转换手动访问成员
uint64_t
node
这个选项是非常不切实际的。我还能如何以可移植的方式定义这种“打包”数据结构?还有其他方法吗?
答:
C 语言标准不允许将 的内存布局指定到最后一位。其他语言有(我想到了 Ada 和 Erlang),但 C 没有。struct
因此,如果您想要实际的可移植标准 C,请为数据指定一个 C,并使用指针在特定的内存布局之间进行转换,可能会从大量值组合和分解为大量值以避免字节序问题。编写此类代码容易出错,需要复制内存,并且根据您的用例,它在内存和处理方面都可能相对昂贵。struct
uint8_t
如果你想通过 C 语言直接访问内存布局,你需要依赖 C 语言规范中没有的编译器功能,因此不是“可移植的 C”。struct
因此,下一个最好的办法是使您的 C 代码尽可能可移植,同时防止为不兼容的平台编译该代码。您可以为平台和编译器的每个受支持的组合定义并提供特定于平台/编译器的代码,并且使用 的代码在每个平台/编译器上都可以是相同的。struct
struct
现在,您需要确保不会意外地为内存布局不完全是代码和外部接口所需的平台/编译器进行编译。
从 C11 开始,可以使用 和 。static_assert
sizeof
offsetof
因此,如果您可以要求 C11(我认为您可以在使用时需要 C11,那么类似以下内容的东西应该可以完成工作,它不是 C99 的一部分,而是 C11 的一部分)。这里的“可移植 C”部分是修复每个平台/编译器的代码,其中编译由于其中一个声明失败而失败。alignas
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
评论
_Static_assert
static_assert
assert.h
assert
static_assert
static_assert
_Static_assert
评论
#pragma pack(push)
uint64_t *
uint64_t *
alignas(8)
node0
node
alignas
node
((uint16_t *)Octree.node)[-1]
static_assert
offsetof
sizeof
((uint16_t *)Octree.node)[-1]
p[x]
p