首页
/ Protobuf Editions 特性生命周期设计:FeatureSupport、EDITION_LEGACY 与支持窗口校验机制

Protobuf Editions 特性生命周期设计:FeatureSupport、EDITION_LEGACY 与支持窗口校验机制

2026-09-05 09:11:20作者:秋阔奎Evelyn

本文基于 Protobuf 仓库中的设计文档 edition-lifetimes.md,系统讲解 Editions(版本)机制下"特性(feature)生命周期"问题的来龙去脉:为什么 Edition 2023 之后引入/移除 feature 会成为风险、Protobuf 团队如何用 FeatureSupport 四个字段选项加 EDITION_LEGACY 占位版本给出解决方案,以及该设计如何落地到 descriptor.proto 与 protoc 的特性解析校验中。读完后你将理解 Editions 中"版本"与"特性"的完整生命周期模型,并能在源码层面追踪其实现与验证逻辑。

背景:从 Edition Zero 到重新评估特性生命周期

Editions 机制的落地最初依据的是两篇设计文档:Protobuf Editions Design: FeaturesLife of an Edition。后者对 edition 和 feature 的生命周期持非常强势的立场(例如 feature 是"过渡性"的、移除 feature 只能在破坏性发布中批量进行)。此后,围绕 editions 的许多想法在 Edition Naming 中被简化——我们选择了一套由 Protobuf 团队拥有并定义的更严格的命名方案(edition 用整数枚举表示、按时间全序排列)。

在把 editions 推广到各个 protoc 插件并规划 edition 2024 的过程中,团队意识到:feature 的生命周期也需要重新评估。另有一份内部文档 Editions: Life of a Feature(不对外公开)提出了 Life of an Edition 的替代愿景,尝试对 feature 与 edition 的交互施加更紧的约束;它预见了许多当下正在遇到的问题,并给出了一个可能的解决方案——本文正是对这一问题的正式回应。

现状总览:features 与 editions 解耦的模型

今天,features 和 editions 在很大程度上是相互独立的:

  • 我们有一组 feature,各自控制某种行为,它们在任何 edition 中都可以使用
  • 每个 edition 本质上只是所有 feature 的一套独立默认值;
  • 用户可以在任何 edition 中覆盖任何已发布 feature 的取值;
  • 每个生成器(以及 protoc 本身)都会声明自己支持的 edition 范围(edition support window),遇到范围外的 proto 文件会直接拒绝。

这套模型有一个很好的性质:保持行为不变的 edition 升级永远可以安全执行(以当前最新的 proto 语言为参照)。它把 edition 与破坏性变更解耦开来,使我们只需要关心一套版本方案——即 OSS 的发布。

但在只有 Edition 2023 一个 edition 的当下,这套体系运转良好;向前看,问题就出现了。edition 和 feature 的生命周期中各有三个关键事件:引入(introduction)、弃用(deprecation)、移除(removal)。弃用本质上是"软移除",用于给用户足够的预警时间,因此两类问题(弃用与移除)面临的困境是一样的。

引入一个 Edition

由于每个生成器插件(以及 protoc)都声明了自己的 edition 支持窗口,引入新 edition 目前被处理得很好。我们可以享受推广 Edition 2023 时的全部好处,并在之后的每个 edition 中复用(例如:可以做出以 edition 为门禁的激进语言变更)。

放弃一个 Edition

今天,放弃对某个 edition 的支持其实意义不大。我们可以简单地在某个破坏性发布中把二进制的最低支持 edition 抬升,但这与 feature 支持没有任何关联,最多只能帮我们清理一些按 edition 分支的解析器代码。而这些代码总是与某个新 edition 引入时的语言变更绑定,本可以在移除旧 edition 时一并收尾。

引入一个 Feature(问题最大的一环)

每引入一个新 feature,我们都要为每一个 edition 指定它的默认值(protoc 强制要求每个 edition 都有一套已知的默认值集合)。同时,它会立即在所有 edition 中可被覆盖。这意味着:声明过支持旧 edition 的存量二进制,可能突然收到一个覆盖了它完全不知道的 feature 的 proto 文件

我们在引入 string_type feature(属于 New String APIs,不对外公开)时就遇到了这个问题。当时的解法是临时(ad hoc)加了一层校验,禁止在 Edition 2023 中覆盖该 feature,直到我们准备好发布它为止。但这个解法不具备普适性:在 OSS 中存在任意老的二进制,甚至在 google3 内部,构建周期内也可能存在长达六个月的历史二进制。这些老二进制没有那层校验,会 happily 地处理带 string_type 覆盖的 Edition 2023 文件——尽管它们根本不知道该 feature 该如何正确处理。

