Read OSS

Rust インテグレーションと C++ コードジェネレーター:コード生成パターンの研究

上級

前提知識

  • 第4回:シリアライゼーションの内部実装(TcTable、アリーナ)
  • 第5回:μpb ランタイムのアーキテクチャ
  • Rust の所有権モデルと C++ テンプレートパターンへの理解

Rust インテグレーションと C++ コードジェネレーター:コード生成パターンの研究

コード生成とは、言語設計と現実的なエンジニアリングが交わる場所です。ターゲット言語ごとに、生成される protobuf コードの設計に異なる制約が課されます。ジェネレーターはその制約を乗り越えながら、使いやすくパフォーマンスにも優れたコードを生み出さなければなりません。

本記事では、設計スペクトルの両極を体現する2つのジェネレーターを掘り下げます。一方は、デュアルカーネルアーキテクチャによって所有権にまつわる独自の課題を解決した Rust ジェネレーター、もう一方は、10種類以上のフィールド型の複雑さをストラテジーパターンで整理した成熟した C++ ジェネレーターです。また、第3の選択肢として位置づけられる新しい C++ API、HPB についても取り上げます。

Rust のデュアルカーネルアーキテクチャ

Rust の protobuf サポートは、アーキテクチャ的に他と一線を画しています。完全に異なる2つのランタイムバックエンドに対応しているのです。rust/cpp_kernel/mod.rs は C++ protobuf ランタイムをベースにしたバックエンドを、rust/upb_kernel/mod.rs は upb をベースにしたバックエンドをそれぞれ提供しています。

なぜ2つのバックエンドが必要なのでしょうか。理由は実用的なところにあります。Google の内部システムは C++ protobuf を広く使っており、Google 内部で動く Rust コードは既存の C++ メッセージインスタンスと相互運用できなければなりません。一方、外部ユーザーにとっては upb の軽量さがメリットになります。どちらかに統一して相手方に適応を強いるのではなく、Rust チームは抽象化レイヤーを構築するという道を選びました。

flowchart TD
    subgraph "User-Facing API"
        API["Rust Protobuf API<br/>View&lt;'msg, T&gt;, Mut&lt;'msg, T&gt;"]
    end
    
    subgraph "Kernel Abstraction"
        TRAIT["Kernel trait implementations<br/>(Message, Serialize, etc.)"]
    end
    
    subgraph "cpp_kernel"
        CPP["C++ protobuf FFI<br/>message.rs, repeated.rs,<br/>map.rs, string.rs"]
    end
    
    subgraph "upb_kernel"
        UPB["upb FFI<br/>message.rs, repeated.rs,<br/>map.rs, string.rs"]
    end
    
    API --> TRAIT
    TRAIT --> CPP
    TRAIT --> UPB

両カーネルとも、messagerepeatedmapstringraw という同じサブモジュール群をエクスポートしています。cpp_kernel は std::ffi::{c_int, c_void} をインポートし、FFI 経由で生の C++ ポインターを扱います。upb_kernel は upb のアリーナ型とミニテーブル型をインポートします。どちらのカーネルが有効かはユーザーから見えません。生成されたコードとランタイムライブラリが、統一された API として提供されるからです。

プロキシパターン:View 型と Mut 型

Rust protobuf の設計で最も知的に面白いのが、プロキシ型システムです。その設計思想は rust/proxied.rs の冒頭に詳しく記されています。

問題の核心は、Rust の参照(&T&mut T)が protobuf のフィールドアクセスに適していないことです。理由は2つあります。

  1. メモリ表現の不一致:フィールドのメモリ上の表現が、ユーザーの期待と異なる場合があります。たとえば、アクセス頻度の低い int64 フィールドが 32 ビットのパック形式で格納されていたり、プレゼンス情報がインラインではなく中央集権的なビットセットに保持されていたりします。i64 として連続したメモリ上に値が存在しなければ、&i64 参照を作ることはできません。

  2. アリーナのライフタイムとの結合:upb では、リピートフィールドへの追加や文字列のセットといったミューテーション操作にアリーナパラメーターが必要です。Rust の &mut T にはこの追加コンテキストを持つ手段がありません。さらに深刻なのは、&mut Tmem::swap() を許してしまう点です。これによって、アリーナをまたいでポインターが交換され、アリーナ所有のデータが静かに破壊されるおそれがあります。

この問題を解決するのがプロキシ型です。

pub trait Proxied: SealedInternal + AsView<Proxied = Self> + Sized + 'static {
    type View<'msg>: AsView<Proxied = Self> + IntoView<'msg>;
}

