可移植的C语言二进制序列化基元

10
据我所知,C库没有提供将数字值序列化为非文本字节流的帮助。如果我错了请纠正我。
目前使用最广泛的工具是POSIX中的htonl等函数。但这些函数有以下缺点:
  • 没有64位支持。
  • 没有浮点数支持。
  • 没有有符号类型的版本。在反序列化时,无符号到有符号的转换依赖于有符号整数溢出,这是未定义行为。
  • 它们的名称没有说明数据类型的大小。
  • 它们依赖于8位字节和精确大小的uint_N_t的存在。
  • 输入类型与输出类型相同,而不是引用一个字节流。
    • 这需要用户执行指针类型转换,可能不安全。
    • 执行了这种类型转换后,用户很可能会尝试以其本机内存布局输出和转换结构体,这是一种糟糕的做法,会导致意外错误。
一个将任意大小的字符序列化为8位标准字节的接口将处于C标准和将八位组设置为传输基本单位的任何标准(ITU?)之间,但是旧标准没有得到修订。现在C11有许多可选组件,可以在像线程这样的组件旁边添加二进制序列化扩展,而不会对现有实现造成要求。这样的扩展是否有用,还是担心非二补码机器只是毫无意义的呢?

1
除非在许多系统上,long 是 64 位的,而且一个具有非 8 位字节的系统不太可能完全匹配 16 位的 short 或 32 位的 long。C 标准故意模棱两可。 - Potatoswatter
@BoPersson 这完全忽略了可移植性这一点。文件格式存在,无论您是否*想要它们。如果您有一个包含IEEE 754浮点数的流,根据RFC 791字节顺序,那么这是符合标准的接口,并且可以合理地期望C与之配合。存储或传输文本可能会使大小翻倍,即使不考虑可移植性的概念,也可能不是可行的解决方案。 - Potatoswatter
1
你的可移植性需要非IEEE系统支持转换到和从非本地浮点格式。这不是我希望在语言标准中看到的内容。 - Bo Persson
1
htonl等函数不会“原地操作”,它们会返回转换后的结果,而不改变输入值。 - David Gelhar
如果一个大型机没有库来反序列化这样的文件,那就完美地证明了我的观点。 - Potatoswatter
显示剩余8条评论
3个回答

6

我从未使用过它们,但我认为谷歌的Protocol Buffers可以满足您的要求。

  • 64位类型、有符号/无符号和浮点类型全部支持
  • 生成的API是类型安全的
  • 序列化可以在流之间完成

这个教程似乎是一个很好的介绍,你可以在这里阅读实际的二进制存储格式。


从他们的网页上:

什么是协议缓冲区?

协议缓冲区是谷歌的语言中立、平台中立、可扩展的机制,用于序列化结构化数据——想象一下 XML,但更小、更快、更简单。你只需定义一次数据的结构,然后就可以使用特殊生成的源代码,轻松地将结构化数据写入和读取到各种数据流中,并使用各种语言(如 Java、C++ 或 Python)。

纯 C 中没有官方实现(只有 C++),但有两个 C 端口可能适合您的需求:

我不知道它们在非 8 位字节存在的情况下表现如何,但应该相对容易找出。


4
在我看来,像htonl()这样的函数的主要缺点是它们只完成了序列化工作的一半。如果你的机器是小端的,它们只会翻转多字节整数中的字节。序列化还有另一个重要的工作就是处理对齐,而这些函数不会处理。
很多CPU无法(高效地)访问存储在地址不是整数倍的内存位置上的多字节整数。这就是为什么永远不要使用结构体覆盖来序列化或反序列化网络数据包的原因。我不确定这是否是您所说的“原地转换”。
我经常与嵌入式系统一起工作,并且我的库中有一些函数,我总是在生成或解析网络数据包(或任何其他I/O:磁盘、RS232等)时使用。
/* Serialize an integer into a little or big endian byte buffer, resp. */
void SerializeLeInt(uint64_t value, uint8_t *buffer, size_t nrBytes);
void SerializeBeInt(uint64_t value, uint8_t *buffer, size_t nrBytes);

/* Deserialize an integer from a little or big endian byte buffer, resp. */
uint64_t DeserializeLeInt(const uint8_t *buffer, size_t nrBytes);
uint64_t DeserializeBeInt(const uint8_t *buffer, size_t nrBytes);

除了这些功能之外,还有一堆宏定义,比如:

#define SerializeBeInt16(value, buffer)     SerializeBeInt(value, buffer, sizeof(int16_t))
#define SerializeBeUint16(value, buffer)    SerializeBeInt(value, buffer, sizeof(uint16_t))
#define DeserializeBeInt16(buffer)          DeserializeBeType(buffer, int16_t)
#define DeserializeBeUint16(buffer)         DeserializeBeType(buffer, uint16_t)

(de)serialize功能按字节读取或写入值,因此不会出现对齐问题。你也不需要担心符号问题。首先,现在所有系统都使用2补码(除了一些ADC可能用其他补码,但是你也不应该使用这些函数)。然而,即使在使用1补码的系统上,它也应该能够工作,因为(据我所知)带符号整数在转换为无符号整数时会转换为2补码(而这些函数接受/返回无符号整数)。
你的另一个论点是它们依赖于8位字节和精确大小的uint_N_t的存在。我的函数也是如此,但我认为这不是个问题(对于我使用的系统和编译器,这些类型总是被定义)。如果你愿意,可以调整函数原型以使用unsigned char代替uint8_t,以及使用long longuint_least64_t代替uint64_t

是的,你基本上描述了我的理想界面... - Potatoswatter
当将有符号整数转换为无符号整数时,它确实会被转换为2的补码。但是,如果要将无符号值从二进制补码表示法转换为机器表示中的有符号数字,则更难避免未定义行为。虽然可以完成这个过程,但它不像强制转换那样简单。 - Doug Currie

1

有趣的是,它似乎更像是htonl的高级封装而不是更便携的替代品。由于某种原因,他们没有用数字命名数据类型。我不明白Linux API如何是可移植的,因为xdr_long需要一个long参数——甚至不是unsigned long。这无法实现RFC中的超整数。此外,为了提高性能,每个数据都放置在四字节代码单元中,这似乎对序列化的作用感到困惑。 - Potatoswatter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接