首页
/ Protobuf Editions Features 设计深度解析:以自定义选项定义特性(Features as Custom Options)机制

Protobuf Editions Features 设计深度解析:以自定义选项定义特性(Features as Custom Options)机制

2026-09-04 15:27:29作者:董灵辛Dennis

本文基于 protobuf 仓库的设计文档 protobuf-editions-design-features.md,深入讲解 Protobuf Editions 项目如何借助 proto2 自定义选项(custom options)表达"特性(Features)":特性如何在 file/message/field/enum 等实体上生效、如何通过 MergeFrom 语义实现跨实体继承、targetretention 约束如何防止误用、以及 edition_defaults 扩展如何把"某 edition 的默认特性集"编码进描述符。读完后,你将能够读懂当前 descriptor.protoFeatureSet 的真实定义,理解 protoc 解析 edition 默认值的算法,并在自己的 .proto 文件中正确使用 option features.* 语法。

一、背景:Editions 为什么需要 "Features"

Protobuf Editions 项目 用"edition"让 Protobuf 能够安全演进。按设计文档的定义:一个 edition 本质上就是"一组特性(features)+ 每个特性一个默认值"的集合。特性的集合、以及某个特性的默认值,只有在新 edition 引入时才能改变。

特性(feature)的作用是在 .proto 文件的每个实体(entity)维度上定义具体的变更与演进点——实体可以是文件(file)、消息(message)、字段(field),或文件中其他任意词法元素。换言之:

  • edition 决定"默认是什么";
  • feature 允许你在具体实体上"覆盖默认"。

这份设计文档替代了一个早期方案(早期方案用字符串来定义特性)。新方案的关键洞察是:Protobuf 早已支持 自定义选项(custom options),直接复用这套机制即可获得丰富的语法表达,而无需向 Protobuf 语言中引入任何新的语法形式(syntactic form)。这一点极大降低了语言层面的破坏性,也是该设计最终被批准(Approved: 2022-10-13,作者 @haberman 与 @fowles)的原因。

二、示例用法:option features.* 在 .proto 文件里长什么样

设计文档给出了一份完整的示例,展示了三种层级的特性设置——文件级 option、消息内部覆盖、以及语言专属特性:

edition = "2023";

package experimental.users.kfm.editions;

import "net/proto2/proto/features_cpp.proto";

option features.repeated_field_encoding = EXPANDED;
option features.enum = OPEN;
option features.(pb.cpp).string_field_type = STRING;
option features.(pb.cpp).namespace = "kfm::proto_experiments";

message Lab {
  // `Mouse` 是 open 的,因为它继承了文件的值
  enum Mouse {
    UNKNOWN_MOUSE = 0;
    PINKY = 1;
    THE_BRAIN = 2;
  }
  repeated Mouse mice = 1 [features.repeated_field_encoding = PACKED];

  string name = 2;
  string address = 3 [features.(pb.cpp).string_field_type = CORD];
  string function = 4 [features.(pb.cpp).string_field_type = STRING_VIEW];
}

enum ColorChannel {
  // 关闭来自外层文件的作用
  option features.enum = CLOSED;

  UNKNOWN_COLOR_CHANNEL = 0;
  RED = 1;
  BLUE = 2;
  GREEN = 3;
  ALPHA = 4;
}

这段示例包含了理解整个特性系统的三个关键要素:

  1. 文件级特性option features.repeated_field_encoding = EXPANDED; 对整个文件生效,成为所有子实体的默认值;
  2. 实体级覆盖mice 字段用 [features.repeated_field_encoding = PACKED] 局部覆盖文件默认值;ColorChanneloption features.enum = CLOSED; 把文件级的 OPEN 反向关掉。注意 Mouse 枚举没有显式设置,因此"继承"了文件值而表现为 open;
  3. 语言专属特性features.(pb.cpp).xxx 是典型的 proto2 扩展选项语法,pb.cpp 是 C++ 代码生成器注册的扩展命名空间。

这套语法在当前仓库中是真实可用的。例如 cpp_features.proto 就注册了文档所设想的 C++ 专属特性:

// src/google/protobuf/cpp_features.proto
package pb;

import "google/protobuf/descriptor.proto";

extend google.protobuf.FeatureSet {
  optional CppFeatures cpp = 1000;
}

message CppFeatures {
  enum StringType {
    STRING_TYPE_UNKNOWN = 0;
    VIEW = 1;
    CORD = 2;
    STRING = 3;
  }
  optional StringType string_type = 2 [
    retention = RETENTION_RUNTIME,
    targets = TARGET_TYPE_FIELD,
    targets = TARGET_TYPE_FILE,
    feature_support = { edition_introduced: EDITION_2023 },
    edition_defaults = { edition: EDITION_LEGACY, value: "STRING" },
    edition_defaults = { edition: EDITION_2024, value: "VIEW" }
  ];
  ...
}

