Read OSS

Protocol Buffers ソースコード:全体像を把握する

中級

前提知識

  • Protocol Buffers の基本的な概念と .proto ファイルの動作に関する知識
  • C++ ヘッダーファイルをある程度読み慣れていること

Protocol Buffers ソースコード:全体像を把握する

Protocol Buffers のリポジトリは、現存するオープンソースプロジェクトの中でも特に影響力の大きなものの1つです。Google をはじめ世界中の主要な分散システムがデータのシリアライズに protobuf を採用しています。このリポジトリは C++ だけで約 20 万行を擁し、Java・Python・Ruby・PHP・Rust・C#・Objective-C・Kotlin といった言語にもわたる大規模なコードベースです。「protoc はどうやって .proto ファイルを動くコードに変換するのか」「ランタイムはどうやってナノ秒単位でメッセージをパースするのか」——そんな疑問をお持ちなら、このシリーズが答えを丁寧に解説します。

この最初の記事は、いわば「地図」です。全体の構造を把握するためのメンタルモデルを作り、ディレクトリ構成を見渡し、2 つの C ランタイムが存在する理由を理解し、メインのエントリーポイントを追いかけます。読み終える頃には、リポジトリのどのファイルを開いても、それが全体のどこに位置するかがわかるようになっているはずです。

三層アーキテクチャ

protobuf のあらゆる処理は、概念的に 3 つの層を通って流れていきます。コードベースをナビゲートするうえで最も重要な知識は、この層の構造を理解することです。

flowchart TB
    subgraph Schema["Schema Layer"]
        A[".proto files"] --> B["Descriptor Graph<br/>(FileDescriptor, Descriptor, FieldDescriptor)"]
    end
    subgraph Codegen["Code Generation Layer"]
        B --> C["protoc + Language Generators"]
        C --> D["Generated Source Files<br/>(.pb.h/.pb.cc, .java, .py, etc.)"]
    end
    subgraph Runtime["Runtime Layer"]
        D --> E["Generated Code + Runtime Library"]
        E --> F["Serialize / Parse / Reflect"]
    end

スキーマ層は、言語に依存しない型システムを定義します。.proto ファイルはまず FileDescriptorProto としてパースされ、次に DescriptorPool 内でイミュータブルな FileDescriptor として構築されます。このディスクリプターグラフは src/google/protobuf/descriptor.h を起点として、すべてのメッセージ、フィールド、enum、サービスのメモリ上の標準表現となります。

コード生成層はディスクリプターを読み込み、言語ごとのソースファイルを生成します。この処理を取り仕切るのが protoc コンパイラで、登録された CodeGenerator の実装に処理を振り分けます。各ジェネレーターは、対象言語らしいコードを出力する方法を知っています。

ランタイム層は、エンドユーザーが実際にリンクする部分です。基底クラス(MessageLiteMessage)、シリアライズ/パースのロジック、アリーナアロケーション、リフレクションなどを提供します。生成されたコードはこれらの基底クラスを継承し、ランタイムのパーステーブルに組み込まれます。

この分離こそが protobuf を言語中立にしている核心です。スキーマ層は共通化されており、各言語はそれぞれ専用のジェネレーターとランタイムを持ちます。

ディレクトリ構成を歩く

リポジトリは大規模ですが、トップレベルのレイアウトは三層モデルにきれいに対応しています。

