首页
/ Protocol Buffers Options Attributes(target / retention)设计解析:为 Options 划定作用域与生命周期

Protocol Buffers Options Attributes(target / retention)设计解析:为 Options 划定作用域与生命周期

2026-09-04 18:16:38作者:齐添朝

本文围绕 Protocol Buffers 的设计文档《Protobuf Design: Options Attributes》(docs/design/editions/protobuf-design-options-attributes.md)展开,完整解读 targetretention 这两个"选项属性"的语义、用法与在 Editions 特性(features)体系中的定位,并结合当前仓库中 descriptor.proto 的定义、protoc 编译器校验逻辑 以及配套测试,说明这两个机制从设计提案到落地实现的完整链路。读完本文,你将掌握:如何为自己的 options/feature 字段声明合法作用实体、如何让 protoc 按声明拒绝非法放置,以及如何在代码生成器中正确剥离 RETENTION_SOURCE 选项。

背景:Editions 特性体系催生"选项的作用域"问题

Protobuf Editions 项目的规划是:用**自定义选项(custom options)**来建模"特性"(features),并鼓励各语言绑定基于 options 构建自己的自定义能力(参见 protobuf-editions-design-features.md)。这一路线带来两个新问题,正是该设计文档要解决的:

  1. 作用域问题:历史上 options 只作用于固定的实体类型(文件选项、消息选项……),而"特性"几乎可以出现在任意实体上。如果不加约束,每个 feature 都会"弥漫"到所有实体,用户和工具都会被大量无意义的选项噪声淹没。
  2. 体积问题:为了让代码生成器或 protoc 本身携带的信息不必进入最终二进制,需要一种机制让部分选项在生成物中被剔除,从而减小运行时 descriptor 的体积。

为此,设计提案(作者 @kfm,2022-08-26 批准)为 options 引入了两个属性:target(允许该选项绑定到哪些实体)与 retention(选项的保留级别)。文档明确指出:这两个属性加在非 option 的字段上(即普通数据字段)时是 no-op(无操作)

Target 属性:声明 option 可以合法绑定到哪些实体

枚举定义与默认行为

设计文档提出的原始定义(命名与 Java 注解的 @Target 属性一脉相承):

message FieldOptions {
  ...
  optional OptionTargetType target = 17;

  enum OptionTargetType {
    TARGET_TYPE_UNKNOWN = 0;
    TARGET_TYPE_FILE = 1;
    TARGET_TYPE_EXTENSION_RANGE = 2;
    TARGET_TYPE_MESSAGE = 3;
    TARGET_TYPE_FIELD = 4;
    TARGET_TYPE_ONEOF = 5;
    TARGET_TYPE_ENUM = 6;
    TARGET_TYPE_ENUM_VALUE = 7;
    TARGET_TYPE_SERVICE = 8;
    TARGET_TYPE_METHOD = 9;
  };
}

规则非常简洁:

  • TARGET_TYPE_UNKNOWN(或未设置 target)视为"未指定",protoc 允许该选项出现在任意实体上;
  • 一旦显式指定 target,protoc 只允许该选项出现在文件级(file level)或其目标实体上,其他任何放置都会触发编译错误。

文档示例:features.enum 的作用域约束

文档给出的典型示例是 Editions 的 Features 消息中声明一个仅允许作用于枚举的 feature:

message Features {
  ...

  enum EnumType {
    OPEN = 0;
    CLOSED = 1;
  }
  optional EnumType enum = 2 [
      target = TARGET_TYPE_ENUM
  ];
}

于是下面的使用方式中,前两处合法、第三处非法:

// foo.proto
edition = "tbd"

option features.enum = OPEN;  // allowed at FILE scope

enum Foo {
  option features.enum = CLOSED;  // allowed at ENUM scope
  A = 2;
  B = 4;
}

message Bar {
  option features.enum = CLOSED;  // disallowed at Message scope

  enum Baz {
    C = 8;
  }
}

