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<'msg, T>, Mut<'msg, T>"]
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
两套内核导出相同的子模块集合:message、repeated、map、string 和 raw。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 字段访问,原因有两点:
-
内存表示不匹配:字段的实际内存布局可能与用户预期不符。例如,访问频率较低的
int64字段可能采用压缩的 32 位存储,或者 presence 信息被统一存储在一个位集合中而非内联。如果值在内存中并不以连续的i64形式存在,就无法构造&i64引用。 -
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<'msg, T>"] {
"Type alias for T::View<'msg>"
"Like &'msg T but can carry arena"
}
class MutT["Mut<'msg, T>"] {
"Type alias for T::Mut<'msg>"
"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,因此字段访问器返回的是 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<K, V> 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 的 View 和 Mut 类似——在不暴露原始指针的前提下,提供对 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.hAPI,不妨评估一下 HPB。它相对年轻,但在体积上要精简得多。
跨生成器的共性模式
纵观所有生成器,可以归纳出以下几条共性规律:
-
后端抽象:Rust 和 HPB 都在统一 API 背后支持多套运行时后端。随着 protobuf 团队持续推动更多语言向 upb 迁移,这一模式有望进一步普及。
-
代理类型保障 arena 安全:Rust 的
View/Mut和 HPB 的Ptr/Proxy针对 arena 所有数据的访问问题,各自独立地得出了相似的解决方案。 -
字段类型的策略模式:C++ 生成器的字段生成器层次结构是最典型的体现,但每个生成器内部都在按字段类型进行分派。
-
Editions 支持:每个现代生成器都实现了
GetMinimumEdition()/GetMaximumEdition(),并使用了第 3 篇中介绍的FeatureResolver基础设施。
在第 7 篇中,我们将跳出实现细节,从更宏观的视角审视 protobuf 如何通过一致性测试套件、失败追踪系统和 CI 基础设施,在所有支持的语言中持续保障正确性。