首页
/ Protobuf Java Lite 的 Editions 适配方案:MessageInfo 紧凑编码与 feature 位设计

Protobuf Java Lite 的 Editions 适配方案:MessageInfo 紧凑编码与 feature 位设计

2026-09-04 13:55:24作者:曹令琨Iris

在 Protocol Buffers 的 Editions 演进中,Java Lite 运行时(主要服务于 Android 场景)面临一个独特挑战:它的解析/序列化不依赖完整的 Descriptor,而是依赖一份高度紧凑、嵌入生成代码的 MessageInfo 编码,而这份编码深度绑定了 proto2/proto3 语法假设。本文基于设计文档 java-lite-for-editions.md,系统讲解该问题如何通过在 flags 中新增 is_edition 位、复用字段条目中已有的 Edition Zero feature 位、以及将 DELIMITED 编码的消息字段按 group 处理来解决,并结合当前仓库源码给出实现落点的验证依据。读完后你将能够理解 Java Lite 编码格式的位级布局、各 Editions feature 与编码位的映射关系,以及被否决的替代方案(MiniDescriptor 编码等)背后的权衡。

背景:Java Lite 的紧凑编码与 is_proto3

Java 的 "Lite" 实现采用一种自定义的 descriptor 嵌入格式,其动机是 Android 等平台上对代码体积和性能的苛刻要求。代码生成器会为每个 message 生成一段 descriptor 风格的信息字符串,写入 RawMessageInfo;运行时将其解码为 MessageSchema,作为 Java Lite 解析与序列化的"类 descriptor"schema。

从当前仓库源码可以看到这一结构:RawMessageInfo.java 的类注释详细描述了紧凑格式——一个 String 对象把字段号、字段类型、hasbits 偏移、oneof 索引等信息编码为 1~3 个 UTF-16 字符组成的整数序列,一个 Object[] 数组存放字段引用、类引用等。生成端则位于 src/google/protobuf/compiler/java/lite/ 下各生成器(如 primitive_field.ccmessage_field.cc),通过 WriteIntToUtf16CharSequence 写入这些整数。

问题在于:该编码中大量逻辑依赖一个 is_proto3 位(即 flags 的低位,flags & 0x1 表示 proto2)来判断语法行为,这对 Editions 不友好。同时还有一个兼容性约束:解析器必须保持向后兼容——同一主版本内,新运行时必须能读懂旧生成的代码,因此任何格式变更都要极为谨慎。

总体思路:复用已有 feature 位,而非另起炉灶

设计文档给出的核心洞察是:好消息是,Editions Zero Features 所定义的绝大多数 feature,在 MessageInfo 的字段条目编码中已经存在对应的位(这些位用于编码"已解析(resolved)"的 feature 值)。因此方案是:

  1. 把仍然读取 is_proto3 的语法判断,迁移为读取这些对应的 feature 位;
  2. 少数其他按语法分支的实现需要合并、统一为单一 syntax 无关代码路径;
  3. 为 Editions Zero 阶段刻意扩展编码格式——未来如果新 editions feature 无法塞进现有位,届时再考虑整体改造 MessageInfo 编码。

编码变更一:flags 增加 is_edition

RawMessageInfo 的 flags 在编码中原本只使用了部分位,设计建议利用未使用位新增 is_edition

[0]: flags, flags & 0x1 = is proto2?, flags & 0x2 = is message?, flags & 0x4 = is edition?

解码后的 ProtoSyntax 相应增加 Editions 选项:

public enum ProtoSyntax
  PROTO2;
  PROTO3;
  EDITIONS;

这一设计在当前仓库中已有实现落点:RawMessageInfo.java 中定义了 IS_PROTO2_BIT = 0x1IS_EDITION_BIT = 0x4,其 getSyntax() 方法按位返回 ProtoSyntax.EDITIONSProtoSyntax.java 枚举也确实包含 PROTO2 / PROTO3 / EDITIONS 三个值,与设计文档一致。

文档同时指出:目前没有必要显式编码 editions 字符串或 feature options 原文——这些已解析的 feature 值会直接编码在各自的字段条目中。

编码变更二:Edition Zero Features 与既有编码位的映射

RawMessageInfo 的字段条目已经通过 GetExperimentalJavaFieldType 编码了大多数已解析的 Editions Zero feature 位,运行时在 fieldTypeWithExtraBits 中读位解码。设计文档给出的逐 feature 处理策略如下(完整继承原文档表格):