移除一个 Feature

在另一个极端,我们需要一种方式来弃用并移除对某个 feature 的支持。由于预期大多数 feature 会存续多年,我们此前被迫思考这种情况的次数不多。当前的既定方案是:先把 feature 字段定义标记为 deprecated,再在一个破坏性发布中彻底移除它(并在 google3 内彻底清退)。

这套方案的问题在于,它给试图理解我们支持保证的用户带来了大量复杂度:他们需要追踪每一个所使用 feature 的生命周期,还要应对不同版本 protoc 与其插件之间难以预测的相互作用。如果我们在 protoc 中移除了一个全局 feature,某些插件可能仍然期望看到它而因此损坏,另一些则毫不在乎、继续正常工作。

推荐方案:用字段选项声明 Feature 生命周期

四个新的字段选项

方案的核心是为 feature 定义添加四个新的字段选项:

选项 含义
edition_introduced 该 feature 首次可用的 edition;在此之前使用 EDITION_LEGACY 的默认值且不可覆盖
edition_deprecated 该 feature 变为弃用的 edition;之后使用可能触发警告
deprecation_warning 弃用警告的自定义文本
edition_removed 该 feature 实际被移除的 edition;之后保留最后一个默认值但禁止覆盖

这允许每个 feature 声明自己被引入的 edition、被弃用的 edition、预期何时移除(触发弃用警告),以及实际移除的 edition。

EDITION_LEGACY:"无限过去"的占位版本

方案还新增一个特殊 edition 枚举值 EDITION_LEGACY,作为"无限过去"的占位符:

  • 对于早于 edition_introduced 的 edition,会分配 EDITION_LEGACY 对应的默认值,且该值必须始终表示该 feature 出现之前的空操作(noop)行为;proto 文件在不升级到新 edition 的情况下不允许覆盖该 feature;
  • 被弃用的 feature 可以获得比普通 deprecated 选项更特殊对待:一条自定义警告,提示用户应迁出;
  • 对于晚于 edition_removed 的 edition,最后一个 edition 的默认值继续保留,但 proto 文件中不允许再覆盖。

一个假设的 feature 示例

文档给出的完整示例如下(一个假设的 feature do_something):

optional FeatureType do_something = 2 [
    retention = RETENTION_RUNTIME,
    targets = TARGET_TYPE_FIELD,
    targets = TARGET_TYPE_FILE,
    feature_support {
      edition_introduced = EDITION_2023,
      edition_deprecated = EDITION_2025,
      deprecation_warning = "Feature do_something will be removed in edition 2027",
      edition_removed = EDITION_2027,
    }
    edition_defaults = { edition: EDITION_LEGACY, value: "LEGACY" }
    edition_defaults = { edition: EDITION_2023, value: "INTERMEDIATE" }
    edition_defaults = { edition: EDITION_2024, value: "FUTURE" }
];

其各阶段行为是:

  1. edition 2023 之前:feature 永远取默认值 LEGACY,且 proto 文件被禁止覆盖它;
  2. edition 2023:默认值变为 INTERMEDIATE,用户可以覆盖回旧默认值或指向未来行为;
  3. edition 2024:默认值再变为 FUTURE
  4. edition 2025:任何对它的覆盖开始发出弃用警告;
  5. edition 2027:禁止覆盖该 feature,行为永远固定为 FUTURE

注意:示例中的 2025/2027 是假设值。当前仓库中已发布的 edition 为 2023、2024、2026(见下文 Edition 枚举),文档写作时"下一个 edition"还是 2024。

与当前仓库源码的对照:方案已经落地

从源码结构看,这份设计文档的建议已经被采纳并实现。当前仓库中的关键证据:

1. EDITION_LEGACY 已存在于 Edition 枚举中,且注释与设计文档一字不差地呼应了"无限过去"的定位,见 Edition 枚举定义

enum Edition {
  EDITION_UNKNOWN = 0;
  // A placeholder edition for specifying default behaviors *before* a feature
  // was first introduced.  This is effectively an "infinite past".
  EDITION_LEGACY = 900;
  EDITION_PROTO2 = 998;
  EDITION_PROTO3 = 999;
  EDITION_2023 = 1000;
  EDITION_2024 = 1001;
  EDITION_2026 = 1002;
  EDITION_UNSTABLE = 9999;
  // ...
  EDITION_MAX = 0x7FFFFFFF;
}

2. FeatureSupport 消息与 feature_support 字段选项已定义,见 FeatureSupport 消息。四个推荐字段全部在场,且实现中额外增加了一个 removal_error(字段 5)——文档中未提及的移除错误文案字段:

