ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

MongoDB 内嵌 protobuf 的 upb 代码生成器共享内部 API:upb_generator/common 模块全解析 MongoDB 内嵌 protobuf 的 upb 代码生成器共享内部 APIupb_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 下 cC 代码生成、minitablemini table 生成、reflectionupbdefs 生成以及 protobuf 上层 hpb 生成器共用的公共设施不对用户公开这些 API 仅供生成器内部使用不构成 upb 对外的公共 API用户的生成代码不应依赖它们。从目录结构看该模块由两个 Bazel 目标组成BUILDnames纯字符串工具库仅依赖 Abseil 字符串组件cpp_to_upb_defC 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.ccfoo.proto的 C 源文件名为StripExtension(file.name()) .upb.cminitable/generator.cc生成StripExtension(proto_filename) .upb_minitable.creflection/header.cc生成.upbdefs.hreflection/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报错函数输入输出关键约束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 找回 FieldDescriptorconst 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/.chpb 生成器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),仅供参考
返回列表