可以注意到,最终落地版本把文档草案中的 string_field_type = STRING/CORD/STRING_VIEW 演进为了 string_type = VIEW/CORD/STRING,并增加了 feature_support 选项标注该特性从哪个 edition 开始生效。测试文件 edition2023_string_type.proto 及其 golden 文件 test_messages_proto3_editions.proto 则验证了不同 string 特性值下各语言代码生成结果。

三、语言专属特性:用扩展号(extension number)划分领地

为了让每个代码生成器管理自己的专属特性,设计采用 proto2 扩展(extensions)机制。文档建议在 descriptor.protoFeatures 消息上预留扩展段:

// In net/proto2/proto/descriptor.proto:
syntax = "proto2";
package proto2;

message Features {
  ...
  extensions 1000;  // for features_cpp.proto
  extensions 1001;  // for features_java.proto
}

这样任何第三方代码生成器只要在 descriptor.proto 中预留一个扩展号,就能用 editions 机制管理自己的演进。在 .proto 文件中的用法是:

edition = "2023";

import "third_party/protobuf/compiler/cpp/features_cpp.proto"

message Bar {
  optional string str = 1 [features.(pb.cpp).string_field_type = true];
}

在当前仓库中,这一机制已经被完整实现。descriptor.protoFeatureSet 消息声明了一系列语言扩展:pb.CppFeatures(1000)、pb.JavaFeaturespb.GoFeatures(1002)、pb.PythonFeaturespb.CSharpFeaturespb.JavaMutableFeaturespb.Proto1Features 等。各语言的特性文件分散在仓库中,例如:

各语言的特性选项布局还专门有一篇设计文档说明,见 editions-feature-extension-layout.md

四、继承(Inheritance):Features 消息扩展所有 *Options

要让"文件级默认 → 消息级覆盖 → 字段级覆盖"的继承链成立,设计上做了一个很干净的决定:定义一个单一的 Features 消息,并让每一种 option 消息都包含一个该类型的字段

// In net/proto2/proto/descriptor.proto:
syntax = "proto2";
package proto2;

message Features {
  ...
}

message FileOptions {
  optional Features features = ..;
}

message MessageOptions {
  optional Features features = ..;
}
// All the other `*Options` protos.

在实现层面,特性继承的行为完全等价于 MergeFrom

void InheritFrom(const Features& parent, Features* child) {
  Features tmp(parent);
  tmp.MergeFrom(child);
  child->Swap(&tmp);
}

即:子实体的显式设置优先,父实体只填充子实体未设置的字段。这个实现策略有一个直接的好处——任何自定义后端(custom backend)都能用现成的 MergeFrom 语义忠实地实现继承,不需要额外的继承引擎。这也解释了为什么 edition 的默认值本身可以被表示成一个 Features 实例(见第七节):edition 默认、文件选项、消息选项、字段选项全部走同一条合并路径。

五、Target 属性:限制特性可挂载的实体

继承虽然方便,但有两个滥用风险:

  1. 过度使用继承会让 .proto 文件的简单重构变难;
  2. 并非所有特性在所有实体上都有意义——例如 features.enum = OPEN 挂在字段上就是无意义的。

为此设计引入了 target 属性(概念上类似 Java 注解的 @Target),用来限定一个特性可以挂载到哪些实体上:

enum FeatureTargetType {
  FILE = 0;
  MESSAGE = 1;
  ENUM = 2;
  FIELD = 3;
  ...
};

例如把 enum 特性限定在 ENUM 目标上:

message Features {
  ...
  enum EnumType {
    OPEN = 0;
    CLOSED = 1;
  }
  optional EnumType enum = 2 [
      target = ENUM
  ];
}

在当前仓库的最终实现中,target 从"单个枚举值"演进为 targets 重复选项,并且几乎每个通用特性都同时允许在 FILE 级设置(作为默认)和在具体实体级覆盖。例如 descriptor.proto 中:

optional FieldPresence field_presence = 1 [
  retention = RETENTION_RUNTIME,
  targets = TARGET_TYPE_FIELD,
  targets = TARGET_TYPE_FILE,
  ...
];

protoc 在解析时会校验特性选项只出现在合法 target 上,违反时直接报编译错误,把"挂在错误实体上"的写法挡在编译期。

六、Retention:让特性不必保留到运行时描述符