message FeatureSupport {
  optional Edition edition_introduced = 1;
  optional Edition edition_deprecated = 2;
  optional string deprecation_warning = 3;
  optional Edition edition_removed = 4;
  // The removal error text if this feature is used after the edition it was
  // removed in.
  optional string removal_error = 5;
}

该选项挂在 FieldOptions.feature_support(字段 22)上;此外 EnumValueOptions 也拥有 feature_support(字段 4),说明枚举值级别的生命周期也被纳入了同一套机制。

3. 真实 feature 正在使用生命周期声明。 例如 FeatureSet 中的各全局 featurefield_presenceenum_type 等声明 edition_introduced: EDITION_2023 并配有 EDITION_LEGACY 默认值;enforce_naming_style 声明 edition_introduced: EDITION_2024,且默认值在 2024/2026 间演进(STYLE_LEGACYSTYLE2024STYLE2026);enforce_proto_limits 声明 edition_introduced: EDITION_2026descriptor.proto 中也存在已声明 edition_removed 的 feature(如 一个 edition_removed: EDITION_2024 的字段选项 和一个 edition_removed: EDITION_2026 的字段选项)——feature 的"死亡"从此有了显式的、可被工具校验的 edition 边界。

4. 支持窗口校验已在 protoc 特性解析中实现。 feature_resolver.cc 中可以看到与文档描述完全一致的判定逻辑:计算某 edition 的默认可覆盖集合时,落在 [edition_introduced, edition_removed) 窗口之外的 feature 会被排除(窗口检查);当 proto 文件覆盖了一个超出存在窗口的 feature 时,会报"该 feature 尚未引入/已被移除"的错误,超过 edition_deprecated 则输出 deprecation_warning错误与警告路径);另外还有对 FeatureSupport 自身一致性的检查,例如 edition_deprecated 必须不早于 edition_introduced、声明 deprecation_warning 必须同时声明 edition_deprecatededition_removed 必须晚于 edition_introduced 等(一致性校验)。对应的单元测试位于 feature_resolver_test.cc

对 Edition 生命周期的连带影响

把 feature 生命周期绑定到具体 edition 上,让 edition 本身获得了更多含义。edition 相关的破坏性变更仍然只发生在破坏性发布中,但现在所有 edition 相关的破坏性变更都来自这一流程:当我们放弃一个 edition 时,破坏性变更必然来自之前已弃用 feature 的移除。通过定期放弃旧 edition,我们可以逐步清理代码库。

Edition 升级可能变成破坏性变更

该设计的一个直接后果:edition 升级现在可能引入破坏。任何使用了已弃用 feature 的 proto 文件,当其 edition 被提升到该 feature 已被移除的版本时就会损坏。在 google3 内部,我们需要在移除 feature 之前彻底清退所有弃用用法。

不过相对现状这并不是实质变化——我们本来就需要先移除所有用法才能移除 feature。关键差异在于:我们现在有选项把一部分团队 allowlist 在旧 edition 上,同时让整个生态向前迁移;也可以 allowlist 专门的测试用例停留在旧 edition 上,从而持续测试已移除的 feature。

垃圾回收(GC)

另一个后果是:在 edition_removed 声明之前,我们无法清理 feature 相关代码,除非所有早于该 edition 的版本都已被放弃。这把 feature 支持与 edition 支持直接绑定了起来——尤其是在 OSS 中,我们无法强制用户把 proto 升级到最新 edition。

可预测性:主要的收益

这套策略最大的胜点是澄清了支持保证,让库更可预测。我们可以保证:处于某个特定 edition 的 proto 文件不会发生任何行为变化,除非:

  1. 我们在 editions 框架之外做出了破坏性变更;
  2. 我们放弃了该 proto 文件所用的 edition。

同时我们也可以保证:只要用户远离已弃用的 feature,他们就能不做任何修改地升级到下一个 edition。

实现路径

为什么实现非常容易

文档指出,这套设计"非常容易"在当前实现,理由是:只需添加新的字段选项和新的占位 edition,然后在 protoc 中实现新校验即可。因为两个错误条件(使用 feature 超出其存在窗口)和警告(使用弃用 feature)都只触发于被覆盖的 feature,而生成器的 feature 扩展必须被 import 才能被覆盖,所以 protoc 已经拥有它需要的所有信息,"protoc 不知道 feature 默认值"的问题根本不存在。从仓库源码看,这一判断是准确的:校验逻辑完全建立在 FeatureSupport 元数据之上(见上文 feature_resolver.cc 各段)。

