• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
指剑问道
Nothing can stop my yearning for freedom
博客园    首页    新随笔    联系   管理    订阅  订阅

Nanopb 编码规则说明

Nanopb 编码规则说明

本文档结合 nanopb 源码(pb_encode.c、pb.h)与 Google Protocol Buffers 编码规范 说明 nanopb 的编码规则。


一、整体编码流程

入口为 pb_encode():按消息描述(fields)中的字段顺序遍历,对每个字段决定是否编码,再按类型写出 Tag + Payload。

  • 编码顺序 = 生成代码里字段描述的顺序(.pb.h 中 FIELDLIST 的顺序),不是按 tag 数值排序。
  • 每个被编码的字段在流中形式为:先 Tag(varint),再 Payload;repeated 会对应多组 Tag+Payload 或一条 packed。

源码位置:pb_encode.c — pb_encode()、encode_field()。


二、Wire 格式:Tag + Payload

每个出现在流中的字段都由 Tag(一个 varint) 和 Payload 组成。

2.1 Tag 的构成

// pb_encode.c
bool pb_encode_tag(pb_ostream_t *stream, pb_wire_type_t wiretype, uint32_t field_number)
{
    pb_uint64_t tag = ((pb_uint64_t)field_number << 3) | wiretype;
    return pb_encode_varint(stream, tag);
}
  • tag = (field_number << 3) | wire_type
  • field_number:proto 中的字段号(如 = 1, = 2)
  • wire_type:决定后续 Payload 的格式(见下表)

2.2 Wire 类型与 C 类型对应

由 pb_encode_tag_for_field() 根据字段的 逻辑类型(PB_LTYPE_xxx) 选择 wire_type:

Wire 类型 值 含义 使用的 C / proto 类型
PB_WT_VARINT 0 变长整数 int32/64, uint32/64, bool, enum, sint32/64
PB_WT_64BIT 1 固定 8 字节 fixed64, sfixed64, double
PB_WT_STRING 2 长度+内容 string, bytes, 子消息, packed repeated
PB_WT_32BIT 5 固定 4 字节 fixed32, sfixed32, float

源码位置:pb_encode.c — pb_encode_tag_for_field();pb.h — pb_wire_type_t。


三、各类型 Payload 的编码规则

3.1 Varint(PB_WT_VARINT)

  • 规则:每字节 7 位有效数据,最高位为延续位(1=还有下一字节,0=结束);小端,低 7 位先写出。
  • 用于:Tag、int32/64、uint32/64、bool、enum,以及“长度”等。

多字节 varint 的含义

  • 长度:一个 varint 由 1~10 个字节组成。
  • 单字节布局
    • bit7:延续位。1 = 后面还有字节,0 = 本字节为最后一个。
    • bit0~bit6:有效载荷(7 位)。
  • 顺序:数值按小端排列——最低 7 位先写,再写次低 7 位,依次类推;解码时按相同顺序把各字节的低 7 位拼成整数。
  • 单字节情况:数值 ≤ 127 时,bit7 = 0,只占一个字节。

示例(数值 300)

  • 二进制:100101100。
  • 按 7 位分组:低 7 位 101100(= 44),高 7 位 0000010(= 2)。
  • 编码为两字节
    • 第一字节 0xAC:低 7 位 = 44,bit7 = 1(继续)→ 10101100。
    • 第二字节 0x02:低 7 位 = 2,bit7 = 0(结束)→ 00000010。
  • 结果:0xAC 0x02。

有符号 sint32/sint64(zigzag)

先做 zigzag 映射,再按无符号 varint 编码:

  • 负数:zigzag = ~(value << 1)
  • 非负:zigzag = value << 1

Bool

0 不编码(见“何时不编码”);非 0 编码为 varint 1。

源码位置

pb_encode.c — pb_encode_varint()、pb_encode_svarint()。

3.2 Fixed32 / Fixed64(PB_WT_32BIT / PB_WT_64BIT)

  • 规则:小端,固定 4 或 8 字节。
  • 用于:float、double、fixed32/64、sfixed32/64。