Edition Zero Feature 现有编码 变更
features.field_presence kHasHasBit (0x1000) 保持不变。
java.legacy_closed_enum kMapWithProto2EnumValue (0x800) 替换为 kLegacyEnumIsClosedBit。此后该位会对所有 enum 字段生效,而不仅是 enum 类型的 map 值。过渡期内针对旧 gencode 仍需检查 syntax。
features.enum_type EnumLiteGenerator 会为 open enum 在 gencode 中写入 UNRECOGNIZED(-1) 值。这是 enum 级 feature,不编码在 MessageInfo 中。 Editions Zero 中不需要,因为 Java Lite 运行时中 enum 的闭合性由字段级的 java.legacy_closed_enum 逐字段决定(见 Edition Zero Feature: Enum Field Closedness)。修复 Java 的非一致性(non-conformance)问题后才需要使用。注意:若 java.legacy_closed_enum 未设置,该信息会隐式编码在 kLegacyEnumIsClosedBit 中,因为对应的 FieldDescriptor 辅助函数会回退到 EnumDescriptor
features.repeated_field_encoding GetExperimentalJavaFieldTypeForPacked 保持不变。
features.string_field_validation kUtf8CheckBit (0x200) 保持不变。HINT 对 Java 不适用,行为与 MANDATORYNONE 相同。
features.message_encoding 不存在。 按 type group 编码。见下文。

这些位在当前仓库源码中的具体定义可见 internal_helpers.cc

int GetExperimentalJavaFieldType(const FieldDescriptor* field) {
  static const int kMapFieldType = 50;
  static const int kOneofFieldTypeOffset = 51;

  static const int kRequiredBit = 0x100;
  static const int kUtf8CheckBit = 0x200;
  static const int kCheckInitialized = 0x400;
  static const int kLegacyEnumIsClosedBit = 0x800;
  static const int kHasHasBit = 0x1000;
  ...
}

对照文档可以看出,设计中的替换已被采纳:当前源码中 0x800 位的名字就是 kLegacyEnumIsClosedBit(而非文档提到的旧名 kMapWithProto2EnumValue),且对普通 enum 字段与 map enum 值字段都会设置。文档还指出:这些位虽然已在部分地方被正确使用,但解码端仍残留若干按 syntax 分支的用法,应改为检查对应 feature 位;字段条目中还有一些未使用位可供未来字段级 feature 使用,但 Editions Zero 阶段不应需要它们。此外,is_proto3 与 feature 位的解析结果似乎只在 protobuf 内部使用,并未作为公共 API 暴露——这为做这些内部改造降低了兼容风险。

features.message_encoding:DELIMITED 消息字段按 GROUP 编码

对于 features.message_encoding = DELIMITED(即 Editions 中取代 group 语法的特性,其 wire type 为 3/4 而非长度前缀的 2),编译器应在编码 message info 之前就把这类消息字段当作 group 处理。这意味着 GetExperimentalJavaFieldTypeForSingular 应把字段类型编码为 GROUP(17),而不是其实际类型 MESSAGE(9):

int GetExperimentalJavaFieldTypeForSingular(const FieldDescriptor* field) {
  int result = field->type();
  if (result == FieldDescriptor::TYPE_MESSAGE) {
    if (field->isDelimited()) {
      return 17; // GROUP
    }
  }
}

ImmutableMessageFieldLiteGenerator::GenerateFieldInfo 在生成消息字段的 field info 时会调用它。嵌套 message 自身的 MessageInfo 编码无需改动,因为 group 和 message 在该编码中本就相同。

由于每个消息字段独立处理,下面这个 editions 风格的 post-editions proto:

// foo.proto
edition = "tbd"

message Foo {
  message Bar {
    int32 x = 1;
    repeated int32 y = 2;
  }
  Bar bar = 1 [features.message_encoding = DELIMITED];
  Bar baz = 2; // not DELIMITED
}

MessageSchema 眼中将与其 pre-editions 等价物完全一致:

message Foo {
  group Bar = 1 {
    int32 x = 1;
    repeated int32 y = 2;
  }
  Bar baz = 2; // not DELIMITED
}

文档推荐这个方案,正是为了把对编码格式和 group 处理方式的改动降到最低。从当前源码结构看,internal_helpers.cc 中的 GetExperimentalJavaFieldTypeForSingular 已经将 TYPE_GROUP 映射到 17,与 FieldType 中 group 的编号一致;未来的一次破坏性变更中,可以考虑把 FieldType.GROUP 更名为 FieldType.MESSAGE_DELIMITED(保持相同编号与编码)以提升语义清晰度,但现阶段保留现名。

