Protobuf Editions 中的 Feature 扩展布局设计:按生成器划分还是按运行时实现划分?
本文以 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.proto 的 FeatureSet 定义)中看到这个决策的实际落地形态。
背景:谁该拥有这些 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 中列出会议里讨论出的两大复杂性:
- Polyglot(多语言混用):upb 或 C++ 运行时在多语言场景下应遵循哪套 features?这一歧义其实今天就已存在——所有 proto2 字符串以及大量 proto3 字符串在跨语言传输时本身就是不安全的。
- Shared Implementations(共享实现):upb 和 C++ 被用作多种语言的后端。如果只有一套
upb或cpp级别的 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.upb、pb.cpp_impl这类"按运行时实现"的顶层扩展,只有按语言/生成器命名的.pb.cpp、.pb.java、.pb.python……——这正是备选方案 2 的形态; .pb.java_mutable(JavaMutableFeatures,扩展号 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 默认值与显式声明合并、完成特性继承解析的工作;FeatureSetDefaults(descriptor.proto 处定义)则携带"每个 edition 的默认特性集合",让解析退化为"找最近的匹配 edition 再做 proto 合并"。特性如何被各生成器/运行时消费,可继续参见 docs/design/editions/editions-life-of-a-featureset.md 的完整流程描述。
对实践者的启示
- 写 editions proto 时,扩展命名空间按"你使用哪个生成器"选择:用 C++ codegen 就写
features.(pb.cpp).x,用 Python codegen 就写features.(pb.python).x,无论底层是不是 upb——这与文档"所有 Python 实现共享同一套 features"的设计一致。 - 实现级差异用特性名表达,而不是新扩展:需要区分后端时,参照
features.(pb.python).upb_utf8_validation的命名思路,在自己拥有的生成器扩展内加定向特性。 - 理解共享实现的边界:以 upb/C++ 为后端的多种语言之间,行为可能受"同一实现、不同语言 features"的冲突约束(文档方案 2 的 Cons),同进程多语言混用时值得额外验证序列化/校验行为。
- 迁移语义依赖 edition 默认值而非扩展布局:
utf8_validation等特性的 edition 默认值(proto2→NONE、proto3→VERIFY)保证了 proto2/proto3 向 editions 的机械迁移在语义上是 no-op,这也是整个 feature 布局设计要服务的最终目标。
延伸阅读(均在当前仓库内)
- docs/design/editions/what-are-protobuf-editions.md:Editions 总纲,features/editions 的基本概念与生命周期。
- docs/design/editions/edition-zero-features.md:edition zero 首批特性的定义,其中
string_field_validation(MANDATORY/HINT/NONE)正是本文主题讨论的前身,后被utf8_validation取代。 - docs/design/editions/editions-life-of-a-featureset.md:一个 FeatureSet 从文件到生成代码的完整旅程。
- docs/design/editions/README.md:editions 设计文档索引。
- src/google/protobuf/descriptor.proto:
FeatureSet、FeatureSetDefaults的权威定义。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00