首页
/ Protobuf Editions 中的 Feature 扩展布局设计:按生成器划分还是按运行时实现划分?

Protobuf Editions 中的 Feature 扩展布局设计:按生成器划分还是按运行时实现划分?

2026-09-04 21:34:50作者:尤辰城Agatha

本文以 docs/design/editions/editions-feature-extension-layout.md 设计文档(作者 @mkruskal-google、@zhangskz,2023-08-23 批准)为主体,讲清 Protobuf Editions 项目中的一个关键设计决策:全局 features option 的语言级扩展(feature extensions)到底应该归谁拥有——是归运行时实现(C++、upb、Java……),还是归代码生成器(protoc 插件)。读完后,你将理解四个备选方案各自的利弊、最终取舍背后的权衡逻辑,并能在仓库源码(descriptor.protoFeatureSet 定义)中看到这个决策的实际落地形态。

背景:谁该拥有这些 Feature 扩展?

What are Protobuf Editions 提出了用 edition = ... 取代 syntax = ...、用可继承的 features 控制代码生成与运行时行为的总体计划,并计划用"全局 features proto 的扩展"来让 protobuf 团队之外的各方定义自己关心的特性。但当时遗留了一个模糊点:这些扩展的归属边界——"语言(language)"、"代码生成器(code generator)"、"运行时实现(runtime implementation)"三者相似却不等同:

  • 一种语言(如 Python)可能有多种运行时实现(纯 Python、Python/C++、Python/upb);
  • 一种运行时实现(如 upb、C++)可能被多种语言共享(Python、Rust、Ruby、PHP 都以它们为后端)。

触发这次讨论的具体案例是 "Editions Zero Feature: utf8_validation"(该文档未对外发布,但后来去掉争议选项的版本即仓库中的 edition-zero-features.md 所讨论的 UTF-8 校验特性)。原本唯一的扩展特性 legacy_closed_enum(Java/C++)没有归属歧义,而 utf8_validation 有:在 Python 中,proto2/proto3 下的当前行为对三种实现(pure Python、Python/C++、Python/upb)各自不同。这正是"扩展按谁划分"问题的典型压力测试。

两个让"按运行时实现划分"复杂化的现实

原文档在 Overview 中列出会议里讨论出的两大复杂性:

  1. Polyglot(多语言混用):upb 或 C++ 运行时在多语言场景下应遵循哪套 features?这一歧义其实今天就已存在——所有 proto2 字符串以及大量 proto3 字符串在跨语言传输时本身就是不安全的。
  2. Shared Implementations(共享实现):upb 和 C++ 被用作多种语言的后端。如果只有一套 upbcpp 级别的 features,那么语言切换到这些共享实现时将更难迁移,因为没有按语言独立的开关。同样,这也是现状——今天切换运行时实现就可能带来微妙且危险的行为变化。

文档给出的结论是:既然当前只有两种行为且其中一种无歧义,不如在 edition zero 推广过程中先搁置这个决定,等收集到更多边界案例再说;同时后续 edition 对 features 的重新建模自由度很大,所以初始实现应保持最简单(即下文备选方案 2)。

四种备选方案逐一对比

方案一:按运行时实现划分(Runtime Implementation Features)

这是 "Editions Zero Feature: utf8_validation" 中的原始设想:features 按运行时实现组织。例如 Protobuf Python 用户需要根据底层实现设置不同扩展:features.(pb.cpp).<feature>features.(pb.upb).<feature>

  • 优点:与 Editions 之前可表达的行为范围最一致。
  • 缺点:底层实现往往对用户不透明(用户甚至可能不知道自己在用哪个后端);而且缺乏针对"语言/实现组合"的独立开关——例如无法独立设置"Python-on-C++"的行为而不动 C++ 本身,这会加大从其他 Python 实现迁移的难度。

方案二:按代码生成器划分(Generator Features,最终采纳方向)

features 只按生成器划分——每个 protoc 插件拥有一套自己的 features。这是团队在后续讨论中做出的第二个决定。它与方案一非常相似,但更贴合"features 主要服务于 codegen"的目标。