文档同时给出了时间约束:如果等到 edition 2024 发布后再做,2024 中新增的 feature 将从 2023 开始可用,届时要么刻意回移支持、要么先清退所有相关用法才能启用校验层。因此推荐尽快实现,在 2024 推广开之前完成。当前仓库的状态印证了这一点——EDITION_2024 已经存在,而 FeatureSupport 校验也已经上线。

支持动态消息的运行时:overridable_features / fixed_features

对已经推广 editions 的生成器而言,本方案不需要任何改动。但对于支持动态消息的运行时,我们希望在加载时校验,防止无效描述符流入。由于所有运行时都能访问 protoc 编译出的 defaults IR,可以把尽可能多的信息打包进去以减少重复:具体做法是在 FeatureSetEditionDefault 中、除现有 features 字段之外新增两个 FeatureSet 字段

  • overridable_features —— 用户在该 edition 中允许覆盖的默认值;
  • fixed_features —— 用户不允许覆盖的默认值。

现有 features 字段保留作为迁移工具,避免破坏已经使用它计算默认值的插件和运行时;可以在 27.0 发布前从 OSS 中剥离它,待所有人迁移完成后彻底移除。

当前仓库的 FeatureSetDefaults 定义 显示了这一演进的真实结果:FeatureSetEditionDefaultoverridable_features(字段 4)与 fixed_features(字段 5)已就位,而旧的 features 字段已被 reservedreserved 1, 2; reserved "features";)——即文档所说的迁移窗口已经结束、旧字段已从 OSS 中移除:

message FeatureSetEditionDefault {
  optional Edition edition = 3;
  // Defaults of features that can be overridden in this edition.
  optional FeatureSet overridable_features = 4;
  // Defaults of features that can't be overridden in this edition.
  optional FeatureSet fixed_features = 5;
  reserved 1, 2;
  reserved "features";
}

任何语言要计算某个 edition 的完整默认值,只需合并这两个 FeatureSet 对象。拆分的好处是:每个需要它的语言都能较容易地为动态消息实现校验。对于传入的未解析 FeatureSet(user_features),校验算法如下:

  1. 从 user_features 中剥离所有未知字段;
  2. 从 user_features 中剥离运行时不处理的扩展;
  3. merged_features := user_features.Merge(overridable_defaults)
  4. assert merged_features == overridable_defaults

只要每个 feature 都是标量值(合并即简单覆盖),该算法即成立。oneof 与 repeated feature 已被禁止,且计划在 OSS 发布前禁止 message 型 feature。

文档也坦承了一个小缺口:我们不会对其他语言拥有的 feature 做校验——语言 A 的动态消息会被天真地允许随意指定语言 B 的 feature。这并非最优,但与现状一致(动态消息的校验本来就比 protoc 处理的描述符宽松得多)。另一方面,语言 A 的所有者可以选择较容易地为语言 B 的 feature 添加校验,而无需重新实现 protoc 那种基于反射的 import 检查:只需把那些 feature 加进 defaults IR 的编译,并在校验时不剥离这些扩展即可。代价是语言 A 的 edition 支持窗口会被绑定到 B 上(至少对动态消息而言),A 无法在 B 之前扩展其最大 edition。对 Protobuf 这种 monorepo 内的生成器,这似乎没问题,但在其他地方未必可取。

修补旧 Edition(patch editions)

Edition Naming 中,我们放弃了"补丁 edition"的概念,因为 edition 永远向前、向后兼容——只有在推广提速后一年内需要多个 edition 时才可能需要补丁。但本方案改变了这一假设:edition 现在既非向前兼容(新 feature 在旧 edition 中不工作),也非向后兼容(旧 feature 可能在新 edition 中失效)。

假设 editions 层自身出了 bug,我们可能需要一个"补丁 edition"来安全地发布修复。例如:假设我们发现在 edition 2023 中 edition 默认值的计算是错误的并已误发布,而 issue 已修复且 edition 2024 也已发布——我们无法造一个 2023A"补丁"来修它,因为 edition 是整数表示的(2023 和 2024 相邻)。但我们仍希望给仍停留在 edition 2023 的用户发布某种修复,让他们在(可能破坏性的)2024 之前完成最小升级。

若真出现这种情况,一个可行办法是在 FileDescriptorProto 中引入新的整数字段 edition_patch。这需要一些工作才能纳入 feature 解析并推广到每个插件,但鉴于 edition 已经对多数用户隐藏(见 Editions Feature Visibility),这应该不算太糟。只要补丁从不引入/移除 feature 或改变其默认值,protoc 和插件就永远可以使用它所知最新的补丁来代表该 edition。

文档要求