pub trait MutProxied: SealedInternal + Proxied + AsMut<MutProxied = Self> + 'static {
    type Mut<'msg>: AsMut<MutProxied = Self> + IntoMut<'msg> + IntoView<'msg>;
}
classDiagram
    class Proxied {
        <<trait>>
        +View~'msg~ : AsView
    }
    
    class MutProxied {
        <<trait>>
        +Mut~'msg~ : AsMut + IntoView
    }
    
    class ViewT["View&lt;'msg, T&gt;"] {
        "Type alias for T::View&lt;'msg&gt;"
        "Like &'msg T but can carry arena"
    }
    
    class MutT["Mut&lt;'msg, T&gt;"] {
        "Type alias for T::Mut&lt;'msg&gt;"
        "Like &'msg mut T but arena-safe"
    }
    
    Proxied <|-- MutProxied
    Proxied --> ViewT : defines
    MutProxied --> MutT : defines

View<'msg, T> は読み取り用プロキシ(&'msg T に相当)、Mut<'msg, T> は書き込み用プロキシ(&'msg mut T に相当)です。これらは単純な参照ではなく、アリーナポインターを保持し、パックされたストレージを扱い、安全でない swap を防ぐスマートなラッパーです。ライフタイムパラメーター 'msg により、プロキシが借用元のメッセージより長く生き残ることを防いでいます。

Tips: アリーナ割り当てデータを扱う Rust API を設計するなら、protobuf のプロキシパターンは「安定したメモリ表現を持たないデータへの参照」という問題に対する、よく練られた解決策です。proxied.rs のコメントブロックはリポジトリ全体でも屈指の設計解説文書です。ぜひ一読してみましょう。

Rust コードジェネレーター

RustGenerator は、CodeGenerator のサブクラスとしてシンプルに実装されています。

class PROTOC_EXPORT RustGenerator final
    : public google::protobuf::compiler::CodeGenerator {
 public:
    bool Generate(const FileDescriptor* file, const std::string& parameter,
                  GeneratorContext* generator_context,
                  std::string* error) const override;

    uint64_t GetSupportedFeatures() const override {
        return FEATURE_PROTO3_OPTIONAL | FEATURE_SUPPORTS_EDITIONS;
    }
    Edition GetMinimumEdition() const override { return Edition::EDITION_PROTO2; }
    Edition GetMaximumEdition() const override { return Edition::EDITION_2024; }
};

このジェネレーターは proto2/proto3 と Editions システム(Edition 2024 まで)の両方に対応しています。Generate() メソッドは、リンクされているカーネルに関わらず動作する Rust ソースファイルを生成します。生成されたコードは Proxied / MutProxied トレイトを使っているため、フィールドアクセサーは生の参照ではなく View 型・Mut 型を返します。

Rust ジェネレーターは他の言語ジェネレーターと並んで src/google/protobuf/compiler/rust/ に置かれており、プラグインではなく組み込みジェネレーターとして実装されています。これは意図的な選択であり、protoc のデスクリプターシステムおよび Editions サポートとの緊密な統合を確保するためのものです。

C++ コードジェネレーター:フィールド型に対するストラテジーパターン

C++ コードジェネレーターは、リポジトリ内で最も成熟し、最も複雑なジェネレーターです。その設計の核心は、フィールドのコード生成に用いられるストラテジー階層です。

FieldGeneratorBase は、すべてのフィールド型ジェネレーターが継承する抽象基底クラスです。フィールドの属性を問い合わせるための豊富な述語メソッドを提供しています。

class FieldGeneratorBase {
 public:
    bool should_split() const;           // Cold split section?
    bool is_trivial() const;             // int, float, double, enum, bool?
    bool has_trivial_value() const;      // Trivial or raw pointer?
    bool has_trivial_zero_default() const; // memset-zero initializable?
    bool is_message() const;             // Message or group type?
    bool is_weak() const;                // Weak message field?
    bool is_lazy() const;                // Lazy message field?
    bool is_string() const;              // String or bytes?
    // ... virtual methods for codegen
};
classDiagram
    class FieldGeneratorBase {
        <<abstract>>
        +should_split() bool
        +is_trivial() bool
        +is_message() bool
        +is_string() bool
        +GenerateAccessorDeclarations()*
        +GenerateAccessorDefinitions()*
        +GenerateMergingCode()*
        +GenerateSwappingCode()*
    }
    
    class PrimitiveFieldGenerator {
        "int32, int64, float, etc."
    }
    class StringFieldGenerator {
        "string, bytes"
    }
    class MessageFieldGenerator {
        "Nested messages"
    }
    class MapFieldGenerator {
        "map&lt;K, V&gt; fields"
    }
    class EnumFieldGenerator {
        "Enum-typed fields"
    }
    class CordFieldGenerator {
        "Cord-backed strings"
    }
    
    FieldGeneratorBase <|-- PrimitiveFieldGenerator
    FieldGeneratorBase <|-- StringFieldGenerator
    FieldGeneratorBase <|-- MessageFieldGenerator
    FieldGeneratorBase <|-- MapFieldGenerator
    FieldGeneratorBase <|-- EnumFieldGenerator
    FieldGeneratorBase <|-- CordFieldGenerator