例如:所有 Python 实现共享同一套 features(features.(pb.python).<feature>);若某特性确实需要针对特定实现,可以把特性名本身定向到该实现,如 features.(pb.python).upb_utf8_validation 只会被 Python/upb 使用。

  • 优点:允许对不同目标语言共享同一实现的情况做独立控制(例如 Python 的 upb 特性不会影响 PHP)。
  • 缺点:upb 需要理解"自己应该遵循哪门语言的特征",而 upb 目前并不知道自己是给哪门语言服务的;在行为冲突时,共享实现的跨语言进程内共享(如 Python-upb 与 PHP-upb 同进程)会受到限制,可能还需要额外的检查。

方案三:迁移到 bytes(Migrate to bytes)

既然争议围绕 utf8 校验,干脆不在 edition zero 引入这个开关,把目前不强制 UTF-8 校验的字段全部迁移为 bytes。这大概率需要一个新的代码生成特性来"把 bytes 的 getter/setter 生成为字符串 API",但不存在当前看到的归属歧义。

文档认为此路不可行:utf8 校验并不是简单的开/关二值决策,它在语言之间差异很大——很多情况下 UTF-8 在部分语言中校验、在另一些语言中不校验,还有 C++ 那种"只记日志但放行非法 UTF-8"的 hint 行为。文档同时留了个口子:可以在后续 LSC(large-scale change)中通过定向禁用所有相关语言校验的特性组合,部分实现该思路。

  • 优点:回避问题,不需要任何 upb 特性,C++ 特性全部退化为纯代码生成特性;避免在 edition zero 引入一个非常复杂的特性。
  • 缺点:以当前复杂度看基本做不到;会有 O(10M) 量级的 proto2 string 字段被盲目改为 bytes

方案四:嵌套特性(Nested Features)

允许共享的 feature set 消息:upb 定义自己的 feature 消息,但把它作为全局 FeatureSet 的扩展;使用 upb 实现的语言在自己的 feature 里内嵌一个该类型的字段,实现更细粒度的控制。C++ 则既扩展全局 FeatureSet,也允许作为其他语言的字段出现。此外可在特性校验阶段加入检查,强制"不可能的组合"不被指定——例如在当前实现下,features.(pb.python).cpp 必须与 features.(pb.cpp) 恒等,因为没有机制区分二者。

  • 优点:比方案一、二更显式。
  • 缺点:可能过度显式——proto 属主被迫大量复制(duplicate)特性声明。

决策逻辑:信息不足时选最简单的模型

文档 Overview 的结论值得单独强调:只有两种行为、且其中一种无歧义,此时强行选定"按实现划分还是按生成器划分"的归属模型风险大于收益。与其在 edition zero 就定死复杂的所有权结构,不如让初始实现保持简单(备选方案 2:按生成器划分),把"按实现定向"留作特性命名层面(如 upb_utf8_validation 这样的特征名)的柔性能力,待推广期积累更多边界案例后再决定是否升级建模方式。这一决策也呼应了 what-are-protobuf-editions.md 中"codegen backends own the definitions of their features"的总体原则——特性定义权归各语言后端,而归属单位是生成器

仓库源码印证:FeatureSet 扩展声明就是"按生成器划分"

设计文档讨论的抽象问题,在当前仓库的 src/google/protobuf/descriptor.proto 中有明确的落地形态,可以作为事实核对。

1. 每个生成器独占一个 FeatureSet 扩展号

FeatureSet 消息(descriptor.proto 处定义)末尾用 extension_range 显式登记了已分配的语言级扩展,其编号即"按生成器划分"的直接证据:

extensions 1000 to 9994 [
  declaration = { number: 1000, full_name: ".pb.cpp",      type: ".pb.CppFeatures" },
  declaration = { number: 1001, full_name: ".pb.java",     type: ".pb.JavaFeatures" },
  declaration = { number: 1002, full_name: ".pb.go",       type: ".pb.GoFeatures" },
  declaration = { number: 1003, full_name: ".pb.python",   type: ".pb.PythonFeatures" },
  declaration = { number: 1004, full_name: ".pb.csharp",   type: ".pb.CSharpFeatures" },
  declaration = { number: 1100, full_name: ".imp.impress_feature_set", type: ".imp.ImpressFeatureSet" },
  declaration = { number: 9989, full_name: ".pb.java_mutable", type: ".pb.JavaMutableFeatures" },
  declaration = { number: 9990, full_name: ".pb.proto1",   type: ".pb.Proto1Features" }
];

