首页
/ Protobuf Editions 设计解析:用特性开关把 Schema 语言越磨越严

Protobuf Editions 设计解析:用特性开关把 Schema 语言越磨越严

2026-09-04 14:14:26作者:钟日瑜

本文围绕 Protobuf 官方设计文档 stricter-schemas-with-editions.md 展开,解读其提出的十一种"语言严格化"提议及配套的 feature 棘轮(ratchet)迁移策略;并结合当前仓库中 Edition 2024 真正落地的 features.enforce_naming_style 特性(见 descriptor.protodescriptor.cc)说明这类严格化规则是如何在编译器中实现、默认值如何按 Edition 逐版翻转的。读完本文,你将掌握 Editions 特性集机制的工作方式,以及如何在自己的 .proto 文件中提前适配 2024/2026 版的命名与结构约束。

为什么 Protobuf 语言需要"变严"

该设计文档开宗明义地指出:Protobuf 语言在语法空间的一些角落出乎意料地宽松——这些角落在实际使用中极少被触及,却给后端(code generator)和运行时(runtime)平添了大量复杂度。文档的定位是一份"Editions 使用案例备忘录",而非完整设计文档,其核心套路是:为每一个宽松角落引入一个布尔(或枚举)feature,起始默认值兼容旧行为,再在后续 Edition 中翻转默认值——即"棘轮式"收紧。

这一机制的载体就是 Edition 特性集(FeatureSet)。从当前仓库可以看到,FeatureSet 消息中每个特性都带有 feature_support(引入的 Edition)与若干 edition_defaults(各 Edition 的默认值),例如 descriptor.proto 中已落地的命名风格特性:

enum EnforceNamingStyle {
  ENFORCE_NAMING_STYLE_UNKNOWN = 0;
  STYLE2024 = 1;
  STYLE_LEGACY = 2;
  STYLE2026 = 3;
}
optional EnforceNamingStyle enforce_naming_style = 7 [
  retention = RETENTION_SOURCE,
  targets = TARGET_TYPE_FILE,
  targets = TARGET_TYPE_EXTENSION_RANGE,
  targets = TARGET_TYPE_MESSAGE,
  targets = TARGET_TYPE_FIELD,
  targets = TARGET_TYPE_ONEOF,
  targets = TARGET_TYPE_ENUM,
  targets = TARGET_TYPE_ENUM_ENTRY,
  targets = TARGET_TYPE_SERVICE,
  targets = TARGET_TYPE_METHOD,
  feature_support = {
    edition_introduced: EDITION_2024,
  },
  edition_defaults = { edition: EDITION_LEGACY, value: "STYLE_LEGACY" },
  edition_defaults = { edition: EDITION_2024, value: "STYLE2024" },
  edition_defaults = { edition: EDITION_2026, value: "STYLE2026" }
];

注意几个关键细节:

  • retention = RETENTION_SOURCE 表示该特性只在编译期(源码解析/校验)起作用,不会进入运行时的 descriptor 二进制,因此严格化检查几乎零运行时成本;
  • 该特性可作用在文件、message、field、oneof、enum、enum entry、service、method 等多个层级,即文档中"feature 可应用到任意实体"(can be applied to any entity)的设想已经体现——子实体可以覆写父级默认值;
  • edition_defaults 清晰地演示了棘轮路径:Legacy 下是 STYLE_LEGACY(不检查),Edition 2024 起默认 STYLE2024,Edition 2026 起默认 STYLE2026(更严)。

文档提出的全部规则都遵循同样的形态:feature 名、可作用的实体、初始为宽松的默认值、未来 Edition 收紧。下面按原文档的章节顺序逐条展开。

实体命名:三种大小写风格的正则约束

文档指出,Protobuf 目前只要求标识符匹配 ASCII 规则 [A-Za-z_][A-Za-z0-9_]*,不施加任何命名风格约束。这给后端带来三类麻烦:

  1. 后端必须在 PascalCase、camelCase、snake_case、SHOUTY_CASE 之间互相转换,且"正确"地做这件事相当棘手;
  2. 多余的下划线(PascalCase 名称中夹杂的下划线、前缀/后缀下划线、连续下划线)会让大小写转换出错,还可能与后端生成的私有名称冲突;
  3. Protobuf 实际上不支持非 ASCII 标识符(Java 等语言不支持),这一点却没有被明确写死。