各具象ジェネレーターは GenerateAccessorDeclarations()GenerateAccessorDefinitions()GenerateMergingCode()GenerateSwappingCode() といった仮想メソッドを実装します。メッセージレベルのジェネレーターがすべてのフィールドジェネレーターを統括し、hasbit の割り当て、oneof ユニオンの管理、そして .pb.h.pb.cc ファイル全体の生成を担います。

CppGenerator クラス自体は、Runtime enum を通じて複数のランタイムモードをサポートしています。

enum class Runtime {
    kGoogle3,           // Internal google3 runtime
    kOpensource,        // Open-source runtime
    kOpensourceGoogle3  // Open-source with google3 paths
};

これにより、同じジェネレーターが Google 内部のビルドシステム向けとオープンソースリリース向けの両方のコードを生成でき、#include パスが適切に切り替わります。この挙動は opensource_runtime_ フラグと runtime_include_base_ 文字列によって制御されています。

HPB:upb 上に構築された新しい C++ API

HPB(Header-based Protobuf)は、第3の C++ の道を示すものです。重量級の C++ protobuf ライブラリではなく、upb の軽量ランタイムをベースに、モダンな C++ の使い心地を提供します。

メインヘッダーを見ると、Rust と同様のデュアルバックエンド設計が読み取れます。

#if HPB_INTERNAL_BACKEND == HPB_INTERNAL_BACKEND_UPB
#include "hpb/backend/upb/upb.h"
#elif HPB_INTERNAL_BACKEND == HPB_INTERNAL_BACKEND_CPP
#include "hpb/backend/cpp/cpp.h"
#else
#error hpb backend unknown
#endif

HPB の API はアリーナベースのメッセージ生成とポインターベースのアクセスを採用しています。

template <typename T>
typename T::Proxy CreateMessage(Arena& arena) {
    return backend::CreateMessage<T>(arena);
}

Ptr<T>Proxy 型は、Rust の ViewMut に相当する役割を担います。生のポインターを露出させることなく、アリーナ割り当てされたメッセージへの安全なアクセスを提供するのです。DeepCopy 操作は明示的に行う設計になっており、所有権の移転が API 上でわかりやすく表現されています。

flowchart TD
    subgraph "Traditional C++ Protobuf"
        TC["Full C++ runtime<br/>~MB code size<br/>Global state<br/>Reflection built-in"]
    end
    
    subgraph "HPB"
        HPB_API["Modern C++ API<br/>Small code size<br/>No global state<br/>Opt-in reflection"]
        HPB_API --> UPB_BE["upb backend"]
        HPB_API --> CPP_BE["C++ backend"]
    end
    
    subgraph "Raw upb"
        RAW["C API<br/>Minimal code size<br/>Manual MiniTable management"]
    end

HPB はまだ発展途上ですが、モダンな C++ protobuf API のあるべき姿に対する protobuf チームのビジョンを体現しています。アリーナ中心の設計、明示的な所有権、そして重量級の C++ ライブラリではなくコンパクトな upb ランタイムによるバックエンド——これらがその特徴です。

Tips: 既存の .pb.h API との後方互換性が不要な新規 C++ プロジェクトで protobuf を使うなら、HPB の評価を検討してみてください。まだ若いプロジェクトですが、従来よりも大幅に軽量です。

ジェネレーター全体に共通するパターン

各ジェネレーターを横断して眺めると、いくつかの共通パターンが浮かび上がります。

  1. バックエンド抽象化:Rust と HPB はどちらも、統一された API の裏側で複数のランタイムバックエンドをサポートしています。protobuf チームがより多くの言語を upb へ移行させるにつれて、このパターンはさらに広がっていくでしょう。

  2. アリーナ安全性のためのプロキシ型:Rust の View/Mut と HPB の Ptr/Proxy は、アリーナ所有データへのアクセスという同じ問題に対して、独立に類似した解決策へとたどり着いています。

  3. フィールド型に対するストラテジーパターン:C++ ジェネレーターのフィールドジェネレーター階層が最も明示的な例ですが、すべてのジェネレーターが内部的にフィールド型でのディスパッチを行っています。

  4. Editions サポート:すべてのモダンなジェネレーターが GetMinimumEdition() / GetMaximumEdition() を実装し、第3回で取り上げた FeatureResolver インフラを活用している。

第7回では実装の詳細から一歩引いて、protobuf がコンフォーマンステストスイート・障害追跡システム・CI インフラを通じて、すべての言語にわたる正確性をどのように維持しているかを見ていきます。