备选做法:新增 kIsMessageEncodingDelimitedBit

文档同时给出了一个次优方案:把 DELIMITED 消息仍按类型 MESSAGE 编码,另用一个未使用位(0x1100)作为 kIsMessageEncodingDelimitedBit,表示"该消息应按 group 方式解析/序列化"。该位需要一路传递到 MessageSchema,后者在 case Message 等分支中对该位特殊处理。之所以不推荐,是因为它需要在多处代码里处理这一特殊状态,改动面更大。

统一按语法分支的代码路径

除 feature 位映射外,还有若干位置按 syntax 分成 proto2/proto3 两条代码路径,大量代码重复,应统一为单一 syntax 无关路径、改按相关 feature 位分支。设计文档列出的具体改造点:

位置 作用 改造内容
ManifestSchemaFactory.newSchema() MessageInfo -> Schema 允许 editions 使用 extensions。
MessageSchema.getSerializedSize() Message -> 序列化大小 合并 getSerializedSizeProto2/3
MessageSchema.writeTo() 序列化 Message 合并 writeFieldsInAscendingOrderProto2/3
MessageSchema.mergeFrom() 解析 Message 合并 parseProto2/3Message
DescriptorMessageInfoFactory.convert() Descriptor -> MessageInfo 合并 convertProto2/3

这些方法位于 MessageSchema.java 等文件中,读者可在当前仓库中搜索上述方法名验证统一后的代码形态。文档还强调:这类代码可读性较差,改造时应通过注释或辅助函数(例如 isEnforceUtf8)标明实际使用的 feature 位;同时 Java Lite 存在不少死代码,许多 syntax 用法可以顺手删除或合并。

替代方案及其权衡

设计文档最后评估了三条替代路径,全部建议推迟到 Editions Zero 之后再与主方案一并复评:

替代方案 1:引入新的向后兼容 MessageInfo 编码

为 editions 增加一套新的向后兼容 MessageInfo 编码:is_edition == true 指示新格式,is_edition == false 指示旧格式。这可以编码现有格式没有空余位承载的额外信息(如 editions 字符串或更多 feature)。当前格式中每个字段条目的可用位是固定数量的,一旦超量或需要消息级 feature,就必须引入新格式;在未来正式放弃 proto2/3 支持的主版本中,可以彻底移除旧格式。

  • 优点:对未来 editions 和 feature 具有前瞻性。
  • 缺点:会阻塞 editions zero 落地,被迫先行完成暂用不到的复杂编码改动;且需要对所有 MessageInfo 解码逻辑做更侵入的更新。

替代方案 2:切换到 MiniDescriptor 编码

Java Lite 可改用 MiniDescriptor 编码规范。它同样为轻量、最小 descriptor 信息而优化,且目前不编码 proto2/proto3 语法,天然更接近 editions 兼容;其 FieldModifier/MessageModifier 位与 Java Lite 的字段 feature 位类似地对应部分 editions zero feature,也可扩展支持更多 feature。据称该格式支持任意数量的 modifier 位,但设计者指出这一点需要复核确认,避免存在类似的 feature 数量硬上限。尚不明确的是:它是否满足 Android 的体积/性能需求,以及它与 Java Lite Schema 的兼容程度。文档还提到 MiniDescriptor 本身也有若干待定改动,应在其稳定前避免引入更多实现方。

  • 优点:统一实现,降低长期维护成本;MiniDescriptor 迟早要为 editions 更新。
  • 缺点:阻塞 editions zero 于不必要的复杂编码变更;对解码逻辑的更新更侵入;可能需要主版本升级才能破坏兼容;存在未知代码体积/schema 兼容性约束需要探索。

替代方案 3:什么都不做

  • 优点:没有工作量。
  • 缺点:Editions 被彻底阻塞,Java Lite 的 proto 只能停留在过去。

小结

该设计方案的精髓在于"最小改动":不引入新格式,而是通过一个空闲 flag 位(0x4 = is edition)区分 editions 生成代码,并把 Editions Zero 的语义完全落到字段条目中早已存在的 feature 位上(presence、enum closedness、packed 编码、UTF-8 校验),仅 features.message_encoding = DELIMITED 采用"编译期即按 GROUP(17)编码"这一低成本的统一手段。当前仓库中 RawMessageInfo.javaProtoSyntax.javainternal_helpers.cc 的实现印证了方案落地。若未来需要承载更多 feature 位或消息级 feature,替代方案 1 与新编码格式的讨论将被重新激活——这正是文档为 Editions Zero 之后预留的演进路径。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384