news 2026/9/17 4:13:53

MongoDB 内嵌 protobuf 的 upb 代码生成器共享内部 API:upb_generator/common 模块全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MongoDB 内嵌 protobuf 的 upb 代码生成器共享内部 API:upb_generator/common 模块全解析

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.

这句话包含两个关键信息,也是理解整个模块的两条主线:

  1. 被多个代码生成器复用:这里不是某个单独生成器的私有实现,而是 upb_generator 下 c(C 代码生成)、minitable(mini table 生成)、reflection(upbdefs 生成)以及 protobuf 上层 hpb 生成器共用的公共设施;
  2. 不对用户公开:这些 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++ 反射对象(FileDescriptorDescriptor等),而输出 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(); }

关键设计有两处:

  1. 去重:若池中已有同名文件则直接返回,避免重复注册;
  2. 依赖优先:与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报错:

函数输入输出关键约束
FindMessageDefDescriptor*upb::MessageDefPtr消息必须已通过AddFile入池,否则失败
FindEnumDefEnumDescriptor*upb::EnumDefPtr枚举必须已在池中
FindBaseFieldDefFieldDescriptor*upb::FieldDefPtr仅限非扩展字段:ABSL_CHECK(!field->is_extension()),按containing_type+ 字段号查找
FindExtensionDefFieldDescriptor*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在生成流程中的角色:

  1. C 生成器(c/generator.cc):用FileWarning写警告头、IncludeGuard写保护宏、StripExtension推导.upb.c/.upb.h文件名;
  2. minitable 生成器(minitable/generator.cc):同样的三件套,外加基于StripExtension的弱 mini table 文件命名;
  3. reflection 生成器(reflection/header.cc / reflection/source.cc):把FileWarningIncludeGuard以键值对注入模板渲染,输出.upbdefs.h/.c
  4. 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 四类生成器的并行演进。

建议按以下顺序深入仓库源码:

  1. names.h → names.cc:掌握 5 个命名/头部工具的精确语义;
  2. cpp_to_upb_def.h → cpp_to_upb_def.cc:掌握 Descriptor 与 upb def 的双向桥接及依赖优先注册策略;
  3. BUILD:理解可见性与依赖约束如何落实"内部 API"定位;
  4. 再对照 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 4:13:10

元胞自动机实现人群疏散模型:MATLAB仿真全流程解析

前阵子有个学弟找我问毕业设计&#xff0c;题目是“基于元胞自动机的人口疏散模型MATLAB实现”。他最开始的理解特别乐观&#xff1a;把房间画成网格&#xff0c;人涂成几个格子&#xff0c;设定出口&#xff0c;然后一运行就能看到人流往门口涌&#xff0c;最后做两张曲线图收…

作者头像 李华
网站建设 2026/9/17 4:13:06

涨紧芯轴硬度检测:选对方法比选对设备更重要

1. 项目概述&#xff1a;为什么涨紧芯轴的硬度检测不能“差不多就行”工装涨紧芯轴——这玩意儿听着冷门&#xff0c;但在机加工、齿轮制造、轴承装配、汽车变速箱壳体镗孔这些现场&#xff0c;它就是夹具系统的“心脏”。它不是普通轴&#xff0c;而是靠弹性变形产生径向涨紧力…

作者头像 李华
网站建设 2026/9/17 4:12:33

FPGA积分赛备赛实战:从数电基础到上板调试全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 4:12:32

G-Helper 调校指南:ROG Keris II Ace 无线鼠标设置完整教程

G-Helper 调校指南&#xff1a;ROG Keris II Ace 无线鼠标设置完整教程 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, Vivobook, Zenboo…

作者头像 李华
网站建设 2026/9/17 4:11:54

CSK5062离线语音红绿灯系统实战:工业级状态机与抗干扰设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华