首页
/ Protobuf Editions Evolution:Edition 的全序定义、后端默认值机制与未来 Edition 问题

Protobuf Editions Evolution:Edition 的全序定义、后端默认值机制与未来 Edition 问题

2026-09-05 21:31:57作者:蔡丛锟

本文围绕 Protocol Buffers 仓库中 Editions 设计文档《Edition Evolution》(docs/design/editions/edition-evolution.md)展开,讲清楚三个核心问题:Edition 之间如何定义全序(total order)以便第三方后端做版本比较、各语言后端如何向 protoc 上报自己各 Edition 的特性(feature)默认值、以及"Edition 引入了旧版本无法解析的新特性"这一未来兼容性问题如何处理。读完本文,你可以理解 protobuf 用 Edition 机制把 .proto 文件"拉向未来"的设计取舍,并能在源码中定位到这些设计落地的具体证据(如 Edition 枚举 与 C++ 后端内嵌的 FeatureSetDefaults 序列化数据)。

背景:Edition 与 Feature 是什么

Protobuf Editions 为 protobuf 文件提供了一条演进路径:把文件升级到更高的 edition,而不是反复发明新语法。Edition 机制建立在 feature(特性)之上——feature 是附着在 .proto 文件语法元素上的开关,可以显式设置,也可以由 edition 隐含。feature 分两类:

  • 作用于 protoc 前端行为的 proto: feature(例如 proto:closed_enums);
  • 作用于特定后端的 feature(例如 cpp:string_view)。

《Edition Evolution》正是围绕这套机制回答两个问题(引自原文档 Overview 一节):

  1. 什么时候该创建一个 edition?
  2. 后端如何把它们的默认值告知 protoc

需要注意:该文档中"Total Ordering of Editions"一节已被后续的 Edition Naming 设计文档大幅取代(原文以 NOTE 明确标注)。阅读时应把下文的全序定义理解为早期方案的历史脉络,而"后端上报默认值"与"未来 Edition"两节的思路仍是当前实现的重要基础。

Edition 的总序(Total Order)定义

FileDescriptorProto.edition 字段是字符串而非整数。原因为文档所述:避免"每年必须铸造多个 edition"的尴尬——即使已经发布了 edition = "2022";,紧急情况下仍可发布 edition = "2022b"; 这样的修订版。

字符串 edition 带来的直接问题是比较:第三方后端(文档以 Haskell 后端为例)并不掌握 protobuf 核心团队的发版节奏,甚至可能自行铸造 edition。为了让后端判断"某个 proto 是否晚于我引入默认值的 edition",protobuf 定义了 edition 上的全序

  • edition 字符串按 '.' 切分成若干分量;
  • 每个分量按规则 a.len < b.len && a < b 比较,即先比长度、长度相同再按字典序比。这样能保证 9 < 10 这类按位比较会出错的情形正确成立;
  • 约定 edition 要么是年份(如 2022),要么是"年份.修订号"(如 2022.1),由此得到全序:
2022 < 2022.0 < 2022.1 < ... < 2022.9 < 2022.10 < ... < 2023 < ... < 2024 < ...

在这个全序上,后端不需要硬编码"当前最新 edition",只需回答相对性问题。原文给出的典型用法是:

  • 如果 haskell:more_monads 从 2023 开始为 true,后端只需查询 file.EditionIsLaterThan("2023")
  • 如果它在 2023.1 又变回 false,后端可以查询 file.EditionIsBetween("2023", "2023.1")

也就是说,默认值逻辑被表达为对 edition 的区间谓词(less-than / is-between),而不是对某个 edition 的等值匹配——这允许后端(虽然官方并不特别推荐)频繁调整默认值而不与核心脱节。

在仓库当前源码中,Edition 已经从纯字符串演进为带数值的枚举(字符串仍是对外语法,枚举用于内部比较与特性解析)。src/google/protobuf/descriptor.proto 中的 Edition 枚举印证了"可枚举、有序、并保留大量占位"的设计意图:

  • EDITION_UNKNOWN = 0:未知 edition 的占位;
  • EDITION_LEGACY = 900:表示"某特性首次引入之前"的默认行为,等效于"无限过去";
  • EDITION_PROTO2 = 998EDITION_PROTO3 = 999:旧语法的"准 edition",不能用于声明 proto 文件版本,但特性定义必须为 proto2/proto3 提供默认值以保持向后兼容;
  • EDITION_2023 = 1000EDITION_2024 = 1001EDITION_2026 = 1002:已发布(released)的正式 edition,注释说明"具体数值是任意取的";
  • EDITION_UNSTABLE = 9999:用于开发和测试未排期特性的占位 edition。

这与文档"全序 + 区间谓词"的思路一脉相承:每个 edition 都有确定的序号位置,特性默认值按区间解析。仓库中大量 edition 特性的默认值测试也验证了这套机制,例如 editions/defaults_test.cceditions/defaults.bzl,以及针对各 edition 默认 feature 集的输入文件 test_editions_2024_default_features.prototest_editions_2026_default_features.proto

如何创建一个 Edition