这里体现了文档 Discussion 一节中的关键设计决策:target 最初同时承担"标识语义实体"和"决定特性继承粒度"双重职责;经过讨论后收窄为——target 只指定选项可作用的语义实体,特性可以同时设置在 FILE 级与其语义实体上,中间的嵌套层级在初始版本一律拒绝。这种"文件级 + 语义实体级、其余全拒"的规则,为将来任意层级的特性继承保留了前向兼容空间,但当前并不承诺该行为。

当前仓库的实现形态:optional target 演进为 repeated targets

设计文档本身在 "Original Approved Proposal" 一节交代了最终实现与最初批准稿的差异(retention 枚举增加了 UNKNOWN 类型、枚举从顶层移入 FieldOptions 内部并加 TARGET_TYPE_/RETENTION_ 前缀、去掉了不再需要的 STREAM 条目)。当前仓库中 descriptor.protoFieldOptions 已进一步演化为 repeated 形式:

// If set to RETENTION_SOURCE, the option will be omitted from the binary.
enum OptionRetention {
  RETENTION_UNKNOWN = 0;
  RETENTION_RUNTIME = 1;
  RETENTION_SOURCE = 2;
}

optional OptionRetention retention = 17;

// This indicates the types of entities that the field may apply to when used
// as an option. If it is unset, then the field may be freely used as an
// option on any kind of entity.
enum OptionTargetType {
  TARGET_TYPE_UNKNOWN = 0;
  TARGET_TYPE_FILE = 1;
  TARGET_TYPE_EXTENSION_RANGE = 2;
  TARGET_TYPE_MESSAGE = 3;
  TARGET_TYPE_FIELD = 4;
  TARGET_TYPE_ONEOF = 5;
  TARGET_TYPE_ENUM = 6;
  TARGET_TYPE_ENUM_ENTRY = 7;
  TARGET_TYPE_SERVICE = 8;
  TARGET_TYPE_METHOD = 9;
}

repeated OptionTargetType targets = 19;

两个值得注意的变化:

  • 字段号 18 被显式保留:descriptor.proto#L856reserved 18; // reserve target, target_obsolete_do_not_use,说明最初的单值 optional target = 18 方案确实存在过,后被 repeated OptionTargetType targets = 19 取代——这正是文档 "Alternatives" 中讨论的"Use a repeated target"方案被采纳后的最终形态。从源码结构看,一个选项字段现在可以同时声明多个合法作用实体(如 [targets = TARGET_TYPE_MESSAGE, targets = TARGET_TYPE_FILE])。
  • 枚举值 7 的命名从提案中的 TARGET_TYPE_ENUM_VALUE 变为 TARGET_TYPE_ENUM_ENTRY,与 descriptor 体系的用语(EnumValueDescriptor 亦称 enum entry)保持一致。

Retention 属性:让选项止步于源码,减小生成物体积

语义定义

为压缩运行时 protobuf descriptor 的体积,特性可以声明保留规则(同样借鉴 Java 注解的 @Retention):

enum FeatureRetention {
  RETENTION_UNKNOWN = 0;
  RETENTION_RUNTIME = 1;
  RETENTION_SOURCE = 2;
}
  • RETENTION_SOURCE:仅供代码生成器或 protoc 自身参考的选项;
  • RETENTION_RUNTIME(未设置时的默认值):选项会进入生成的 descriptor——这与目前所有 options 的行为一致。

文档对此设定了强制性要求:凡是会输出(generated)descriptor 的代码生成器,必须将其生成的 descriptor 中所有 SOURCE retention 选项省略/剥离。 文档给出的示例是把 C++ 专属的 namespace 选项声明为"纯源码级"信息:

message Cpp {
  enum StringType {
    STRING = 1;
    STRING_VIEW = 0;
    CORD = 2;
  }

  optional string namespace = 2 [
      retention = RETENTION_SOURCE,
      target = TARGET_TYPE_FILE
  ];
}

编译器侧的剥离机制:retention.h

