Read OSS

Rust 集成与 C++ 代码生成器:代码生成模式深度解析

高级

前置知识

  • 第 4 篇:序列化内部机制(TcTable、arena)
  • 第 5 篇:μpb 运行时架构
  • 熟悉 Rust 所有权模型与 C++ 模板模式

Rust 集成与 C++ 代码生成器:代码生成模式深度解析

代码生成是语言设计与工程实践的交汇地带。每种目标语言都对生成的 protobuf 代码提出了不同的约束,生成器必须在这些约束下产出既符合语言习惯、又具备良好性能的代码。

本文将深入研究两个处于设计光谱两端的生成器:Rust 生成器通过双内核架构解决了独特的所有权问题,而 C++ 生成器则借助成熟的策略模式应对 10 余种字段类型带来的复杂性。此外,我们还将介绍 HPB——一条全新的 C++ API 发展路径。

Rust 的双内核架构

Rust 的 protobuf 支持在架构上独树一帜:它同时支持两套完全不同的运行时后端。rust/cpp_kernel/mod.rs 模块以完整的 C++ protobuf 运行时作为后端,而 rust/upb_kernel/mod.rs 则以 upb 作为后端。

为何需要两套后端?原因非常务实。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 的 arena 和 mini-table 类型。用户无需关心当前激活的是哪套内核——生成的代码和运行时库共同呈现出一套统一的 API。

代理模式:View 与 Mut 类型

Rust protobuf 在智识层面最有趣的设计,当属其代理类型系统。rust/proxied.rs 对此有极为详尽的阐述。

问题的根源在于:Rust 的引用类型(&T&mut T)无法直接用于 protobuf 字段访问,原因有两点:

  1. 内存表示不匹配:字段的实际内存布局可能与用户预期不符。例如,访问频率较低的 int64 字段可能采用压缩的 32 位存储,或者 presence 信息被统一存储在一个位集合中而非内联。如果值在内存中并不以连续的 i64 形式存在,就无法构造 &i64 引用。

  2. arena 生命周期耦合:在 upb 中,写入操作(向 repeated 字段追加元素、设置字符串等)需要传入 arena 参数,而 Rust 的 &mut T 无法携带这类额外上下文。更糟糕的是,&mut T 允许调用 mem::swap(),这可能在不经意间将属于不同 arena 的指针互换,从而悄无声息地破坏数据。

解决方案是引入代理类型:

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)。它们并非普通引用,而是智能包装类型,能够携带 arena 指针、处理压缩存储,并防止不安全的内存交换。'msg 生命周期参数确保代理的存活时间不会超过其所借用的消息。

提示: 如果你正在为 arena 分配的数据设计 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 trait,因此字段访问器返回的是 ViewMut 类型,而非普通引用。

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 union 管理,以及完整 .pb.h.pb.cc 文件的生成。

CppGenerator 类本身通过 Runtime 枚举支持多种运行时模式:

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)代表了 C++ 的第三条路径——以 upb 轻量运行时为底层,提供符合现代 C++ 习惯的 API,而非依赖完整的 C++ protobuf 库。

主头文件揭示了与 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 采用基于 arena 的消息创建方式和指针式访问:

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

Ptr<T>Proxy 类型的作用与 Rust 的 ViewMut 类似——在不暴露原始指针的前提下,提供对 arena 分配消息的安全访问。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 目前仍在持续演进,但它代表了 protobuf 团队对现代 C++ protobuf API 的愿景:以 arena 为中心、所有权语义明确,并以紧凑的 upb 运行时取代臃肿的 C++ 库。

提示: 如果你正在启动一个新的 C++ 项目,使用 protobuf 且无需兼容现有 .pb.h API,不妨评估一下 HPB。它相对年轻,但在体积上要精简得多。

跨生成器的共性模式

纵观所有生成器,可以归纳出以下几条共性规律:

  1. 后端抽象:Rust 和 HPB 都在统一 API 背后支持多套运行时后端。随着 protobuf 团队持续推动更多语言向 upb 迁移,这一模式有望进一步普及。

  2. 代理类型保障 arena 安全:Rust 的 View/Mut 和 HPB 的 Ptr/Proxy 针对 arena 所有数据的访问问题,各自独立地得出了相似的解决方案。

  3. 字段类型的策略模式:C++ 生成器的字段生成器层次结构是最典型的体现,但每个生成器内部都在按字段类型进行分派。

  4. Editions 支持:每个现代生成器都实现了 GetMinimumEdition() / GetMaximumEdition(),并使用了第 3 篇中介绍的 FeatureResolver 基础设施。

在第 7 篇中,我们将跳出实现细节,从更宏观的视角审视 protobuf 如何通过一致性测试套件、失败追踪系统和 CI 基础设施,在所有支持的语言中持续保障正确性。