源码位置:pb_encode.c — pb_encode_fixed32()、pb_encode_fixed64()。

3.3 String / Bytes(PB_WT_STRING)

  • 规则:先写 长度(varint),再写 length 字节的原始内容(string 不含结尾 \0,按 strlen 计)。
  • 空字符串/空 bytes:长度为 0,不写内容。

源码位置:pb_encode.c — pb_encode_string()。

3.4 子消息(Submessage,PB_WT_STRING)

  • 规则:先把子消息单独编码到“ sizing 流”得到 size,再写 varint(size),再写这 size 字节(即整条子消息的编码)。
  • 即:子消息 = 长度(varint) + 整条子消息的二进制,与官方 length-delimited 一致。

源码位置:pb_encode.c — pb_encode_submessage()。

3.5 Repeated 数组:Packed / Unpacked

  • 可 Pack 的类型:数值类(varint、fixed32/64),即 PB_LTYPE <= PB_LTYPE_LAST_PACKABLE。
  • Packed 编码(默认,除非定义 PB_ENCODE_ARRAYS_UNPACKED):
    • 只写 一个 Tag(wire_type = PB_WT_STRING)。
    • Payload = varint(总字节数) + 连续写出所有元素的编码(每个 varint/fixed32/64 按各自规则,不再重复写 tag)。
  • Unpacked(或 string/bytes/message 的 repeated):每个元素单独 Tag + Payload,即多组 [tag][payload]。

源码位置:pb_encode.c — encode_array()。

3.6 Repeated:解码端如何区分 Packed / Unpacked,与 Proto2/Proto3 兼容性

  • 区分方式:解码时先读 Tag,其中的 wire_type 决定当前这一段是 packed 还是单个元素:

    • wire_type == PB_WT_STRING (2):按 packed 处理。先读 varint 得到长度,再在限定长度的子流里循环解码多个 varint/fixed32/fixed64(不再读 tag),直到子流读完。
    • wire_type == PB_WT_VARINT (0) / PB_WT_32BIT (5) / PB_WT_64BIT (1):按 unpacked 处理。当前只解码一个元素,下一个元素由后续的 tag 再带出。

    对应源码(pb_decode.c — decode_static_field(),PB_HTYPE_REPEATED 分支):若 wire_type == PB_WT_STRING 且字段类型可 pack(PB_LTYPE <= PB_LTYPE_LAST_PACKABLE),则走 packed 分支(pb_make_string_substream + 循环 decode_basic_field(..., PB_WT_PACKED, ...));否则走 unpacked 分支,解码一个元素。

  • Proto3 用 repeated 序列化后,Proto2 能否解析:

    • Proto3 的 repeated 标量默认是 packed(一条 tag wire_type=2 + length + N 个值)。
    • 规范要求:对 repeated 标量,解析端必须同时接受 packed 与 unpacked 两种格式。
    • 因此:符合规范的 Proto2 解析器(如 nanopb)可以正确解析 Proto3 发来的 packed repeated;只认 unpacked 的旧实现可能把整段当成 length-delimited 跳过或误解析,导致丢数据或错误。
    • 结论:用 nanopb 等按规范实现的 Proto2 解码端,可以解析 Proto3 的 packed repeated;是否兼容取决于“Proto2 代码”是否实现了对 packed 的支持。
  • 同一 repeated 字段多次出现(混合 packed / unpacked)时解析端怎么处理:
    解码是按流顺序的:主循环每次读一个 Tag(含 wire_type),用 tag 找到对应字段,再按当前这次的 wire_type 处理并追加到该 repeated 数组。

    • 同字段可以多次出现,例如:[tag=5, wire=0, value=1] → 追加 1 个元素;[tag=5, wire=2, length, v2, v3] → 按 packed 解码,追加 2 个元素;再 [tag=5, wire=0, value=4] → 再追加 1 个元素。最终该 repeated 字段就是按出现顺序拼成的列表。
    • 解析端不区分“这段是 packed 还是 unpacked 的约定”,只认当前这段的 wire_type:这段是 wire 2 就按 packed 解并追加多值,这段是 wire 0/1/5 就按单值解并追加一值。因此 packed 和 unpacked 可以在同一条消息里对同一字段混用,解码结果都是按顺序追加到同一个 repeated 数组。