ディレクトリ 役割
src/google/protobuf/ スキーマ + ランタイム C++ コアランタイム、ディスクリプターシステム、アリーナ、パース
src/google/protobuf/compiler/ コード生成 protoc CLI、パーサー、すべての組み込み言語ジェネレーター
upb/ ランタイム(C) µpb — 動的言語向けの軽量 C ランタイム
hpb/ ランタイム(C++) µpb の上に乗る使いやすい C++ ラッパー
upb_generator/ コード生成 µpb ミニテーブル向けコードジェネレーター
hpb_generator/ コード生成 HPB C++ ラッパー向けコードジェネレーター
java/, python/, ruby/, php/, rust/, csharp/, objectivec/ ランタイム 言語ごとのランタイムライブラリ
conformance/ テスト 言語横断の適合性テストスイート
editions/ スキーマ Editions 機能の定義とテストデータ
docs/ ドキュメント 設計ドキュメント、upb ガイド
pkg/ ビルド/パッケージング 配布パッケージング、ファイルリスト生成
.github/workflows/ CI 20 以上の GitHub Actions ワークフロー

C++ コンパイラとランタイムは src/google/protobuf/ 以下にあります。その中の compiler/ にはフロントエンド(トークナイザー、パーサー、CLI)と、言語ごとのジェネレーターのサブディレクトリ(compiler/cpp/compiler/java/compiler/python/ など)が含まれています。

ヒント: compiler/ サブディレクトリには、C++ だけでなくすべての組み込み言語のジェネレーターが含まれています。compiler/rust/generator.h は Rust のコードジェネレーターですが、C++ で書かれており、protoc バイナリの一部です。

デュアルランタイム戦略:C++ vs. µpb

このリポジトリで最も驚かされることの一つは、2 つの独立した C/C++ ランタイムが存在することです。その理由を理解しておくことは非常に重要です。

flowchart LR
    subgraph Full["C++ Runtime (src/google/protobuf/)"]
        direction TB
        ML[MessageLite] --> M[Message]
        M --> R[Reflection]
        M --> AR[Arena Allocation]
        M --> TC[TcParser]
    end
    subgraph Micro["µpb Runtime (upb/)"]
        direction TB
        UM[upb_Message] --> MT[upb_MiniTable]
        UM --> UA[upb_Arena]
        UM --> UD[upb_Decode / upb_Encode]
    end
    Full -.->|"Used by: C++ apps"| U1[C++ Users]
    Micro -.->|"Wrapped by: PHP, Ruby, Python"| U2[Dynamic Languages]

C++ ランタイムはフル機能を備えたユーザー向けライブラリです。リフレクション、遅延フィールド、動的メッセージ、スレッドセーフなアリーナ、高性能な TcParser などを提供し、C++ アプリケーションが直接リンクする対象です。

µpb(micro protobuf、upb/ 配下)は最小限の C カーネルです。機能よりもコードサイズの小ささと安定した C ABI を優先して設計されています。docs/upb/vs-cpp-protos.md にある説明を引用すると:

  • C++ protobuf は C++ アプリケーションが直接利用することを想定したユーザーレベルのライブラリ
  • µpb は他の言語からラップされることを主目的として設計された C カーネルであり、言語ごとの protobuf ライブラリを構築するための土台

PHP、Ruby、Python はいずれも µpb をシリアライズカーネルとして使用し、FFI 経由で呼び出します。これらの言語ディレクトリが upb/ と並んで存在するのはそのためで、各ディレクトリには µpb を各言語らしい API でラップするグルーコードが格納されています。

コードサイズの差は顕著です。descriptor.proto をパース・シリアライズするバイナリの .text セクションは、µpb が 26 KiB であるのに対し、フル C++ ランタイムでは 983 KiB にのぼります。

エントリーポイント:protoc とプラグインシステム

protoc バイナリは src/google/protobuf/compiler/main.cc から始まります。ProtobufMain() 関数は非常に読みやすい構成になっており、CommandLineInterface をインスタンス化し、すべての組み込みジェネレーターを登録してから cli.Run() を呼び出します。

flowchart TD
    A["ProtobufMain()"] --> B["Create CommandLineInterface"]
    B --> C["AllowPlugins('protoc-')"]
    C --> D["Register 11 built-in generators"]
    D --> E["cli.Run(argc, argv)"]
    E --> F{"--X_out flag?"}
    F -->|"Built-in"| G["Invoke registered CodeGenerator"]
    F -->|"Unknown"| H["Find protoc-gen-X plugin binary"]
    H --> I["Pipe CodeGeneratorRequest via stdin"]
    I --> J["Read CodeGeneratorResponse from stdout"]