因此文档为 Protobuf 的三种命名风格各给出一个更严的正则:

风格 正则 适用实体
PascalCase ([A-Z][a-zA-Z0-9]*)+ Message、Enum、Service、Method
snake_case [a-z][a-z0-9]*(_[a-z0-9]+)* Field(含 extension)、Package 组件
SHOUTY_CASE [A-Z][A-Z0-9]*(_[A-Z0-9]+)* Enum value

这些模式的核心目的是拒绝多余的下划线、统一 ASCII 字母的大小写;文档强调"仅支持 ASCII"是出于对目标语言的最大可移植性考虑。另外 option 名不在此列——因为 option 本身在 proto 中定义成 field,会自动被 field 规则覆盖。

文档提议的迁移路径是引入布尔特性 feature.relax_identifier_rules(可应用于任意实体):置位时,编译器拒绝包含不满足上述约束标识符的 .proto 文件;默认 true,未来 Edition 翻转为 false。

仓库中的落地情况:这条规则是 Editions 严格化中真正第一个实现的部分,即 enforce_naming_style 特性。其校验逻辑位于 descriptor.cc

if (!has_errors() && pool_->enforce_naming_style_) {
  internal::VisitDescriptors(
      *result, proto, & {
        if (IsStyleOrGreater(&descriptor, FeatureSet::STYLE2024)) {
          ValidateNamingStyle(&descriptor, desc_proto);
        }
      });
}

从源码结构看,DescriptorPool 上有一个 enforce_naming_style_ 总开关(由 command_line_interface.ccdescriptor_pool->EnforceNamingStyle(true) 打开),逐实体遍历 descriptor 树,只有当该实体的 enforce_naming_style 特性值达到 STYLE2024 及以上(且不是 STYLE_LEGACY,见 descriptor_builder.hIsStyleOrGreater 的枚举序判断)时才执行 ValidateNamingStyle。具体校验函数(descriptor.cc 附近)例如:

  • 文件级:package 名必须通过 IsValidLowerSnakeCaseName(空 package 跳过检查);
  • Message 级:名称必须通过 IsValidTitleCaseName(PascalCase);
  • 还有针对 field/enum/method 等实体的重载,以及字段前后缀与 map entry 生成名冲突的检测。

每条违规错误信息末尾都会附上可操作的豁免提示(kNamingStyleOptOutMessage):

(features.enforce_naming_style = STYLE_LEGACY can be used to opt out of this check)

仓库自带的测试用例直观展示了会触发校验的写法,例如 edition2023_naming_style_field.proto

edition = "2023";

// LINT: LEGACY_NAMES

package protobuf_editions_test.edition2023;

message BadFieldMessage {
  string badFieldName = 1;
}

badFieldName 不是 snake_case,在 2024 版默认的命名风格下即构成违规;editions/codegen_tests/ 目录下还有 edition2023_naming_style_enum.protoedition2023_naming_style_enum_value.protoedition2023_naming_style_message.protoedition2023_naming_style_service.protoedition2023_naming_style_method.protoedition2023_naming_style_oneof.protoedition2023_naming_style_extension.protoedition2023_naming_style_file.proto 等一组姊妹文件,分别覆盖文档正则表中各类实体。

关键字用作标识符

现状是 Protobuf 允许把关键字当标识符用,这让 parser 变得比必要的更复杂,且遮蔽(shadowing)行为没有良好定义。文档举了个例子:

message Foo {
  message int32 {}
  optional int32 foo = 1;
}

这里的 int32 到底是类型还是消息名?更棘手的是关键字与类型名都可能出现的上下文,例如 optional foo = 1; 在 proto3 里是"类型为 optional 的非 optional 字段",parser 要看到 = 才能确定。

文档的处置方案分三步:

  1. 将下列关键字全部变成真保留名,不能再用作标识符:
bool      bytes    double  edition   enum      extend    extensions  fixed32
fixed64   float    group   import    int32     int64     map         max
message   oneof    option  optional  package   public    repeated    required
reserved  returns  rpc     service   sfixed32  sfixed64  sint32      sint64
stream    string   syntax  to        uint32    uint64    weak
  1. 引入 #optional 形式的语法,用于把关键字转义为标识符,且只允许用于关键字、不能用于普通标识符;
  2. 迁移特性为布尔 feature.keywords_as_identifiers,可作用于任意实体,置位时拒绝使用关键字名作标识符的文件,按 true → false 迁移。#optional 转义语法本身不需要特性门控。

文档还给出了未来新增关键字的最佳流程:先加一个 feature.xxx_is_a_keyword 特性、初始 true、在某个 Edition 中翻成 false(从此该词在校验意义上成为关键字);如果新词在语法上歧义不大,也可以先作为 Rust 意义上的"上下文关键字"(contextual keyword)使用,而不必等 Edition。文档明确引用 Rust 的做法作为指引:Rust 讨厌上下文关键字因为它复杂化 parser,所以关键字先以上下文形式引入,下一个 Rust edition 中变成正式保留字。

非空 Package

目前空 package 在技术上是被允许的。文档认为应把这一能力从语言中彻底移除,要求每个文件都声明 package。迁移特性为 feature.allow_missing_package:初始 true,随后翻转为 false。

值得注意的是命名风格的实现里与之相关的处理:ValidateNamingStyle 的文件级重载中有一句 // Ignore empty packages for style checks.——即 package 风格校验对空 package 直接跳过(见 descriptor.cc)。从源码结构看,空 package 的彻底禁止属于文档提议、而尚未在命名风格特性中一刀切的部分。

reserved 中的非法名称

现状 reserved "foo-bar"; 会被接受。但 "foo-bar" 本身不是一个合法的字段名,理应被拒绝。文档的理想方案是彻底移除该字符串语法,只允许标识符形式,即 reserved foo, bar;。迁移特性为 feature.allow_strings_in_reserved:初始 true,翻转为 false。

名称解析:几乎全部使用全限定名

现状 Protobuf 采用了一套受 C++ 启发(而且"比 C++ 的还简单不了多少")的复杂名称解析方案:名字可以是不完全限定的相对路径,解析器要在当前包、当前文件等多个作用域里逐段匹配。文档提议改为:每个名字要么是一个单一标识符,要么是完全限定的全限定名——即向 Go 式的名称解析靠拢,实现与解释都显著更简单。

具体规则是:当名字是单一标识符时——

  • 它必须是当前文件顶层定义的类型的名字;
  • 如果它用作 field 的类型,允许它是当前 message 内定义的消息或 enum 的名字;此放宽不适用于 extension field

由于多段路径必须全限定,.foo.Bar 这种以点开头的语法就不再需要——除非用于指代"无 package 文件中定义的消息"。除此之外一律禁止以 . 开头的名字。

迁移特性为 features.use_cpp_style_name_resolution:初始 true,翻转为 false。文档还展望了一个更进一步的方案:如果有严格标识符命名,就能从名字区分消息与 package(Foo.Bar 一定根植于消息而非 package),那时甚至可以规定"小写字母开头的名字是全限定的,否则相对于当前包、且只能找到当前文件中定义的东西"。同时文档也点明了与 Go 的差异:不允许不写全限定名就引用其他 package 中的东西,理由是大型包中源码溯源(source-diving)太难,找不到定义在哪。

枚举值唯一

现状允许 enum 别名:

enum Foo {
  BAR = 5;
  BAZ = 5;
}

文档认为这给部分后端带来显著复杂度,并在 textproto 和 JSON 中导致怪异行为,应当禁止。迁移特性为 features.allow_enum_aliases:true → false。

import 必须被使用

文档提议采纳 Go 的规则:所有非 public import 都必须被使用,即每个 import 至少为该文件提供一个被引用的类型。迁移特性为 features.allow_unused_imports:true → false。

"下一个字段号" 显式保留

文档提到,一些 linter 已经在检查 // Next ID: N 之类的惯用注释。它建议把这一惯例写进语言:每个 message 的第一条内容都应该是 reserved N to max;,语义是 N 为下一个从未使用的字段号。因为它是 message 的第一条 production,所以(原文此处略有截断)工具可以稳定地解析它。

