首页
/ Protobuf Editions 设计解析:Edition Zero 如何以收敛语义统一 proto2 与 proto3

Protobuf Editions 设计解析:Edition Zero 如何以收敛语义统一 proto2 与 proto3

2026-09-05 10:05:24作者:裴麒琰

本文为 Google Protocol Buffers 官方设计文档 edition-zero-converged-semantics.md 的解读与源码级延伸。它解释了 Protobuf 团队如何通过引入 edition 关键字与 features 选项,把 proto2/proto3 十余年来分裂的语义收敛为一套"特性(feature)中心"的模型;读完你能理解 edition 的解析机制、FeatureSet 在 descriptor 中的落点、特性生命周期(introduced/deprecated/removed)的元数据设计,以及从 proto2/proto3 迁移到 Editions 的完整方法论。

一、背景与目标:用特性收敛 proto2/proto3 的语义分歧

该设计文档由 @perezd 与 @haberman 起草,于 2021-10-07 获批。其出发点非常明确:

我们希望减少由 syntax 关键字粗粒度管理的 API 语义复杂性,在采用 editions 后默认使用 proto2/proto3 的收敛语义;在需要时,客户可以借助 editions + features 提供的新能力,在细粒度上选择退出(opt out)与现有用法不兼容的特定语义。

换言之,syntax = "proto2" / syntax = "proto3" 是一个"旋钮太粗"的历史包袱——它把一整包隐含的行为标志捆绑在一起,导致客户经常困惑:"升到 proto3 我得到了什么?又失去了什么?"。Editions 的思路是:不再用一个二元开关切换语义,而是把每个可独立演化的行为拆成 feature,edition 只负责决定这些 feature 的默认值

文档给出的"为什么是现在"(Why Now)包含三点论证,值得完整保留:

  1. 更细粒度的意图表达:edition 提供了比 "proto2"/"proto3" 更细的意图规格。客户采用第一个 edition 后,升级到了所谓"收敛语义",并且可以按需可逆地"降级"回 proto2 或 proto3 语义——方法就是针对不兼容的特性显式 opt out;
  2. 消除 n^2 组合复杂度:如果 edition/feature 还要与显式的 "proto2"/"proto3" 语法指定相互作用,每个受影响运行时都要考虑所有组合。引入 editions 后,Protobuf 团队可以把支持模型转为明确的"以特性为中心"(feature-centric);
  3. 大版本升级的时机红利:editions 的引入几乎必然伴随 major 版本升级,为向细粒度规格过渡提供了充足的理由去做 breaking change。

这一愿景的完整上下文见同目录下的 what-are-protobuf-editions.md(Editions 项目总览)与 edition-zero-feature-enum-field-closedness.md(Enum 开放性特性设计)。

二、edition 关键字:IDL 层面的语义版本基线

文档规定:

  • edition 关键字用于定义某个 文件及其全部内容 所遵循的语义版本基线;
  • 只要 proto 文件声明了 edition,它就自动默认采用 proto2/3 的收敛语义
  • edition 的取值是字符串,按约定编码为年份。

2.1 当前仓库中的实际形态

src/google/protobuf/descriptor.proto 中,edition 被建模为强类型枚举而非自由字符串:

// The full set of known editions.
enum Edition {
  EDITION_UNKNOWN = 0;
  // "无限过去":某特性被引入之前的默认行为锚点
  EDITION_LEGACY = 900;
  // 旧语法"伪版":不可用于指定文件 edition,但特性定义
  // 必须为 proto2/proto3 提供默认值以保证向后兼容
  EDITION_PROTO2 = 998;
  EDITION_PROTO3 = 999;
  // 已发布的 edition,取值任意但按时间递增,便于比较
  EDITION_2023 = 1000;
  EDITION_2024 = 1001;
  EDITION_2026 = 1002;
  EDITION_UNSTABLE = 9999;
  // 测试用占位版、EDITION_MAX 等……
}