四、何时“不编码”该字段

在 encode_field() 中,下列情况 直接 return true,不写任何字节:

情况 说明
Oneof 当前选中的不是该 oneof 成员(*(pSize) != field->tag)
Optional(有 has_xxx) has_xxx == false
Optional(proto3 singular) pb_check_proto3_default_value(field) 为 true(零值/空串/空 bytes/空子消息等)
Pointer 为 NULL 且非 required(required 会报错)
Repeated count == 0,整个数组不写
Callback 类型 encode 回调为 NULL

源码位置:pb_encode.c — encode_field()、pb_check_proto3_default_value()。

4.1 bool 的特别说明

  • 编码:bool 属于 varint;当值为 false(0) 时,在 proto3 单数字段(SINGULAR)或“按默认值省略”的语义下,不会写进字节流(与 int32=0、空 string 等一致)。只有值为 true(非 0) 时才会编码为 varint 1。
  • 解码:若字节流中没有该字段,解码器会填默认值 false。因此解码端无法区分:
    • “发送方未设置该字段”(未编码)
    • “发送方显式设为 false”(同样未编码)
      两种情况下解码结果都是 false。
  • 若需区分“未设置”与“设为 false”:在 proto3 中对该字段使用 optional bool,重新生成后会得到 has_xxx:has_xxx == false 表示未设置,has_xxx == true && xxx == false 表示显式设为 false。

五、编码规则速查表

项目 规则
顺序 按生成代码中的字段顺序依次处理;流中字段顺序可与定义不同。
Tag 始终为 varint:(field_number << 3) | wire_type。
Varint 7 位/字节,高位为延续位;有符号 sint 先 zigzag 再按无符号 varint。
Fixed32/64 小端,4 或 8 字节。
String/Bytes varint(长度) + 原始字节(无 \0)。
子消息 varint(编码长度) + 整条子消息的编码。
Repeated 标量 默认 packed:一个 Tag(PB_WT_STRING) + varint(总长) + 连续元素编码。
不编码 optional 未设置、oneof 未选中、proto3 零值、空数组、NULL 指针等。

六、Proto2 与 Proto3 的区别(与编码相关)

项目 Proto2 Proto3
required 支持。字段必须出现,解码缺字段会报错。 不支持。语法中无 required,所有单数字段均可省略。
optional 支持。可选字段,可有 has_ 表示是否设置。 支持(3.12+)。optional 表示“有存在性”,生成 has_,可区分未设置与默认值。
单数字段(不写 optional) 若不写 required/optional,历史上有“隐式 optional”等;建议显式写 optional。 Singular:可省略,默认值(0/false/"")通常不编码;解码时缺字段则填默认值。
默认值编码 optional 未设置时不编码;设为默认值时是否编码视实现/语义而定。 单数字段的默认值一般不编码(节省空间);解码端用默认值填充。
存在性(presence) optional 有 has_,可区分“未设置”与“设为默认值”。 普通 singular 无 has_,无法区分“未设置”与“默认值”;用 optional 才有存在性。
repeated 支持;packed 需显式指定(或实现默认 packed)。 支持;标量 repeated 默认 packed 编码。
枚举首值 可为任意值;习惯用 0 作“未设置”。 枚举第一个值必须为 0(用于默认值)。

与 nanopb 编码的对应关系:

  • Proto3 singular:由 pb_check_proto3_default_value() 决定是否编码;bool=false、int=0、空 string 等通常不编码。
  • Proto2 optional / Proto3 optional:由 has_ 决定;has_ 为 false 则不编码。
  • Required:编码端应始终写入;解码端缺字段则报错(nanopb 在 pb_decode_inner 末尾检查 required)。

七、参考

  • Google Protocol Buffers Encoding
  • 本仓库:pb_encode.c、pb_encode.h、pb.h
posted on 2026-03-04 11:23  指剑问道  阅读(60)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3