【问题标题】:Portable C binary serialization primitives可移植的 C 二进制序列化原语
【发布时间】:2012-07-15 01:42:55
【问题描述】:

据我所知,C 库在将数值序列化为非文本字节流方面没有提供任何帮助。如果我错了,请纠正我。

使用最标准的工具是来自 POSIX 的 htonl 等。这些函数都有缺点:

  • 不支持 64 位。
  • 不支持浮点数。
  • 没有签名类型的版本。反序列化时,无符号到有符号的转换依赖于有符号整数溢出,即 UB。
  • 它们的名称并未说明数据类型的大小。
  • 它们取决于 8 位字节和精确大小的 uint_N_t。
  • 输入类型与输出类型相同,而不是引用字节流。
    • 这需要用户执行指针类型转换,这可能是不安全的对齐方式。
    • 执行该类型转换后,用户可能会尝试在其本机内存布局中转换和输出结构,这种不良做法会导致意外错误。

用于将任意大小的char 序列化为 8 位标准字节的接口介于 C 标准之间,该标准并不真正承认 8 位字节,而任何标准(ITU?)将八位字节设置为传输的基本单位。但旧标准没有得到修订。

现在 C11 有许多可选组件,二进制序列化扩展可以与线程之类的东西一起添加,而不需要对现有实现提出要求。

这样的扩展会有用吗,还是担心非二进制补码机器就没有意义?

【问题讨论】:

  • 关于hton?/ntoh?和名称中的大小,实际上名称中的大小。最后一个字母是大小,s 代表shortl 代表long
  • @JoachimPileborg 除了long 在许多系统上是 64 位,非 8 位字节的系统不可能完全匹配 16 位 short 或 32 位 long。 C 标准故意模棱两可。
  • 它可以作为一个外部库用于它可能工作的平台。我看不出这种语言有什么好处。如果我在 IBM 大型机上,接收您的 IEEE 浮点格式是没有用的。文本表示会更容易处理。
  • 您的可移植性要求非 IEEE 系统支持与非本地浮点格式之间的转换。这不是我想要的语言标准。
  • htonl 等人不“就地操作”;他们返回转换后的结果而不改变输入值

标签: c portability binaryfiles endianness htonl


【解决方案1】:

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

  • 64 位类型、有符号/无符号和浮点类型为all supported
  • 生成的 API 是类型安全的
  • 可以对流进行序列化

本教程seems like a pretty good introduction,您可以阅读实际的二进制存储格式here


来自他们的web page

什么是协议缓冲区?

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

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

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

【讨论】:

    【解决方案2】:

    在我看来,像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 函数逐字节读取或写入值,因此不会出现对齐问题。您也不必担心签名。首先,如今所有系统都使用 2s 补码(可能除了一些 ADC,但您不会使用这些功能)。然而,它甚至应该在使用 1s 补码的系统上工作,因为(据我所知)有符号整数在转换为无符号时会转换为 2s 补码(并且函数接受/返回无符号整数)。

    您的另一个论点是它们取决于 8 位字节和精确大小 uint_N_t 的存在。这对我的函数也很重要,但在我看来这不是问题(这些类型总是为我使用的系统及其编译器定义的)。您可以调整函数原型以使用 unsigned char 而不是 uint8_t 和类似 long longuint_least64_t 而不是 uint64_t 如果你愿意的话。

    【讨论】:

    • 是的,你几乎描述了我理想的界面……
    • 确实,有符号整数在转换为无符号整数时会转换为 2s 补码。如果不调用未定义的行为,则在另一个方向上进行转换更加困难:从二进制补码表示中的无符号值到机器表示中的有符号数。可以做到,但不像演员表那么简单。
    【解决方案3】:

    请参阅 xdr 库和 XDR 标准 RFC-1014 RFC-4506

    【讨论】:

    • 很有趣,但它似乎更像是htonl 的高级包装,而不是更便携的替代方案。出于某种原因,他们没有用数字命名数据类型。我看不出 Linux API 是如何可移植的,因为xdr_long 接受long 参数——甚至unsigned long 也不行。这无法实现 RFC 中的hyper integer。另外为了性能,每个数据都放在一个四字节的代码单元中,这似乎对序列化的作用感到困惑。
    猜你喜欢
    • 2011-06-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多