仓库中这一职责由 retention.h 中的 API 族承担,分两层:

  • 整文件剥离StripSourceRetentionOptions(const FileDescriptor&, bool include_source_code_info) 返回一份剔除了所有 RETENTION_SOURCE 选项的 FileDescriptorProto;若 include_source_code_info 为真,还会填充 source code info 并同步剥离其中对应 source-retention 选项的部分(避免源码位置信息泄露被剔除选项的存在)。
  • 单实体剥离:针对 FileDescriptorDescriptor(message)、EnumDescriptorFieldDescriptorOneofDescriptorServiceDescriptorMethodDescriptorExtensionRange 各自提供 StripLocalSourceRetentionOptions 重载。头文件注释明确提示:多数代码生成器不需要这些细粒度接口,它们只用于"只想处理单个实体"的特殊场景。

protoc 主流程中,command_line_interface.cc 在填充 source_file_descriptors 时会移除 source-retention 选项;面向插件的接口同样如此——code_generator.h#L162 的注释提醒插件开发者:发给运行时的 descriptor 之前要处理 source-retention 特性。

测试证据

  • unittest_retention.proto 系统性地覆盖了 retention 属性的三种声明位置:直接标在自定义选项扩展字段上(optional int32 source_retention_option = 504878676 [retention = RETENTION_SOURCE])、标在 options 消息内部嵌套字段上(optional int32 source_retention_field = 3 [retention = RETENTION_SOURCE];),以及嵌套在 repeated 消息字段内——并在 file、message、field、oneof、enum、enum entry、extension range、service、method 九类实体上逐一施加这些选项,构成回归测试的基础数据。
  • command_line_interface_unittest.cc 中的用例直接验证了核心承诺:编译产物中 runtime_retention_option 仍然存在,而 source_retention_option "should have been stripped"(应当已被剥离)。

Target 校验的源码级实现

设计文档承诺"非法放置产生编译错误",这一行为在 command_line_interface.cc 中落地为一段递归校验逻辑:

  1. 兼容性判定command_line_interface.cc#L1173-L1178):
// Indicates whether the field is compatible with the given target type.
bool IsFieldCompatible(const FieldDescriptor& field,
                       FieldOptions::OptionTargetType target_type) {
  const RepeatedField<int>& allowed_targets = field.options().targets();
  return allowed_targets.empty() ||
         absl::c_linear_search(allowed_targets, target_type);
}

与文档语义完全对应:targets 为空即视为"任意实体可用",否则线性查找当前实体类型是否在白名单中。

  1. 递归深入command_line_interface.cc#L1209-L1243):ValidateTargetConstraintsRecursive 遍历 options 消息中所有已设置的字段,对不兼容字段通过 DescriptorPool::ErrorCollector 记录错误,错误信息格式为 ``Option <full_name> cannot be set on an entity of type <entity>.(实体名由 TargetTypeString翻译为file/message/enum entry等可读字符串);若字段本身是 message 类型(即嵌套的 options 消息,如options消息内再嵌套一层带targets` 的字段),则对其(repeated 时逐元素)递归校验。
  2. 入口与动态展开command_line_interface.cc#L1250-L1269):ValidateTargetConstraints 先把 options 消息转成 DynamicMessage 以获得对自定义选项(普通反射拿不到扩展字段的可见性)的检查能力;若 descriptor pool 中找不到该 options 消息类型(说明用户 proto 不依赖 descriptor.proto,自然没有自定义选项),则跳过 DynamicMessage 的开销直接递归校验。各实体类型到 OptionTargetType 的映射由一组 GetTargetType 重载完成(FileDescriptor* → TARGET_TYPE_FILEDescriptor* → TARGET_TYPE_MESSAGEEnumValueDescriptor* → TARGET_TYPE_ENUM_ENTRY 等,见 command_line_interface.cc#L1277-L1312)。

对应的单测(command_line_interface_unittest.cc)构造了覆盖全部九种 target 的选项消息,包括同一字段声明多个 target 的组合(targets = TARGET_TYPE_MESSAGE, targets = TARGET_TYPE_FILE),验证了多值 targets 下的合法/非法放置判定。

设计决策复盘:为什么这样设计