文档 Creating an Edition 一节的核心观点是:在某种意义上,每个 edition 都已经存在了——剩下的只是为它定义 features。 按 feature 类型分两种情况:

  1. proto: featureprotoc 前端天然知道这些 feature,它们直接实现在前端(如 proto:closed_enums 决定枚举是否封闭)。
  2. 后端 feature:后端必须能够产出一种形如 message Edition { repeated Feature defaults = 1; } 的 proto,用上文 less-than / is-between 谓词描述"某个 edition 应该长什么样"。protoc 可以用这些信息展示它针对已接入后端所知道的全部 feature 集合;当然,后端在代码生成(codegen)决策时也必须使用这份信息。

这一设计在仓库中有直接对应物:每个语言后端都把自己支持的 edition 默认值以序列化后的 FeatureSetDefaults 形式编译进运行时。例如 C++ 后端的 src/google/protobuf/cpp_edition_defaults.h 内嵌了一段 PROTOBUF_INTERNAL_CPP_EDITION_DEFAULTS 字符串,注释明确写道:

"This file contains the serialized FeatureSetDefaults object corresponding to the C++ runtime. This is used for feature resolution under Editions."

这正是文档所述"后端上报 defaults"机制的落地形态:前端(protoc)与运行时共享同一份 edition→feature 默认值描述,代码生成期与运行期的 feature 解析因此保持一致。其他语言后端也有类似结构,如 PHP 的 FeatureSetDefaults.php 与 C# 的 FeatureSetDescriptor.cs

"来自未来的 Edition":新编码与旧读取器问题

这是文档中最具前瞻性的一节。场景设定:Haskell 后端 v5.0 引入了 haskell:more_monads,且该特性有运行时组件——descriptor 中必须存在这个特性,运行时才能正确解析消息。但线上有一个运行 v4.2 的旧服务,它构建时使用的 proto 已经是 edition 2023(或它动态加载了该 proto)。此时 v5.0 客户端发送了一个"来自未来"的不兼容消息。由于解析失败是不可接受的服务降级,文档列出两个选项:

  • 选项 A:Edition 不允许引入"要求读取方接受新编码"的特性;同理,也不允许添加会约束旧解析器的限制。
  • 选项 B:Edition 可以引入这类特性,但必须纳入某种**构建地平线(build horizon)**约束。

文档明确否定了选项 A 的充分性:它听起来合理,但意味着 protobuf 永远无法把某些演进做进 editions——例如"把 message 字段从 length-prefixed chunk 改为 group 编码"这类根本性的编码变更就是被它挡住的。因此结论倾向选项 B:定义一个构建地平线,让新编码特性的启用受"参与方是否都在地平线内构建"的约束。这一约束在后续 Editions 设计文档中延续为 minimum required edition、生命周期管理等主题(可参见同目录的 minimum-required-edition.mdedition-lifetimes.md)。

仓库中的现状印证

把文档的三问与当前仓库对照,可以看到设计已经落地为可验证的机制:

  • Edition 有序且可枚举src/google/protobuf/descriptor.protoEdition 枚举给出确定序号;conformance/test_protos/test_messages_edition2023.prototest_messages_edition_unstable.proto 表明 conformanace 测试套件已按 edition 分档覆盖 2023 与 unstable edition。
  • edition 字符串进入 descriptorFileDescriptorProto 中的 optional Edition edition = 14; 字段与 syntax 字段配套(edition 存在时 syntax 必须为 "editions"),见 descriptor.proto 的注释。
  • 后端默认值随运行时发布:如前述 C++ 的 cpp_edition_defaults.h;C++ codegen 侧的 edition 默认值测试见 editions/generated_reflection_test.cceditions/generated_files_test.cc
  • edition 特性的引入/移除区间descriptor.proto 中的特性定义直接使用了 edition_introduced / edition_removed 字段表达默认值随 edition 变化的区间,例如某个行为"在 editions 2024 及以上默认开启,2024 移除显式选项"、cc_enable_arenas "在 editions 2026 及以上移除"(descriptor.proto 附近),这正是文档"less-than / is-between 谓词"思想的字段化实现。
  • editions 代码生成对照测试editions/codegen_tests/ 目录包含大量 edition2023_*edition2024_* 的命名风格、多文件生成、symbol visibility 等测试 proto,editions/golden/ 下则保存了 proto2/proto3→edition 转换的 golden 结果,可用于核对 edition 演进行为是否符合设计预期。

小结

《Edition Evolution》用三个设计要点回答了 Editions 机制的"演化"问题:

  1. 全序比较代替 edition 等值匹配,使任何后端(包括不受 protobuf 核心团队控制的第三方后端)都能以 EditionIsLaterThan / EditionIsBetween 形式的区间谓词声明自己的默认值策略;
  2. 后端自描述:后端以序列化 FeatureSetDefaults 的形式上报各 edition 的 feature 默认值,protoc 与运行时据此做统一的 feature 解析,仓库中 C++ 的 cpp_edition_defaults.h 即其实例;
  3. 未来兼容性问题通过"构建地平线"而非"禁止新编码"来解决,保留了把编码层演进纳入 editions 的可能性。

若要进一步理解 edition 的命名规范与生命周期管理,建议继续阅读 edition-naming.mdlegacy-syntax-editions.mdeditions-life-of-a-featureset.md

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