为了减小 protobuf 运行时(runtime)描述符(descriptor)的体积,特性被允许声明 retention(保留级别),同样是借鉴 Java 注解的 retention 概念:

enum FeatureRetention {
  SOURCE = 0;
  RUNTIME = 1;
}
  • SOURCE:特性只在源码/编译阶段有意义,可以不进入运行时描述符,例如纯粹的代码生成风格选项;
  • RUNTIME:特性会影响序列化/反序列化行为,必须保留到运行时供各语言 runtime 使用。

descriptor.proto 的实际定义中,所有影响线上行为的核心特性——field_presenceenum_typerepeated_field_encodingutf8_validationmessage_encodingjson_format——均标注了 retention = RETENTION_RUNTIME;而像 cpp_features.proto 中仅供 C++ 生成器使用的 string_typeenum_name_uses_string_view 也标注为 RUNTIME(C++ 生成的代码依赖它们),这体现了"哪些语言需要看到该特性"这一工程权衡。

七、Edition 的规范化表示:edition_defaults 与默认值求解算法

这是设计文档中最精妙的部分。一个 edition,实质上就是一个 Features proto 实例,并作为用 MergeFrom 做继承的基底。这使 protoc 与各语言生成器可以直接复用既有格式(如 text-format)来表达"某个 edition 下各特性的取值"。

设计者明确指出:直觉上"字段默认值(field defaults)"似乎是对的载体,但行不通——因为默认值是 edition 相关的。因此方案是在 protoc 提供的 features 定义中加入一个扩展:

message Features {
  // ...
  message EditionDefault {
    optional string edition = 1;
    optional string default = 2;  // Textproto value.
  }

  extend FieldOptions {
    // Ideally this is a map, but map extensions are not permitted...
    repeated EditionDefault edition_defaults = 9001;
  }
}

注意设计文档里特意留了一句注释:理想情况下这应该是一个 map(edition → default),但 Protobuf 不允许 map 类型的扩展,所以用 repeated 代替。

默认值求解算法(为特定文件 foo.proto 在特定 edition current 下构建默认特性集):

  1. 构造一个全新的 Features feats;
  2. Features 的每个字段,读取其 Features.edition_defaults 选项(记作 defaults),并按 edition 名称的全序(参见 Life of an Edition)排序;
  3. 二分查找 defaults小于或等于 current 的最新一个 edition
    • 若字段是单值标量类型,直接用该 edition 指定的值作为 feats 中该字段的值;
    • 否则(复合/扩展消息),把所有早于 current 的 edition 的值从最旧的开始依次 merge 起来,作为该字段的值;
  4. 对这个算法而言,Features 的每个字段都被视为 required通过 edition 默认值搜索找不到显式默认值时,应导致编译错误——因为这意味着该文件的 edition 太旧,不认识这个特性;
  5. foo.proto 通过 import 可见的每个 Features 扩展(即语言专属特性消息),对扩展消息执行同样的算法,再把结果加入 feats

该算法刻意保证了三个性质:

  • 语言作用域的特性通过 import 被发现——而这些特性本来就必须先被 import 才能在文件中使用;
  • 每个值都被显式设置,从而能正确拒绝太旧的文件;
  • "来自未来"的文件不会被算法一票否决,为提供类似 --allow-experimental-editions 的开关留出空间,方便后端先行实现新 edition。

仓库中的落地实现

当前仓库中,edition_defaults 已从"string edition + string textproto value"演进为强类型形式:edition: EDITION_2023, value: "EXPLICIT" 这样的 FieldOptions.EditionDefault 直接写在 descriptor.proto 的特性定义里,例如 field_presence 同时携带了三条默认:

edition_defaults = { edition: EDITION_LEGACY, value: "EXPLICIT" },
edition_defaults = { edition: EDITION_PROTO3, value: "IMPLICIT" },
edition_defaults = { edition: EDITION_2023, value: "EXPLICIT" }

而文档算法第 4 条"太旧应报错"的约束,由 feature_resolver.cc 中的校验逻辑落地:ValidateFieldDescriptor 检查每个特性字段必须声明 feature_support(含 edition_introduced),并验证 edition_defaults 中每个默认值都落在"引入 edition 之后、移除 edition 之前"的窗口内,违反则报错:

if (d.edition() < support.edition_introduced()) {
  return Error("Feature field ", field.full_name(),
               " has a default specified for edition ", d.edition(),
               ", before it was introduced.");
}

