Read OSS

探索 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.ccProtobufMain() 函数负责注册所有内置代码生成器,并调用 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.hupb/wire/decode.hupb/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 设计来抽象两者的差异来实现。

HPBhpb/)是较新的成员——一个构建在 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.ymltest_java.ymltest_python.ymltest_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 行的编译流水线的。