MongoDB 内嵌 protobuf 的 upb 代码生成器共享内部 API:upb_generator/common 模块全解析
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
导读
本文聚焦 MongoDB 仓库内嵌的 protobuf 发行版中upb_generator/common目录(README.md),它是 upb 代码生成器体系里被多个生成器共同复用的"内部工具层":一方面提供命名与文件头生成的通用函数(names库),另一方面提供把 C++ protobuf 反射对象桥接到 upb 运行时定义(def)的转换工具(cpp_to_upb_def库)。读完本文,你将理解这两个内部库每个 API 的精确语义、底层实现、Bazel 可见性约束,以及它们在 upb 的 C / minitable / reflection 生成器和 hpb 生成器中的真实调用方式。
一、模块定位:README 定义了什么
upb_generator/common目录的 README.md 只有一句话,但精准划定了边界:
This directory contains APIs that are used by multiple code generators, but not public to users.
这句话包含两个关键信息,也是理解整个模块的两条主线:
- 被多个代码生成器复用:这里不是某个单独生成器的私有实现,而是 upb_generator 下 c(C 代码生成)、minitable(mini table 生成)、reflection(upbdefs 生成)以及 protobuf 上层 hpb 生成器共用的公共设施;
- 不对用户公开:这些 API 仅供生成器内部使用,不构成 upb 对外的公共 API,用户的生成代码不应依赖它们。
从目录结构看,该模块由两个 Bazel 目标组成(BUILD):
names:纯字符串工具库,仅依赖 Abseil 字符串组件;cpp_to_upb_def:C++ Descriptor 与 upb def 之间的桥接库,依赖 protobuf 与 upb 的 reflection、mini_table、mem 等模块。
二、names 库:跨生成器的命名与头部生成工具
names库的声明位于 names.h,实现在 names.cc。它提供 5 个纯函数,全部基于absl::string_view,不触碰 protobuf 反射,因此可以被打包成"轻量目标",供其他生成器引用而不拖入反射依赖——这一点在 BUILD 的注释中明确强调。
2.1 IsDescriptorProto:识别描述符原型文件
bool IsDescriptorProto(absl::string_view filename) { return filename == "net/proto2/proto/descriptor.proto" || filename == "google/protobuf/descriptor.proto"; }用于判断给定文件是否是descriptor.proto(同时兼容 Google 内部路径net/proto2/proto/descriptor.proto与开源路径google/protobuf/descriptor.proto)。descriptor.proto 是描述其他 proto 文件的"元原型",生成器对它的处理往往需要特判。真实调用证据:
- c/names_internal.cc:当输入是 descriptor.proto 时,头文件名直接使用固定的
descriptor.upb.h,而不是基于文件路径推导; - minitable/names_internal.cc:同理,对 descriptor.proto 使用固定文件名
descriptor.upb_minitable.h。
2.2 StripExtension:去掉文件名扩展名
std::string StripExtension(absl::string_view fname) { size_t lastdot = fname.find_last_of('.'); if (lastdot == std::string::npos) { return std::string(fname); } return std::string(fname.substr(0, lastdot)); }取最后一个.之前的部分;若没有点则原样返回。这是生成器推导"输出文件基名"的基础步骤,例如:
- c/generator.cc:
foo.proto的 C 源文件名为StripExtension(file.name()) + ".upb.c"; - minitable/generator.cc:生成
StripExtension(proto_filename) + ".upb_minitable.c"; - reflection/header.cc:生成
.upbdefs.h;reflection/source.cc 生成.upbdefs.c。
2.3 IncludeGuard 与内部辅助函数:生成头文件保护宏
std::string ToCIdent(absl::string_view str) { return absl::StrReplaceAll(str, {{".", "_"}, {"/", "_"}, {"-", "_"}}); } std::string ToPreproc(absl::string_view str) { return absl::AsciiStrToUpper(ToCIdent(str)); } std::string IncludeGuard(absl::string_view filename) { return ToPreproc(filename) + "_UPB_H_"; }流程清晰:先把文件名中的.、/、-全部替换为_(ToCIdent),再转为大写(ToPreproc),最后拼接_UPB_H_后缀。例如google/protobuf/descriptor.proto会得到形如GOOGLE_PROTOBUF_DESCRIPTOR_PROTO_UPB_H_的宏。它被用在:
- c/generator.cc 与 minitable/generator.cc:生成各 .upb.h / .upb_minitable.h 的
#ifndef头; - reflection/header.cc:以
{"include_guard", IncludeGuard(file.name())}的键值对形式注入模板渲染上下文。
2.4 FileWarning:生成"禁止手改"警告头
std::string FileWarning(absl::string_view filename) { return absl::Substitute( "/* This file was generated by upb_generator from the input file:\n" " *\n" " * $0\n" " *\n" " * Do not edit -- your changes will be discarded when the file is\n" " * regenerated.\n" " * NO CHECKED-IN " "PROTOBUF GENCODE */\n" "\n", filename); }生成的标准警告注释块包含输入文件名,并明确"NO CHECKED-IN PROTOBUF GENCODE"。它在 c、minitable、reflection 三个生成器中都被写入输出文件头部(如 c/generator.cc、minitable/generator.cc、reflection/header.cc)。
2.5 PadPrefix:条件性空格前缀
std::string PadPrefix(absl::string_view tag) { return tag.empty() ? "" : absl::StrCat(" ", tag); }一个极简工具:非空 tag 前补一个空格,空 tag 返回空串。用于在生成代码中按需拼接带空格的注释/标记前缀,避免出现多余的前导空格。
三、cpp_to_upb_def 库:C++ Descriptor 与 upb def 的桥接
cpp_to_upb_def库(cpp_to_upb_def.h / cpp_to_upb_def.cc)解决一个核心矛盾:代码生成器通常拿到的是 protobuf C++ 反射对象(FileDescriptor、Descriptor等),而输出 upb 代码时需要查询 upb 自己的定义池(upb::DefPool)。该库提供两者之间的双向查找与注册接口。
3.1 ToUpbProto:把 FileDescriptor 序列化回 upb 可解析的原型
google_protobuf_FileDescriptorProto* ToUpbProto(const FileDescriptor* file, upb::Arena* arena) { google::protobuf::FileDescriptorProto proto; file->CopyTo(&proto); std::string serialized_proto = proto.SerializeAsString(); google_protobuf_FileDescriptorProto* upb_proto = google_protobuf_FileDescriptorProto_parse( serialized_proto.data(), serialized_proto.size(), arena->ptr()); ABSL_CHECK(upb_proto) << "Failed to parse proto"; return upb_proto; }实现思路是"绕一圈":先把 C++ 的FileDescriptor拷贝到FileDescriptorProto并序列化为字符串,再用 upb 的解析函数(google_protobuf_FileDescriptorProto_parse)在临时upb::Arena上重建 upb 版本的描述符。这样两个体系通过 wire format 完成数据交换,解析失败会立即触发ABSL_CHECK终止。
3.2 AddFile:递归注册文件及其全部依赖
void AddFile(const FileDescriptor* file, upb::DefPool* pool) { const std::string name(file->name()); if (pool->FindFileByName(name.c_str())) return; // 去重 for (int i = 0; i < file->dependency_count(); i++) { AddFile(file->dependency(i), pool); // 先注册依赖 } upb::Arena tmp_arena; upb::Status status; ABSL_CHECK(pool->AddFile(ToUpbProto(file, &tmp_arena), &status)) << status.error_message(); }关键设计有两处:
- 去重:若池中已有同名文件则直接返回,避免重复注册;
- 依赖优先:与
google::protobuf::DescriptorPool一致,upb::DefPool要求先注册全部依赖才能注册自身,因此这里按dependency_count()深度优先递归。
从 hpb_generator/context.h 可以看到它被 hpb 生成器在构造上下文时调用(upb::generator::AddFile(file, &pool_)),确保后续所有查找都能命中 defpool。
3.3 正向查找:C++ 反射对象 → upb def 指针
四个查找函数均以full_name()为键在 defpool 中查找,查找失败即ABSL_CHECK报错:
| 函数 | 输入 | 输出 | 关键约束 |
|---|---|---|---|
FindMessageDef | Descriptor* | upb::MessageDefPtr | 消息必须已通过AddFile入池,否则失败 |
FindEnumDef | EnumDescriptor* | upb::EnumDefPtr | 枚举必须已在池中 |
FindBaseFieldDef | FieldDescriptor* | upb::FieldDefPtr | 仅限非扩展字段:ABSL_CHECK(!field->is_extension()),按containing_type+ 字段号查找 |
FindExtensionDef | FieldDescriptor* | upb::FieldDefPtr | 仅限扩展字段:ABSL_CHECK(field->is_extension()),直接按全名查扩展 |
其中FindBaseFieldDef的实现(cpp_to_upb_def.cc)值得注意:它先定位字段所属消息,再调用message_def.FindFieldByNumber(field->number()),即用字段号而非字段名作为跨体系匹配键——因为字段号才是 .proto 语义上稳定不变的标识。
hpb_generator/context.h 中FindBaseFieldDef(pool_, field).layout_index()的用法展示了典型场景:生成器需要把字段在 mini table 中的 layout index 直接写入生成的 C++ 代码。
3.4 反向查找:从 upb_MiniTableField 找回 FieldDescriptor
const FieldDescriptor* FindFieldDescriptor( const Descriptor* message, const upb_MiniTableField* field_def) { int field_number = upb_MiniTableField_Number(field_def); const FieldDescriptor* field = message->FindFieldByNumber(field_number); ABSL_CHECK(field) << "No field in message " << message->full_name() << " with number " << field_number; return field; }这是正向查找的逆操作:当生成器手头只有 upb 的 mini table 字段结构(upb_MiniTableField,这是 upb 运行时紧凑内存布局的字段描述)时,通过upb_MiniTableField_Number取出字段号,再在 C++ 的Descriptor中反查FieldDescriptor。这一能力让生成器可以在输出代码前,把 upb 布局层面的信息(如字段在 mini table 中的偏移、mode 位)与 C++ 反射的语义信息(如字段名、类型、选项)对齐。
四、BUILD 可见性设计:如何落实"不公开给用户"
README 中"not public to users"的承诺,在 Bazel 层面由 BUILD 的visibility属性强制执行:
cc_library( name = "names", ... visibility = ["//upb_generator:__subpackages__"], ) cc_library( name = "cpp_to_upb_def", ... visibility = [ "//src/google/protobuf/compiler/hpb:__subpackages__", "//third_party/kotlin/protobuf/generator/native:__subpackages__", ], )names只对//upb_generator及其子包开放(c、minitable、reflection 生成器均在其下);cpp_to_upb_def额外对 hpb 编译器(//src/google/protobuf/compiler/hpb)与 Kotlin 生成器开放,这与源码中 hpb_generator/context.h 的#include "upb_generator/common/cpp_to_upb_def.h"相互印证。
同时 BUILD 顶部注释明确names库的设计约束:不应依赖 upb 反射或 C++ proto 反射,保持轻量,供其他生成器引用时不会拖入两套反射体系。这一约束解释了为什么命名工具全部实现为纯字符串操作,而桥接逻辑被拆到独立的cpp_to_upb_def目标中。
五、在 upb 生成器体系中的整体位置
将上述调用链汇总,可以还原upb_generator/common在生成流程中的角色:
- C 生成器(c/generator.cc):用
FileWarning写警告头、IncludeGuard写保护宏、StripExtension推导.upb.c/.upb.h文件名; - minitable 生成器(minitable/generator.cc):同样的三件套,外加基于
StripExtension的弱 mini table 文件命名; - reflection 生成器(reflection/header.cc / reflection/source.cc):把
FileWarning、IncludeGuard以键值对注入模板渲染,输出.upbdefs.h/.c; - hpb 生成器(hpb_generator/context.h):通过
AddFile建立 defpool,再以FindBaseFieldDef等桥接 API 获取 upb 视角的字段定义,完成 C++ 反射到 upb 布局的映射。
可以看出:names承担"文本形态"的通用性(文件名、宏、注释),cpp_to_upb_def承担"语义映射"的通用性(Descriptor ↔ def 双向转换)。两者共同构成 upb 多生成器复用、且不对外暴露的内部工具层,这正是 README 那句简短定位的完整技术内涵。
六、小结与阅读指引
upb_generator/common是一个"小模块、大用途"的典型:它没有复杂的业务逻辑,却是 upb 代码生成体系保持一致性(统一命名规则、统一警告头、统一桥接语义)的关键地基。理解它的价值,不在于每个函数本身的复杂度,而在于它如何通过 Bazel 可见性、依赖约束与清晰的 API 划分,支撑起 c / minitable / reflection / hpb 四类生成器的并行演进。
建议按以下顺序深入仓库源码:
- names.h → names.cc:掌握 5 个命名/头部工具的精确语义;
- cpp_to_upb_def.h → cpp_to_upb_def.cc:掌握 Descriptor 与 upb def 的双向桥接及依赖优先注册策略;
- BUILD:理解可见性与依赖约束如何落实"内部 API"定位;
- 再对照 c/generator.cc、minitable/generator.cc、reflection/header.cc、hpb_generator/context.h 观察真实调用场景。
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考