Read OSS

Protobuf モノレポを読み解く:アーキテクチャとディレクトリ構成ガイド

初級

前提知識

  • Protocol Buffers がシリアライゼーションフォーマットとして何であるか、その目的についての基本的な理解

Protobuf モノレポを読み解く:アーキテクチャとディレクトリ構成ガイド

Protocol Buffers のリポジトリは、インフラソフトウェアの世界でも特に影響力の大きいオープンソースプロジェクトのひとつです。Google のほぼすべてのサービスで使われているだけでなく、業界全体でも広く採用されています。しかし、初めて git clone してみると、70 を超えるトップレベルディレクトリ、2 種類の C ランタイム、10 以上の言語向けコードジェネレーター、そして Bazel と CMake にまたがるビルドシステムが目に飛び込んできます。本記事は、そのリポジトリを歩き回るための地図です。

モノレポの全体像を把握した上で、2 つのコアランタイムとその存在理由を説明します。コンパイルパイプラインの大まかな流れを追い、各言語ランタイムの位置づけも確認しましょう。読み終える頃には、どのサブシステムを調べたくても迷わず目的地へたどり着けるようになっているはずです。

デュアルワールドアーキテクチャ:C++ Protobuf と μpb

このリポジトリを理解する上で最も重要なアーキテクチャ上の事実は、2 つの独立した protobuf ランタイムが共存しているということです。フル機能の C++ 実装と、軽量な C 実装である μpb(micro protobuf)の 2 つです。

C++ ランタイムは src/google/protobuf/ に置かれており、C++ 開発者におなじみの豊富な機能を提供しています。型付きアクセサを持つ生成メッセージクラス、フル reflection、Arena アロケーター、そして高性能なテールコールテーブルパーシングなどが含まれます。スループット重視で最適化された、大規模で洗練されたライブラリです。

μpb は upb/ に置かれており、まったく異なる設計思想を持ちます。README にあるように、upb は「protobuf C++ と同等の速度を持ちながら、コードサイズはけた違いに小さい」という特長があります。オプションの reflection をサポートし、グローバルステートを持たず、動的言語ランタイムへの組み込みを念頭に設計されています。

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 extension module を通じて upb をバックエンドとして使います。Java と C# は独立した実装を持ちます。Rust は特異で、カーネル抽象化レイヤーを通じて 両方の バックエンドをサポートしています。HPB は upb の上に構築された新しい C++ API で、バイナリサイズを抑えたい C++ ユーザー向けの第三の選択肢となっています。

ヒント: Python の protobuf で問題をデバッグするとき、src/google/protobuf/ を見ても答えは見つかりません。Python ランタイムのバックエンドは upb なので、関連する C のコードは upb/python/ にあります。

ディレクトリマップ:目的地を見つける

主要なトップレベルディレクトリを一覧にまとめました。

ディレクトリ 役割
src/google/protobuf/ C++ protobuf ランタイム:メッセージ、ディスクリプター、reflection、arena、ワイヤーフォーマット
src/google/protobuf/compiler/ protoc コンパイラー:CLI、パーサー、組み込みコードジェネレーター群
upb/ μpb C ランタイム:メッセージ、MiniTable スキーマ、arena、ワイヤーエンコーディング/デコーディング
hpb/ Header-based Protobuf — upb 上に構築されたモダンな C++ API
python/ upb をラップする Python C extension
ruby/ upb をラップする Ruby C extension
php/ upb をラップする PHP C extension
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 フィールドで全体を調整しています。モノレポでありながら言語ごとに独立したリリースサイクルを実現しているのは、この仕組みのおかげです。

コンパイルパイプラインを俯瞰する

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 ファイルの読み込み、ディスクリプターの構築、コード生成のディスパッチを担います。

このパイプラインには 2 つの拡張ポイントがあります。ひとつは code_generator.h で定義された CodeGenerator 抽象インターフェースで、すべての組み込みジェネレーターが実装しています。もうひとつは plugin.h に定義されたプラグインシステムで、外部ジェネレーターが stdin/stdout 経由の CodeGeneratorRequest/CodeGeneratorResponse プロトバフを使って protoc と通信します。このプラグインの仕組みがあるからこそ、Go、Dart、Swift などのサードパーティ言語がコアコンパイラーを変更せずに独自の protoc プラグインを持てるのです。

言語ランタイムの構成

すべての言語ランタイムが同じアーキテクチャを持つわけではありません。大きく 3 つの種類に分類できます。

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 extension ラッパー(Python、Ruby、PHP):これらの言語は薄い C extension module を持ち、重い処理はすべて upb に委ねています。python/message.c を見ると、upb/message/message.hupb/wire/decode.hupb/reflection/message.h といった upb のヘッダーを直接インクルードしていることがわかります。Python オブジェクトは upb_Message ポインターの薄いラッパーに過ぎません。

独立実装(Java、C#):これらは完全なランタイム実装を独自に維持しています。java/ にある Java ランタイムは、ワイヤーフォーマットのコーデック、メッセージビルダー、reflection システムをすべて純粋な Java で実装しています。

Rust のデュアルカーネルアプローチは、アーキテクチャ的に最も興味深い設計です。rust/cpp_kernel/rust/upb_kernel/ がそれぞれ代替バックエンドを提供しており、Rust の protobuf は C++ ランタイムでも upb でも動作します。第 6 回の記事で詳しく取り上げますが、これはバックエンドの差異を隠蔽するプロキシベースの API 設計によって実現されています。

HPBhpb/)は比較的新しい存在で、upb の上に構築されたモダンな C++ API レイヤーです。フルの C++ protobuf ランタイムほど重くなく、upb の小さなコードサイズを活かしながら、よりクリーンな C++ インターフェースを提供しています。

ビルドシステムと依存関係

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 を使って構築されており、言語ごとにワークフローファイルが分かれています。.github/workflows/ には test_cpp.ymltest_java.ymltest_python.ymltest_rust.yml などが並んでいます。これらをまとめて動かすのが test_runner.yml で、main へのプッシュ、プルリクエスト、そして毎時のスケジュール実行に応じてトリガーされます。

ヒント: ローカルで protobuf をビルドするなら、Bazel を強くお勧めします。CMakeLists.txt は CMake との統合が必要なダウンストリームの利用者向けに用意されたものであり、正式なビルドは一貫して Bazel ターゲットを使って行われます。

次のステップへ向けて

protobuf モノレポの全体像が見えてきました。2 つのランタイム(C++ と upb)、プラグイン可能なコードジェネレーターを持つコンパイラー、3 種類のアーキテクチャスタイルを持つ言語ランタイム、そして Bazel ファーストのビルドシステム。このシリーズの以降の記事は、すべてこの地図を土台にして進んでいきます。

第 2 回では protoc コンパイラー自体に焦点を当て、.proto ファイルがレキシング、パーシング、ディスクリプターの検証、コード生成のディスパッチへと至る全経路を丁寧にたどります。CommandLineInterface::Run() が 4000 行にもおよぶコンパイルパイプラインをどのように取りまとめているか、その複雑な仕組みを解き明かしていきましょう。