登録される 11 のジェネレーターは以下の通りです。

フラグ ジェネレーター 言語
--cpp_out CppGenerator C++
--java_out JavaGenerator Java
--kotlin_out KotlinGenerator Kotlin
--python_out Generator Python
--pyi_out PyiGenerator Python stubs
--php_out Generator PHP
--ruby_out Generator Ruby
--rbs_out RBSGenerator Ruby type defs
--csharp_out Generator C#
--objc_out ObjectiveCGenerator Objective-C
--rust_out RustGenerator Rust

Go や Dart、あるいはサードパーティの言語など、組み込みでサポートされていない言語向けにはプラグインプロトコルが使われます。認識できない --foo_out フラグを見つけると、protocPATH から protoc-gen-foo を探し、CodeGeneratorRequest を stdin にシリアライズして stdout から CodeGeneratorResponse を読み取ります。C++ でプラグインを書く際のエントリーポイントとして、plugin.h にある PluginMain() 関数がすぐに使える形で提供されています。

ヒント: プラグインプロトコルを使えば、C++ に一切依存せず、任意の言語で protobuf コードジェネレーターを書けます。stdio 上で protobuf メッセージを読み書きできれば十分です。protoc-gen-go が Go で書かれているのはこのためです。

コードベースを効率よく読む

いくつかの実践的なパターンを知っておくと、リポジトリ内を素早く移動できます。

バージョン管理には version.json が使われており、言語ごとに独立したバージョン番号が管理されています。執筆時点では、C++ ランタイムは 7.35-dev、Java は 4.35-devprotoc 本体は 35-dev です。各言語のランタイムが異なるペースで進化するため、バージョンは分岐しています。

ビルドシステムの二重構造もこのリポジトリの特徴です。正規のビルドシステムは Bazel で、外部依存(Abseil、rules_cc、zlib など)はすべて MODULE.bazel に定義されています。エコシステムとの互換性のために CMake も副次的にサポートされており、両者をつなぐのが pkg/BUILD.bazel にある gen_file_lists ルールです。Bazel ターゲットからファイルリストを生成し、CMake がそれを参照する仕組みになっています。

flowchart LR
    A["Bazel BUILD files<br/>(canonical)"] --> B["gen_file_lists rule<br/>pkg/BUILD.bazel"]
    B --> C["src_file_lists.cmake"]
    C --> D["CMakeLists.txt<br/>(secondary)"]

ファイル命名規則も一貫しています。

  • _lite サフィックスは MessageLite のみ(リフレクションなし)を意味する
  • internal/ サブディレクトリは実装の詳細を格納する
  • port_def.inc / port_undef.inc はプラットフォーム固有のマクロ定義を囲むガードのペア
  • src/google/protobuf/ 内の *.proto ファイルは well-known types と内部スキーマ

適合性テストconformance/ 配下)は、全言語にわたる正確さの唯一の情報源として機能します。各言語には専用のフェイルリストファイル(例:failure_list_python.txt)があり、既知の動作の差異が明示されています。これは、シンプルながら非常に効果的な仕様の契約メカニズムです。

次のステップ

全体像が掴めたところで、いよいよ深みへと進みましょう。次の記事では protoc 内部のコンパイルパイプライン全体を追いかけます。.proto ファイルをレキシングする手書きのトークナイザーから FileDescriptorProto メッセージを構築する再帰下降パーサーへ。さらにファイル間の参照を解決する DescriptorPool、言語ごとのソースコードを出力する CodeGenerator インターフェースまでを一気に解説します。「すべての proto ファイルを記述する proto ファイル」という美しい自己参照の謎、descriptor.proto についても深く掘り下げます。