作为该变更的一部分,我们需要向所有插件/运行时所有者公开文档化这一切。方案是:在 protobuf.dev 的 editions 文档站点中创建一个新专题,完整覆盖上述内容,以及插件/运行时所有者需要知道的其他相关细节。

备选方案对比

备选一:维持现状(Continue as usual)

唯一真正的替代方案就是不做改变——它继承上文总览部分列出的全部问题。

优点

  • 短期无需付出任何努力;
  • Edition 升级永远不会是破坏性变更。

缺点

  • 一旦 edition 2024 到来,很可能立刻引发问题;
  • 引入新 feature 危险且不可预测;
  • 移除 feature 会同时影响所有 edition;
  • 每个 edition 中受支持的 feature 集合可能随 protobuf 版本变化;
  • 用户认知负担高:他们需要在各版本间逐个追踪每一个 feature 的进展。

备选二:对动态消息做完整校验(Full Validation)

对已推广 editions 的生成器同样无需改动,但支持动态消息的运行时都需要加校验层,确保没有无效描述符流入。任何支持动态消息的运行时都应有反射,因此同一套基于反射的算法需要在每处复制一遍。对描述符上的每个 FeatureSet,校验形如:

absl::Status Validate(Edition edition, Message& features) {
  std::vector<const FieldDescriptor*> fields;
  features.GetReflection()->ListFields(features, &fields);
  for (const FieldDescriptor* field : fields) {
    // Recurse into message extension.
    if (field->is_extension() &&
        field->cpp_type() == FieldDescriptor::CPPTYPE_MESSAGE) {
      CollectLifetimeResults(
          edition, message.GetReflection()->GetMessage(message, field),
          results);
      continue;
    }

    // Skip fields that don't have feature support specified.
    if (!field->options().has_feature_support()) continue;

    // Check lifetime constrains
    const FieldOptions::FeatureSupport& support =
        field->options().feature_support();
    if (edition < support.edition_introduced()) {
      return absl::FailedPrecondition(absl::StrCat(
          "Feature ", field->full_name(), " wasn't introduced until edition ",
          support.edition_introduced()));
    }
    if (support.has_edition_removed() && edition >= support.edition_removed()) {
      return absl::FailedPrecondition(absl::StrCat(
          "Feature ", field->full_name(), " has been removed in edition ",
          support.edition_removed()));
    } else if (support.has_edition_deprecated() &&
               edition >= support.edition_deprecated()) {
      ABSL_LOG(WARNING) << absl::StrCat(
          "Feature ", field->full_name(), " has been deprecated in edition ",
          support.edition_deprecated(), ": ", support.deprecation_warning());
    }
  }
}

优点

  • 对任何语言中的任何 feature 生命周期违规都能拦截;
  • 更容易理解;
  • 更不易出错;
  • 易于用伪造 feature 做测试。

缺点

  • 只能在构建后(post-build)工作,需要在每种语言中编写大量代码来遍历描述符树并施加这些检查;
  • 有性能顾虑,尤其在 upb 中;
  • 与 protoc 的校验重复,尽管多数语言对动态消息本就做宽松得多的检查。

其他(文档中另列的一节权衡)

  • 优点:把所需的反射量降到最低;
  • 缺点:无法校验我们不认识的语言的扩展(因为它们没有编进二进制);pool 与运行时的 feature 之间可能出现版本偏差;需要反射来剥离意外字段;从代码角度理解该算法比较困难。

小结

这份设计文档回答的核心问题是:当 editions 从"单一 2023 版"走向多版本时,feature 的引入与移除如何不破坏生态。答案是:

  1. edition_introduced / edition_deprecated / deprecation_warning / edition_removed 四个字段选项,把每个 feature 的存在窗口显式声明出来;
  2. EDITION_LEGACY("无限过去")保证窗口之前的默认值恒为 feature 引入前的 noop 行为,且不可覆盖;
  3. 把 edition 的放弃、feature 的移除、代码的垃圾回收统一收拢到"破坏性发布"这一条通道里,从而给出清晰的用户保证:不改 edition、不用弃用 feature,行为就永远不变

从当前仓库可以确认,这套机制已经完整落地:descriptor.proto 中的 Edition 枚举、FeatureSupport 选项、FeatureSetEditionDefaultoverridable_features/fixed_features 拆分(旧 features 字段已 reserved),加上 feature_resolver.cc 中基于支持窗口的错误/警告校验与一致性检查,共同构成了 Editions 特性生命周期的运行时防线。想要进一步理解整个 editions 体系,可继续阅读同目录下的 Life of an EditionEdition NamingProtobuf Editions Design: Features 以及 What are Protobuf Editions

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

项目优选

收起
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
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384