文档还给出两个可选的加强方案:

  • 要求每个字段号要么被使用、要么被保留(外加唯一一条 N to max; 保留);
  • 或者要求最大已用字段号以下的所有字段号都必须被保留——字段号间的空洞(gaps)通常是坏味道。

同样规则也适用于 enum 值。迁移特性为 features.allow_unused_numbers:true → false。

禁止隐式字符串拼接

Protobuf 会在任何允许带引号字符串的位置隐式拼接相邻字符串,例如 option foo = "bar " "baz";。文档指出这在 reserved 上已经酿成过事故:一旦漏写逗号,reserved "foo" "bar"; 就变成了 reserved "foobar";。迁移特性为 features.concatenate_adjacent_strings:true → false。

package 声明必须在文件最前

现状 package 声明可以出现在 syntax/edition 之后的任意位置。文档提议学 Go:package 必须是 edition 之后的第一条声明。迁移特性为 features.package_anywhere:true → false。

严格布尔选项值

现状布尔选项可以接受 truefalseTrueFalseTFoption my_bool = T;)。文档主张只允许小写的 truefalse。迁移特性为 features.loose_bool_options:true → false。

字段号必须使用十进制字面量

现状允许非十进制整数字面量作字段号,例如 optional int32 x = 0x01;。文档注意到"幸运的是"目前不允许前导 +/-,但要求只允许十进制字面量——理由是非十进制字面量几乎没有存在必要,却使语言更难解析。迁移特性为 features.non_decimal_field_numbers:true → false。

从提案到落地:Edition 特性棘轮的完整链路

把原文档的十一条提议与当前仓库实现对照,可以看到这条"棘轮"的完整运转链路,也是 Edition 严格化机制的一般范式:

  1. 特性定义:在 FeatureSetdescriptor.proto)中声明特性字段,标注 feature_support.edition_introduced 与各级 edition_defaultsenforce_naming_style 是枚举型特性(STYLE_LEGACY / STYLE2024 / STYLE2026),比文档设想的布尔开关多了一个"中间档",允许更细粒度的分档收紧;
  2. 默认值解析feature_resolver.cc 中(如 feature_resolver.ccCHECK_ENUM_FEATURE(enforce_naming_style, EnforceNamingStyle, ...))负责按实体的 Edition 解析出该实体的有效特性值,子实体覆写父级;各 Edition 默认值快照还由 editions/defaults_test.cc 等测试锁定;
  3. 编译期校验DescriptorBuilder 构建完 descriptor 后,在 enforce_naming_style_ 总开关打开时逐实体检查,只有特性值达到阈值(STYLE2024 及以上且非 STYLE_LEGACY)的实体才执行 ValidateNamingStyledescriptor.cc);
  4. 可操作的错误提示:违规信息内嵌豁免指引(features.enforce_naming_style = STYLE_LEGACY can be used to opt out),让既有代码库可以逐实体显式豁免,而不必整个文件回退 Edition。

对使用者的实操含义是:如果你的 .proto 已切换到 edition = "2024" 及以后,应确保 message/service/enum 用 PascalCase、field/package 用 snake_case、enum value 用 SHOUTY_CASE(对照 editions/codegen_tests/edition2023_naming_style_field.proto 等测试文件中的反面示例);对于历史命名实在无法修改的实体,可以在该实体上显式设置 features.enforce_naming_style = STYLE_LEGACY 来局部豁免,这正是文档"feature 可应用于任意实体"设想的直接体现。

小结

stricter-schemas-with-editions.md 的价值不在某一条具体规则,而在于它给出了一套可持续的语言收紧方法论:识别宽松角落 → 定义实体级 feature → 宽松默认值保证零破坏迁移 → 新 Edition 翻转默认值完成棘轮。当前仓库中 Edition 2024 的命名风格强制(enforce_naming_style,含 STYLE2026 档位的默认值预留)以及 Edition 2026 的 enforce_proto_limits 等特性(同样定义于 descriptor.proto)都印证了这一方法论正在被逐步执行。对于维护 proto 代码库的团队,尽早按新 Edition 的默认风格整理命名,是避免将来被动迁移成本最低的做法。

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

项目优选

收起
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