注意两个占位值 EDITION_LEGACY(900)与 EDITION_PROTO2/PROTO3(998/999):它们虽然不能用来声明文件的 edition,但所有 feature 的定义都必须为它们提供默认值——这正是"从 proto2/proto3 平滑收敛"的关键:旧语义被编码成了特性表中的两行默认值,而不是被抛弃。

FileDescriptorProto 中的两个相关字段(见 descriptor.proto#L115-L128):

// The syntax of the proto file.
// The supported values are "proto2", "proto3", and "editions".
//
// If `edition` is present, this value must be "editions".
optional string syntax = 12;

// The edition of the proto file.
optional Edition edition = 14;

从源码结构看,syntax 字段被保留为兼容通道:一旦存在 editionsyntax 一律被写成 "editions",真实版本信息全部落在 edition 字段。

2.2 解析器实现:edition 优先、syntax 降级

src/google/protobuf/compiler/parser.cc 中的 ParseSyntaxIdentifier 完整实现了文档的优先级规则:

bool Parser::ParseSyntaxIdentifier(const FileDescriptorProto* file, ...) {
  bool has_edition = false;
  if (TryConsume("edition")) {
    has_edition = true;
  } else {
    DO(Consume("syntax",
               "File must begin with an edition or syntax statement, e.g."
               " 'edition = \"2023\";'."));
  }
  ...
  if (has_edition) {
    if (!Edition_Parse(absl::StrCat("EDITION_", syntax), &edition_) ||
        edition_ == Edition::EDITION_PROTO2 ||
        edition_ == Edition::EDITION_PROTO3 ||
        edition_ == Edition::EDITION_UNKNOWN) {
      RecordError(... "Unknown edition \"", syntax, "\"." ...);
      return false;
    }
    syntax_identifier_ = "editions";   // edition 一律映射为 "editions"
    return true;
  }
  ...
}

几个关键实现细节:

  • edition 优先且互斥:解析器先尝试消费 edition 关键字,只有它不存在时才回落到 syntax。当两者同时出现时 edition 生效、syntax 被忽略——与文档"若 editionsyntax 同时存在,edition 优先、syntax 被忽略"的约定一致;
  • 值校验:edition 字符串被拼成 EDITION_<value> 后用 protobuf 自身的解析器(Edition_Parse)校验,proto2/proto3/unknown 作为 edition 值会被显式拒绝(它们只能作为 syntax 值或特性默认值锚点出现);
  • 缺省告警:文件若既无 edition 也无 syntaxparser.cc#L658-L664 会打印告警并默认按 proto2 处理;
  • edition 下的语法收紧:同文件中还有多处 edition 专属约束,例如 optional 标签在 editions 中不支持(parser.cc#L2498-L2505,字段显隐由 field_presence 特性控制)、group 语法被禁止(parser.cc#L2535-L2538)、option import 要求 edition >= 2024(parser.cc#L2636-L2637)。

一个可以直接编译验证的最小示例:

// editions 风格文件:edition 声明后不再需要(也不应)写 syntax
edition = "2023";
package demo;

message Point {
  optional int32 x = 1;  // editions 中显式字段需要显隐特性支持
}

而传统文件保持 syntax = "proto3"; 写法,解析路径不受影响。

三、features 选项:在 descriptor.proto 中统一挂载

文档的核心机制之一是为 descriptor.proto 引入 features 选项,其设计要点:

  • 统一定义为 repeated 字符串集合(文档初稿形态),可编码"退出某特性"(如 "-string_view")或"引入未来/实验特性"(如 "string_view");
  • features 选项要加到以下 descriptor 选项上:File、Message、Field、Enum、Enum Value、Oneof、Service、Method(Stream 仅限内部仓库);
  • 特性仅在配合 edition 关键字使用时才生效
  • 特性不做正确性校验,以保证向前/向后兼容——未来发行版可以安全地忽略当前不认识的特性。

3.1 最终落地形态:FeatureSet

从源码结构看,初稿中的"repeated string"方案演化为结构化的 FeatureSet 消息(descriptor.proto#L1060-L1076),并在文档列出的全部九个挂载点中落地为 optional FeatureSet features 选项:

挂载位置 字段号
FileOptions(file) features = 21
MessageOptions(message) features = 50
FieldOptions(field) features = 50
EnumOptions(enum) features = 12
EnumValueOptions(enum value) features = 1
OneofOptions(oneof) features = 7
ServiceOptions(service) features = 2
MethodOptions(method) features = 34 附近
其余描述符选项 features = 35

(以上字段号均来自 descriptor.proto 中各 Options 消息内 // Any features defined in the specific edition. 注释下的 optional FeatureSet features = N; 声明。)

FeatureSet 的首个字段展示了特性声明的完整元数据风格:

message FeatureSet {
  enum FieldPresence {
    FIELD_PRESENCE_UNKNOWN = 0;
    EXPLICIT = 1;
    IMPLICIT = 2;
    LEGACY_REQUIRED = 3;
  }
  optional FieldPresence field_presence = 1 [
    retention = RETENTION_RUNTIME,
    targets = TARGET_TYPE_FIELD,
    targets = TARGET_TYPE_FILE,
    feature_support = {
      edition_introduced: EDITION_2023,
    },
    edition_defaults = { edition: EDITION_LEGACY, value: "EXPLICIT" },
    edition_defaults = { edition: EDITION_PROTO3, value: "IMPLICIT" },
    ...
  ];
}

这里能看到文档思想在实现中的完整闭环:

  • edition_defaults = { edition: EDITION_LEGACY, value: "EXPLICIT" }{ edition: EDITION_PROTO3, value: "IMPLICIT" } 正是"proto2/proto3 隐含行为被显式编码为特性默认值"——proto2 字段默认显式存在(EXPLICIT),proto3 标量默认隐式(IMPLICIT),两种旧语法在同一张特性表中被"归档";
  • targets 限定该特性允许挂载的实体层级(field 或 file),呼应文档"特性可在任意 descriptor 层级声明"但需声明适用范围的要求;
  • retention = RETENTION_RUNTIME 标记运行时必须感知的特性。

3.2 特性的生命周期元数据

descriptor.proto#L820-L843 中的 FeatureSupport 消息把"特性的引入—弃用—移除"建模为四个 edition 锚点,这是实现"特性不校验、向前兼容"承诺的配套机制:

message FeatureSupport {
  // 特性首次可用的 edition;更早的 edition 使用 EDITION_LEGACY
  // 的默认值且不可覆盖
  optional Edition edition_introduced = 1;
  // 该 edition 起使用可能触发警告
  optional Edition edition_deprecated = 2;
  optional string deprecation_warning = 3;
  // 该 edition 起特性不再可用,此后使用最后的默认值且不可覆盖
  optional Edition edition_removed = 4;
  optional string removal_error = 5;
}

descriptor.proto#L1072-L1076 附近可以看到真实用例,例如 edition_introduced: EDITION_2023field_presence,以及 edition_removed: EDITION_2024 并附 removal_error 文本的旧行为。运行时侧对应的解析入口是 FeatureSetDefaultsdescriptor.proto#L1297-L1316):每个已知 edition 到其特性默认值的映射表,按"不超过目标 edition 的最近一档"取默认值。

3.3 特性继承:声明在任一层级,影响其下级

文档规定"特性可以在任意 descriptor 层级声明,但特性定义能否影响子类型由 Protobuf 团队酌情决定(例如一个 file 级特性 opt-out 可以影响文件内所有字段)"。最终实现中这体现为特性继承(feature inheritance):子实体的特性值默认为父实体(词法上级)的值,除非子实体显式覆盖,递归适用;且继承对用户完全透明,"表现得像特性被显式写在每个位置上"(见 what-are-protobuf-editions.md 的 What is a feature? 一节)。文件级声明因此可以覆盖全文件的同名特性,这正是大规模迁移中控制 diff 规模的关键手段。

四、特性分类学:language-specific 与 semantic

文档将特性划分为两大类,这一分类直接决定了各运行时的实现责任边界:

4.1 语言特定特性(Language-specific)

作用于某语言生成的 API,对其它语言无意义、可被完全忽略。文档列举的例子:

  • (C++)string 字段改为返回 string_view
  • (Java)移除令人困惑的 Enum#valueOf(int) API;
  • (Java)将 oneof 枚举重命名为规范的驼峰命名。

其本质是 protobuf IDL 与各自代码生成器之间的私有(隧道式)接口:每种语言的 codegen 独立决定某个 edition 的"基础特性集",并独立定义跨 edition 的迁移路径。实现上,语言后端可以通过 extension 定义语言作用域特性(例如文档总览中提到的 [features.(pb.cpp).string_type = CORD][ctype = CORD] 的替代),各 codegen 后端拥有自己特性的定义权。

4.2 语义特性(Semantic)

定义作用于 protobuf 数据模型本身、与语言无关的行为变化。文档给出的例子:

  • Open enums:枚举取值直接放入字段,而非进入 UnknownFieldSet(对应后来实现的 enum_type = OPEN/CLOSED 特性,专项设计见 edition-zero-feature-enum-field-closedness.md);
  • Packed:repeated 字段在二进制线上是否打包(对应 FeatureSet 中的 repeated_encoding = PACKED/UNPACKED 特性)。

语义特性的约束范围显著更广:必须跨语言被遵守,且每种语言都要正确实现该语义。由此推出文档的一个重要结论:每种语言要么(1)知道每个 edition 的规范"基础特性集",要么(2)由 protoc 本身解析出 edition 的"默认特性集"并显式传播进 descriptor。当前仓库选择的是后者:protoc 将特性默认值解析后写入 File/Message/Field 各级 FeatureSet,运行时只需消费已解析的结果,这也是 retention = RETENTION_RUNTIME 标记存在的意义。

五、演化 protobuf IDL 与 descriptor.proto 的成本对比

文档专有一节比较了两条演化路径的侵入性,这是理解 editions 实现策略的关键:

改 protobuf IDL(相对温和):IDL 的解析与解析全部发生在 protoc 中,且解析器只有单一实现。任何仅靠解析器就能解决的变化都相对不具侵入性(文档同时承认内部存在"构建视野"问题——内部系统会在生产环境中解析 proto,解析器变更需要灰度)。上文第二节的 parser.cc 就是这条路径的唯一落点。

改 descriptor.proto(侵入性大):它影响大量下游系统。很多系统通过 descriptor API(如 C++ 的 google::protobuf::Descriptor)或直接访问 descriptor.proto(如 google.protobuf.DescriptorProto)来消费描述符,任何变更都必须更加谨慎。这解释了为何最终实现采用只增不改的策略:新增 FileDescriptorProto.edition 字段(字段号 14)、新增各级 FeatureSet 选项、新增 FeatureSetDefaults 消息,而不动任何既有字段的语义;并且 descriptor 中所有新字段都标注了"仅供插件与编译器使用,其他场景应依赖 protoreflect API"的警示注释。

六、syntax 关键字的弃用

文档规定:当 edition 关键字存在时,syntax 关键字不再被要求或被观察,因为它已被视为冗余;若两者同时存在,edition 优先、syntax 被忽略。

这一点在代码中是精确落地的:ParseSyntaxIdentifier 中一旦走 edition 分支,syntax_identifier_ 被强制置为 "editions",descriptor 的 syntax 字段只会写入 "editions" 一个值;edition 字段则携带真正的版本枚举。反过来,声明 edition = "proto2"edition = "proto3" 会直接报错(Unknown edition),因为这两个值是保留给特性默认值锚点的,不是合法的文件 edition——即"可逆降级回 proto2/proto3 语义"的正确姿势不是把 edition 写成 proto2,而是在 edition 文件中用特性 opt-out 显式表达旧语义(见 edition-evolution.md 对特性弃用与迁移窗口的讨论)。

七、从 proto2/proto3 迁移到 Editions + Features

文档指出,今天的 syntax 使用方式"不透明地捆绑了一组基于 proto2/proto3 存在与否而设置的隐含特性标志"。把 editions/features 定义为处于 proto2/3 收敛态之下,就能让客户自行决定哪些特性对其 proto 用法重要;而把既有用户迁移到 editions,本质上是一次把隐含行为显式化的大规模变更

文档给出的隐含行为对照表(完整继承原文,✅=默认开启,🚫=默认关闭):

Feature proto2 隐含行为 proto3 隐含行为
packed_repeated_primitives 🚫
extensions 🚫
required 🚫
groups 🚫
cpp_string_view 🚫 🚫
java_enum_no_value_of 🚫 🚫
open_enums 🚫
(更多条目……)

这张表就是"收敛语义"的直觉说明:每一行都是一个可以独立 opt-in/opt-out 的 feature,迁移 proto2/proto3 文件 = 把 syntax 换成 edition + 在合适位置补上特性声明。仓库内的 editions/codegen_tests/ 目录就是这套机制的持续回归验证:proto2_*.protoproto3_*.proto(如 proto2_required.protoproto2_packed.protoproto3_optional.protoproto3_utf8_strict.proto)与 edition2023 文件并列存放,确保两种旧语法的行为在新机制下逐字节可复现;editions/golden/ 目录还包含 simple_proto2.protosimple_proto3.proto 等转换金样,用于验证 edition 转换工具的输出。

7.1 大型部署中 features 的复杂度管理

文档结尾指出:为缓解大 proto 项目中 editions 与渐进式特性滚出/同步的复杂性,已另行建立了一个独立设施(separate concept),它可以用于(例如)把 google3 中 syntax 关键字的既有用法整体迁移到 Editions + Features。该设施的详细讨论散见于 editions 设计系列的其他文档,如 minimum-required-edition.md(最低 edition 要求机制)与 life-of-an-edition.md(edition 生命周期),读者可沿此索引继续深入。

八、先例与设计来源

文档"Prior Work"一节列出的三条先例,勾勒出这条设计线的来源:

  • proto2/proto3 收敛愿景(内部文档,未公开);
  • descriptor.proto 的 Epochs 提案(内部文档,未公开);
  • Rust editions——what-are-protobuf-editions.md 明确写道 "Directly inspired by Rust editions",即 edition 只改默认值、不引入新行为,任何 edition 组合的消息始终可以互相导入与互操作。

九、总结:从粗旋钮到特性中心的语义模型

回看整篇设计文档,其骨架可以用一句话概括:edition 决定默认值,feature 表达例外,syntax 退役。当前仓库中的证据链完整支撑了这三大机制——

  • edition 关键字的解析、校验与 syntax 降级逻辑集中在 parser.cc
  • features 选项以 FeatureSet 形态挂载于全部九类 descriptor 选项,并携带 edition_defaults / feature_support 元数据完成"旧语义编码为默认值"与"特性生命周期管理"(descriptor.proto);
  • proto2/proto3 → editions 的等价性由 editions/codegen_tests/editions/golden/ 目录下的对照测试持续守护。

对于维护大型 schema 集合的团队,这套模型的实际收益是:升级 edition 对未使用弃用特性的文件是 no-op;需要保留旧行为时用特性 opt-out 显式声明即可;而所有行为变化都可以追溯到 .proto 文件的一次文本改动(edition bump 或 feature 变更)——这正是文档"让客户自己决定哪些特性对自己重要"的落地形态。

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

项目优选

收起
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.79 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
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384