此外 editions/defaults.bzl 维护了构建系统侧的各 edition 默认特性值,generated_files_test.ccdefaults_test.cc 则持续验证"edition 默认值 → 生成文件"的一致性;protoc 侧对实验性 edition 的支持开关可在 command_line_interface.cc 的 flag 处理中查到,正是算法性质第 3 条所预留的能力。

八、Edition Zero 特性集:从设计草案到最终 FeatureSet

设计文档给出了 edition zero 的完整 Features 消息草案,定义了首批五个核心特性及其 edition 2023 默认值:

message Features {
  enum FieldPresence {
    EXPLICIT = 0;
    IMPLICIT = 1;
    LEGACY_REQUIRED = 2;
  }
  optional FieldPresence field_presence = 1 [
      retention = RUNTIME,
      target = FIELD,
      (edition_defaults) = {
        edition: "2023", default: "EXPLICIT"
      }
  ];

  enum EnumType {
    OPEN = 0;
    CLOSED = 1;
  }
  optional EnumType enum = 2 [
      retention = RUNTIME,
      target = ENUM,
      (edition_defaults) = {
        edition: "2023", default: "OPEN"
      }
  ];

  enum RepeatedFieldEncoding {
    PACKED = 0;
    EXPANDED = 1;
  }
  optional RepeatedFieldEncoding repeated_field_encoding = 3 [
      retention = RUNTIME,
      target = FIELD,
      (edition_defaults) = {
        edition: "2023", default: "PACKED"
      }
  ];

  enum StringFieldValidation {
    REQUIRED = 0;
    HINT = 1;
    SKIP = 2;
  }
  optional StringFieldValidation string_field_validation = 4 [
      retention = RUNTIME,
      target = FIELD,
      (edition_defaults) = {
        edition: "2023", default: "REQUIRED"
      }
  ];

  enum MessageEncoding {
    LENGTH_PREFIXED = 0;
    DELIMITED = 1;
  }
  optional MessageEncoding message_encoding = 5 [
      retention = RUNTIME,
      target = FIELD,
      (edition_defaults) = {
        edition: "2023", default: "LENGTH_PREFIXED"
      }
  ];

  extensions 1000;  // for features_cpp.proto
  extensions 1001;  // for features_java.proto
}

对照当前仓库的最终实现 descriptor.proto,可以看出设计草案与落地版本之间的演进脉络(草案 → 实现):

草案特性 最终 FeatureSet 字段 草案默认值(2023) 最终默认值 备注
field_presence field_presence EXPLICIT EXPLICIT(EDITION_2023)、IMPLICIT(PROTO3)、EXPLICIT(LEGACY) 增加了 UNKNOWN 哨兵值 0,编号整体后移
enum enum_type OPEN OPEN(PROTO3)、CLOSED(LEGACY) 更名为 enum_type,补全 proto2/legacy 默认
repeated_field_encoding repeated_field_encoding PACKED PACKED(PROTO3)、EXPANDED(LEGACY) 同上
string_field_validation utf8_validation REQUIRED VERIFY(PROTO3)、NONE(LEGACY) 重命名并重定为 UTF-8 校验强度
message_encoding message_encoding LENGTH_PREFIXED LENGTH_PREFIXED 保持一致
—(草案未含) json_format ALLOW(PROTO3)、LEGACY_BEST_EFFORT(LEGACY) 落地时新增的 JSON 处理特性

所有草案特性在实现中都补充了 feature_support = { edition_introduced: EDITION_2023 },且 targets 均同时覆盖 FILE 与对应实体类型,允许"文件级默认 + 实体级覆盖"的继承模式。更完整的 Edition Zero 特性语义讨论(presence 纪律、closed/open enum 定义、proto2/proto3 差异收敛等)可参考同目录下的 edition-zero-features.md

九、小结与延伸阅读

这份设计文档确立了 Protobuf Editions 的特性表达范式,其核心决策可以归纳为四条:

  1. 特性 = 自定义选项:复用 proto2 custom options,不引入任何新语法;
  2. 继承 = MergeFromFeatures 消息挂在所有 *Options 上,子实体覆盖父实体,后端实现零额外成本;
  3. target + retention 双约束:编译期挡掉挂载在非法实体上的特性,运行期控制特性是否进入描述符以控制体积;
  4. edition = Features 实例 + edition_defaults 扩展:默认值求解走"排序 + 二分 + 逐版本 merge"算法,显式拒绝过旧文件、宽容对待未来文件。

结合仓库中的实现文件(descriptor.protofeature_resolver.cc、各语言 *_features.proto),可以清楚地看到 2022 年批准的这份设计如何演进为今天 FeatureSet 的完整形态。围绕该文档的延伸阅读:

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

项目优选

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