文档的 Discussion 与 Alternatives 两节记录了完整的取舍过程,对理解"为什么最终形态是现在这样"很有价值:

  • target 与继承解耦:初版设计中 target 同时表达"语义实体"和"继承粒度"。鉴于对"继承被滥用"的担忧,最终定义收窄为只表达语义实体;FILE 级与语义实体级放行、中间层级全部拒绝,是刻意选择的保守起点。
  • optionalrepeated:提案阶段选择 optional target,理由是日后安全地升级为 repeated——当前仓库的 targets(field 19)与 reserved 18 注释正是这条演进路线的实物证据。
  • 命名直接沿用 Java 注解targetretention 的命名刻意对齐 Java annotations,讨论中考虑过其他命名但没有更优解,"与已有概念相似"胜出。
  • 备选方案一:按 target 语义位置允许层级推导。即只声明语义适用的层级,由 protoc 隐式允许该选项出现在所有"词法上可分组该类型"的实体上。优点是工具可以区分"分组用途"与"语义用途"(有利于大规模变更时减少扰动),对用户也更通用(任何 FIELD feature 自动可用于 message)。缺点是所有 target 都被强制允许作用于作用域实体——未被采纳。
  • 备选方案二:用自定义选项实现("We Must Go Deeper")。优点是无需修改 descriptor.proto;缺点是要给基础能力强加额外语法、额外 import,且语言级特性将不得不用魔法语法或侧表,无法与用户自定义 feature 使用一致的表达。
  • 备选方案三:在 protoc 里硬编码行为。优点是"不可能被误用";缺点是不可扩展、需要用户记住的特例更多。
  • 备选方案四:什么都不做,去吃冰淇淋。被否决的理由是"feature 泛滥到其并不适用的实体上,代价太高"。

Motivation:不止服务于 Editions

设计文档的 Motivation 一节强调,虽然这两个属性的直接动机是 Editions 的 features,但它们具备足够通用性,因此被直接加进 FieldOptions。文档举了一个历史例子:若 ExtensionRangeOptions::Metadata 只有 SOURCE retention,本可以显著节省二进制体积;过去这类行为只能按字段逐个特判(special-case),虽能工作但缺乏可扩展性——retention 属性正是把这种"按字段打补丁"的做法升级为通用声明机制。

面向插件与代码生成器的实践要点

综合文档规定与仓库实现,为插件/生成器作者归纳以下要点:

  1. 声明期:在你的 options 定义中用 targets 约束合法作用实体(可多值),用 retention = RETENTION_SOURCE 标记仅供生成期消费的字段;两者加在普通数据字段上无效果,可放心混用。
  2. 消费期RETENTION_SOURCE 选项只保证存在于 protoc 解析出的完整 descriptor 与传给插件的输入中;任何会被持久化或发给运行时的 descriptor(例如随产物发布的 FileDescriptorSet)都必须先经过 retention.h 提供的 StripSourceRetentionOptions 族处理。
  3. 验证期:非法放置不是静默忽略而是编译错误,错误信息形如 Option .cpp.namespace cannot be set on an entity of type `message`.,可在 CI 中直接断言(参考 command_line_interface_unittest.cc 中对 targets 的测试模式)。
  4. 版本适配:若你的 proto 面向不同年代的 protoc,注意最初批准的提案与当前实现存在差异(枚举位置、前缀、UNKNOWN 值、optional/repeated 形态,以及 TARGET_TYPE_ENUM_VALUETARGET_TYPE_ENUM_ENTRY 的改名),以你目标工具链版本的 descriptor.proto 为准。

小结

targetretention 是 Protocol Buffers 将"选项元信息"(options about options)纳入语言本身的两把钥匙:前者在编译期把选项限制在其语义归属的实体上,让 Editions 的 features 不至于扩散到所有实体;后者让纯生成期信息止步于源码,避免污染运行时 descriptor。设计文档以简洁的规则、明确的默认值(未设置 = 全实体可用 / RUNTIME)与可演进的字段形态(optional targetrepeated targets),加上 protoc 校验逻辑retention 剥离 API 的完整落地,构成了一套对插件作者透明、对运行时友好的选项治理机制。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
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
981
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384