extensions 9995 to 9999;  // For internal testing
extensions 10000;         // for https://github.com/bufbuild/protobuf-es

这段声明印证了文档结论的几个要点:

  • 没有 pb.upbpb.cpp_impl 这类"按运行时实现"的顶层扩展,只有按语言/生成器命名的 .pb.cpp.pb.java.pb.python……——这正是备选方案 2 的形态;
  • .pb.java_mutableJavaMutableFeatures,扩展号 9989)作为独立的生成器扩展存在,而非嵌套在 JavaFeatures 里的字段——从源码结构看,"面向特定实现的变体"被建模为另一个生成器级别的扩展,而不是方案 4 的嵌套特性消息;
  • 保留 9995–9999 给内部测试、10000 给第三方生成器(bufbuild/protobuf-es),说明这套编号空间是留给各 codegen 自持特性的开放注册表。

各生成器的特性消息定义在独立文件中,例如 src/google/protobuf/cpp_features.proto(配套生成的 cpp_features.pb.h/cc 同目录),与 extensions 声明中 .pb.CppFeatures 一一对应。

2. 触发讨论的 utf8_validation 特性长什么样

FeatureSet 中的 utf8_validation 字段(紧随 repeated_field_encoding 之后定义)展示了该特性在"去掉争议选项后"的最终形态:

enum Utf8Validation {
  UTF8_VALIDATION_UNKNOWN = 0;
  VERIFY = 2;
  NONE = 3;
  reserved 1;
}
optional Utf8Validation utf8_validation = 4 [
  retention = RETENTION_RUNTIME,
  targets = TARGET_TYPE_FIELD,
  targets = TARGET_TYPE_FILE,
  feature_support = {
    edition_introduced: EDITION_2023,
  },
  edition_defaults = { edition: EDITION_LEGACY, value: "NONE" },
  edition_defaults = { edition: EDITION_PROTO3, value: "VERIFY" }
];

几点与设计文档的呼应:

  • retention = RETENTION_RUNTIME:特性会被序列化进 descriptor 供运行时使用,即它确实横跨 codegen 与 runtime,这正是归属需要慎重设计的原因;
  • edition_defaults 显式区分了 EDITION_LEGACY(proto2 时代,默认 NONE)与 EDITION_PROTO3(默认 VERIFY),把"现状不一致"固化成按 edition 的默认值,而不是在扩展布局上分叉;
  • reserved 1 说明早期曾存在第三个枚举值,后被移除——与文档提及的"问题选项(problematic options)"修订过程一致。

3. 特性解析在哪里发生

从源码结构看,仓库中 src/google/protobuf/ 下存在 feature_resolver.h/.cc 及对应测试 feature_resolver_test.cc,承担把 edition 默认值与显式声明合并、完成特性继承解析的工作;FeatureSetDefaultsdescriptor.proto 处定义)则携带"每个 edition 的默认特性集合",让解析退化为"找最近的匹配 edition 再做 proto 合并"。特性如何被各生成器/运行时消费,可继续参见 docs/design/editions/editions-life-of-a-featureset.md 的完整流程描述。

对实践者的启示

  1. 写 editions proto 时,扩展命名空间按"你使用哪个生成器"选择:用 C++ codegen 就写 features.(pb.cpp).x,用 Python codegen 就写 features.(pb.python).x,无论底层是不是 upb——这与文档"所有 Python 实现共享同一套 features"的设计一致。
  2. 实现级差异用特性名表达,而不是新扩展:需要区分后端时,参照 features.(pb.python).upb_utf8_validation 的命名思路,在自己拥有的生成器扩展内加定向特性。
  3. 理解共享实现的边界:以 upb/C++ 为后端的多种语言之间,行为可能受"同一实现、不同语言 features"的冲突约束(文档方案 2 的 Cons),同进程多语言混用时值得额外验证序列化/校验行为。
  4. 迁移语义依赖 edition 默认值而非扩展布局utf8_validation 等特性的 edition 默认值(proto2→NONE、proto3→VERIFY)保证了 proto2/proto3 向 editions 的机械迁移在语义上是 no-op,这也是整个 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.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 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
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384