探索 Protobuf Monorepo:架构与目录指南
前置知识
- ›了解 Protocol Buffers 的基本概念及其作为序列化格式的用途
探索 Protobuf Monorepo:架构与目录指南
Protocol Buffers 是基础设施软件领域影响力最深远的开源项目之一。它为 Google 几乎所有服务提供序列化支持,也被业界大量采用。然而,当你第一次 git clone 这个仓库时,映入眼帘的是 70 多个顶级目录、两套独立的 C 运行时、面向 10 余种语言的代码生成器,以及横跨 Bazel 和 CMake 的构建系统。本文就是你的导航地图。
我们将带你认识这个 monorepo 的整体布局,解释两套核心运行时各自存在的原因,从高层视角梳理编译流水线,并逐一介绍各语言运行时的位置与角色。读完本文,你就能清楚地知道:想了解任何一个子系统,应该去哪里找。
双运行时架构:C++ Protobuf 与 μpb
关于这个仓库,最重要的架构事实是:它包含两套独立的 protobuf 运行时——功能完整的 C++ 实现,以及轻量级 C 实现 μpb(micro protobuf)。
C++ 运行时位于 src/google/protobuf/,提供大多数 C++ 开发者熟悉的完整功能:带类型访问器的生成消息类、完整的反射机制、Arena 内存分配器,以及高性能的尾调用表解析。这是一个庞大而精密的库,专为高吞吐量场景而优化。
μpb 位于 upb/,走的是截然不同的路线。正如其 README 所描述的,upb 的速度"与 protobuf C++ 相当,但代码体积缩小了一个数量级"。它支持可选反射,没有全局状态,专为嵌入动态语言运行时而设计。
graph TD
subgraph "C++ Protobuf Runtime"
CPP[src/google/protobuf/]
CPPGEN[C++ Generated Code]
JAVA[Java Runtime]
CSHARP[C# Runtime]
end
subgraph "μpb C Runtime"
UPB[upb/]
PY[Python C Extension]
RB[Ruby C Extension]
PHP[PHP C Extension]
HPB[hpb/ - C++ API on upb]
end
subgraph "Dual-Kernel"
RUST[Rust Runtime]
RUST --> CPP
RUST --> UPB
end
CPP --> CPPGEN
CPP --> JAVA
CPP --> CSHARP
UPB --> PY
UPB --> RB
UPB --> PHP
UPB --> HPB
分层逻辑清晰明了:Python、Ruby 和 PHP 均通过 C 扩展模块以 upb 作为后端;Java 和 C# 是独立实现;Rust 则独树一帜,通过内核抽象层同时支持两套后端。HPB 是构建在 upb 之上的新式 C++ API,为希望减小二进制体积的 C++ 用户提供了第三条路径。
提示: 调试 Python protobuf 问题时,不要去
src/google/protobuf/里找——Python 运行时的后端是 upb,相关 C 代码在upb/和python/中。
目录导航
以下是最重要的顶级目录及其用途说明:
| 目录 | 用途 |
|---|---|
src/google/protobuf/ |
C++ protobuf 运行时:消息、描述符、反射、arena、wire 格式 |
src/google/protobuf/compiler/ |
protoc 编译器:CLI、解析器及所有内置代码生成器 |
upb/ |
μpb C 运行时:消息、MiniTable schema、arena、wire 编解码 |
hpb/ |
Header-based Protobuf——构建在 upb 之上的现代 C++ API |
python/ |
封装 upb 的 Python C 扩展 |
ruby/ |
封装 upb 的 Ruby C 扩展 |
php/ |
封装 upb 的 PHP C 扩展 |
java/ |
Java protobuf 运行时(独立实现,不使用 upb) |
csharp/ |
C# protobuf 运行时(独立实现) |
rust/ |
支持双内核的 Rust protobuf 运行时 |
objectivec/ |
Objective-C 运行时 |
conformance/ |
跨语言一致性测试套件 |
editions/ |
Protobuf Editions 系统(用于替代 proto2/proto3 语法) |
bazel/ |
protobuf 的 Bazel 构建规则 |
C++ 编译器代码嵌套在 src/google/protobuf/compiler/ 下,每种语言各有独立子目录:cpp/、java/、python/、rust/、php/、ruby/、csharp/、objectivec/ 和 kotlin/。
根目录下的 version.json 文件记录了各语言的发布版本,用于协调发布节奏。每种语言可以有独立的版本号(例如 C++ 7.35-dev、Java 4.35-dev、Rust 4.35-dev),并通过统一的 protoc_version 字段进行协调。这正是 monorepo 管理各语言独立发版节奏的方式。
编译流水线概览
每个 protobuf 用户的工作流都从一个 .proto 文件开始,最终得到目标语言的生成代码。protoc 编译器负责驱动这一转换过程。整体流程如下:
flowchart LR
A[".proto file"] --> B["Tokenizer<br/>(lexer)"]
B --> C["Parser<br/>(AST builder)"]
C --> D["FileDescriptorProto"]
D --> E["DescriptorPool<br/>(validation + linking)"]
E --> F["FileDescriptor<br/>(immutable schema)"]
F --> G["CodeGenerator<br/>(language-specific)"]
G --> H["Generated Source Files"]
入口点是 src/google/protobuf/compiler/main.cc。ProtobufMain() 函数负责注册所有内置代码生成器,并调用 cli.Run():
// Proto2 C++
cpp::CppGenerator cpp_generator;
cli.RegisterGenerator("--cpp_out", "--cpp_opt", &cpp_generator,
"Generate C++ header and source.");
每种语言都有类似的注册逻辑——Java、Kotlin、Python、PHP、Ruby(包括新增的 RBS 类型定义生成器)、C#、Objective-C 和 Rust。注册完成后,cli.Run(argc, argv) 负责处理参数解析、.proto 文件加载、描述符构建以及代码生成的分发。
流水线有两个扩展点:其一是 code_generator.h 中定义的 CodeGenerator 抽象接口,所有内置生成器均实现该接口;其二是 plugin.h 中描述的插件机制,外部生成器通过 stdin/stdout 以 CodeGeneratorRequest/CodeGeneratorResponse protobuf 消息与 protoc 通信。正是这套插件机制,让第三方语言(Go、Dart、Swift 等)无需修改核心编译器,就能拥有自己的 protoc 插件。
各语言运行时的组织方式
各语言运行时并非一个模子刻出来的,它们在架构上分为三类:
graph TD
subgraph "C Extension Wrappers (upb backend)"
PY["python/<br/>message.c, descriptor_pool.c"]
RB["ruby/<br/>ext/google/protobuf_c/"]
PHP_RT["php/<br/>ext/google/protobuf/"]
end
subgraph "Standalone Implementations"
JAVA["java/<br/>Pure Java runtime"]
CS["csharp/<br/>Pure C# runtime"]
OBJC["objectivec/<br/>Objective-C runtime"]
end
subgraph "Dual-Kernel"
RUST_RT["rust/<br/>cpp_kernel/ + upb_kernel/"]
end
subgraph "New C++ on upb"
HPB_RT["hpb/<br/>Modern C++ API"]
end
C 扩展封装层(Python、Ruby、PHP):这些语言通过轻薄的 C 扩展模块将所有重型计算委托给 upb 处理。查看 python/message.c 可以看到,它直接引入了 upb 的头文件,如 upb/message/message.h、upb/wire/decode.h 和 upb/reflection/message.h。Python 对象本质上是对 upb_Message 指针的轻量封装。
独立实现(Java、C#):这些语言维护着完整的独立运行时。java/ 中的 Java 运行时包含自有的 wire 格式编解码器、消息构建器和反射系统,全部以纯 Java 实现。
Rust 的双内核设计是架构上最具特色的方案。rust/cpp_kernel/ 和 rust/upb_kernel/ 提供了可替换的后端,使 Rust protobuf 既可以运行在 C++ 运行时之上,也可以运行在 upb 之上。正如我们将在第 6 篇文章中深入探讨的,这通过基于代理的 API 设计来抽象两者的差异来实现。
HPB(hpb/)是较新的成员——一个构建在 upb 之上的现代 C++ API 层。它提供了更简洁的 C++ 接口,同时避免了完整 C++ protobuf 运行时的体积负担,还能享受 upb 的小代码尺寸优势。
构建系统与依赖项
protobuf 仓库以 Bazel 作为主要构建系统,CMake 作为备选方案。MODULE.bazel 文件将该模块定义为 protobuf 版本 35.0-dev,要求 Bazel 8.0.0 或更高版本。
主要依赖项如下:
| 依赖项 | 版本 | 用途 |
|---|---|---|
abseil-cpp |
20250512.1 | 核心 C++ 工具库(字符串、容器、同步原语) |
zlib |
1.3.1 | 压缩支持 |
jsoncpp |
1.9.6 | JSON 解析 |
rules_java |
8.6.1 | Java 构建规则 |
rules_python |
1.6.0 | Python 构建规则 |
rules_rust |
0.63.0 | Rust 构建规则 |
flowchart TD
PB["protobuf module"]
PB --> ABSEIL["abseil-cpp"]
PB --> ZLIB["zlib"]
PB --> JSON["jsoncpp"]
PB --> RJ["rules_java"]
PB --> RP["rules_python"]
PB --> RR["rules_rust"]
PB --> RRB["rules_ruby"]
PB --> CC["rules_cc"]
PB --> SK["bazel_skylib"]
CI 基础设施使用 GitHub Actions,每种语言有对应的 workflow 文件。你可以在 .github/workflows/ 中找到 test_cpp.yml、test_java.yml、test_python.yml、test_rust.yml 等。test_runner.yml 负责统一编排,在推送到 main 分支、提交 pull request 以及每小时定时触发时运行。
提示: 如果你需要在本地构建 protobuf,强烈推荐使用 Bazel。
CMakeLists.txt主要面向需要 CMake 集成的下游用户,官方标准构建始终以 Bazel target 为准。
后续文章预告
至此,你已经建立起对 protobuf monorepo 的整体认知:两套运行时(C++ 和 upb)、带有可插拔代码生成器的编译器、三种架构风格的语言运行时,以及以 Bazel 为核心的构建系统。本系列后续文章都将以这张地图为基础展开。
第 2 篇文章将深入 protoc 编译器内部——完整追踪一个 .proto 文件从词法分析、语法解析、描述符校验到代码生成分发的全链路。我们将看到 CommandLineInterface::Run() 是如何驱动这条复杂的、长达 4000 行的编译流水线的。