gRPC C++impl/codegen目录解析:生成代码的最小依赖设计与使用边界
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
导读
在 MongoDB 仓库的第三方依赖中,gRPC C++ 提供了一个特殊的头文件目录include/grpcpp/impl/codegen。它既不是公共 API,也不是纯粹的内部实现,而是专门服务于proto 生成代码(由 protoc + gRPC C++ 插件自动产出的*.pb.h/*.grpc.pb.h)的"最小依赖集合"。本指南将结合仓库内的头文件实现与 Bazel 构建规则,剖析该目录存在的原因、它包含哪些内容、为什么用户代码必须绕开它,以及在 Bazel 多语言proto_library场景下它如何帮助控制构建规模。读完本文,你将理解 gRPC C++ 头文件分层的完整脉络,并掌握正确的 include 边界与构建依赖组织方式。
一、impl/codegen目录为什么存在
关联文档 src/third_party/grpc/dist/include/grpcpp/impl/codegen/README.md 开宗明义地给出了这个目录的定位:
该目录的存在,是为了让生成代码可以按需包含其依赖的少量头文件,而无需依赖整个 gRPC C++ 库。
这一设计对 Bazel 用户尤其重要,特别是使用多语言proto_library目标类型时。生成的代码只需要依赖与这些头文件对应的 gRPC C++ 目标,而不是整个 gRPC C++ 代码库——如果后者成立,那么这些目标的构建时间会急剧膨胀,尤其当这些 proto 目标本身甚至不是 C++ 专属时(例如同一份.proto还要产出 Java、Python、Go 代码)。
换句话说,codegen目录是 gRPC C++ 在依赖粒度上做的一层精细切分:把"生成代码编译所必需的、与 protobuf 序列化/反序列化相关的头文件"单独隔离出来,与"运行完整 gRPC 服务/客户端所需的全部实现"解耦。
从仓库目录布局看,gRPC C++ 的头文件体系实际被划分为四个层次:
| 层次 | 路径 | 性质 |
|---|---|---|
| 公共 API | include/grpcpp/(如grpcpp.h、channel.h、server_builder.h) | 用户代码应从这里 include |
| 内部实现(不稳定) | include/grpcpp/impl/ | 仅 gRPC 库内部使用,API 不稳定 |
| 生成代码专用 | include/grpcpp/impl/codegen/ | 仅生成代码与 gRPC 库代码使用 |
| 可访问支持层 | include/grpcpp/support/ | 用户代码可访问的辅助头文件 |
其中impl/目录自身的 README.md 明确警告:
该目录中的 API 不稳定!这些头文件需要被安装,但不属于公共 API,用户不应直接使用。
而codegen目录正是在这个不稳定内部层之下、专为编译期依赖切分而设的进一步细分。
二、codegen目录内容全景
该目录下实际包含约 50 个头文件,覆盖了生成代码编译时所需的几乎全部基础类型。从功能上可以归纳为以下几类(以下分类依据文件名与头文件内容推断,可对照目录逐一验证):
调用链与 RPC 模型:call.h、call_op_set.h、call_op_set_interface.h、call_hook.h、rpc_method.h、rpc_service_method.h、service_type.h
同步与异步流:sync.h、sync_stream.h、async_stream.h、async_unary_call.h、client_unary_call.h
回调风格 API:callback_common.h、client_callback.h、server_callback.h、server_callback_handlers.h
拦截器体系:interceptor.h、interceptor_common.h、client_interceptor.h、server_interceptor.h、intercepted_channel.h、delegating_channel.h
序列化与 protobuf 桥接:serialization_traits.h、proto_utils.h、proto_buffer_reader.h、proto_buffer_writer.h、byte_buffer.h、slice.h、message_allocator.h
上下文与状态:client_context.h、server_context.h、server_interface.h、channel_interface.h、status.h、status_code_enum.h、string_ref.h、time.h、metadata_map.h、stub_options.h
服务端框架:method_handler.h、method_handler_impl.h、async_generic_service.h、completion_queue.h、completion_queue_tag.h
配置与兼容层:config.h、config_protobuf.h、create_auth_context.h,以及子目录 security/auth_context.h。
值得注意的是,这份列表与grpcpp/support/下的支持头文件高度重合——async_stream.h、byte_buffer.h、status.h、string_ref.h、slice.h等在同一份文件在两层目录中同时存在,这正是下文要说的"兼容性转发"机制。
三、从源码看头文件分层的实际实现
3.1 公共 API 的入口:grpcpp.h
用户代码正确的人手点是 src/third_party/grpc/dist/include/grpcpp/grpcpp.h。它通过 IWYU 的begin_exports/end_exports标注,聚合导出了公共 API 的顶层头文件:
grpc/grpc.h grpcpp/channel.h grpcpp/client_context.h grpcpp/completion_queue.h grpcpp/create_channel.h grpcpp/create_channel_posix.h grpcpp/server.h grpcpp/server_builder.h grpcpp/server_context.h grpcpp/server_posix.h grpcpp/version_info.h并声明了grpc::Version()这一全局版本查询函数。用户代码包含<grpcpp/grpcpp.h>即可获得完整且稳定的 C++ API,而无需触及任何impl/codegen内部文件。
3.2 兼容性转发头文件
codegen目录中的一部分头文件当前扮演的是"转发占位"角色。例如 config.h 的完整实现是:
#ifndef GRPCPP_IMPL_CODEGEN_CONFIG_H #define GRPCPP_IMPL_CODEGEN_CONFIG_H // IWYU pragma: private /// TODO(chengyuc): Remove this file after solving compatibility. #include <grpcpp/support/config.h> #endif // GRPCPP_IMPL_CODEGEN_CONFIG_Hstatus.h 采用完全相同的模式,直接转发到<grpcpp/support/status.h>。这类文件上有两条关键信息:
IWYU pragma: private:告知 include-what-you-use 工具,该头文件是私有的,用户不应直接包含;TODO(chengyuc): Remove this file after solving compatibility.:说明这是一段过渡期的兼容层,一旦外部依赖(如旧版生成代码)迁移完毕,这些转发文件会被删除。
这从源码层面印证了关联文档的论断:codegen目录并非 gRPC C++ 的既定长期公共结构,而是为了兼容与依赖切分而存在的中间态。
四、Bazel 构建视角:最小依赖如何落地
4.1 生成代码的编译期依赖
在 Bazel 中,proto 生成代码的依赖切分体现在 gRPC 仓库根 BUILD 中的grpc++_codegen_proto目标(第 2561 行附近):
grpc_cc_library( name = "grpc++_codegen_proto", hdrs = ["include/grpcpp/impl/generic_serialize.h"], external_deps = [ "absl/strings:cord", "protobuf_headers", "protobuf", ], public_hdrs = [ "include/grpc++/impl/codegen/proto_utils.h", "include/grpcpp/impl/codegen/proto_buffer_reader.h", "include/grpcpp/impl/codegen/proto_buffer_writer.h", "include/grpcpp/impl/codegen/proto_utils.h", "include/grpcpp/impl/proto_utils.h", ], deps = [ "grpc++_config_proto", "grpc++_public_hdrs", "grpcpp_status", ], )可以看到,这个目标只向生成代码暴露与 protobuf 序列化、generic_serialize相关的最小头文件集合,外加 protobuf 与 absl 的外部依赖,而不是整个 gRPC 运行时(channel、server、completion queue 的实现全部被排除在外)。这正是关联文档所描述的"只依赖与这些头文件关联的目标"在 BUILD 层面的直接落地。
4.2cc_grpc_library的 codegen 子目标
生成代码的实际装配发生在 bazel/cc_grpc_library.bzl 的cc_grpc_library规则中(第 103~123 行附近)。其核心逻辑是:
- 若未设置
grpc_only,先分别产出proto_library与cc_proto_library目标(第 85~97 行); - 生成一个内部子目标
_<name>_grpc_codegen,通过generate_cc调用//src/compiler:grpc_cpp_plugin生成*.grpc.pb.h/.cc(第 104~113 行); - 最终
cc_library的srcs与hdrs均取自该 codegen 子目标,并在deps中只追加Label("//:grpc++_codegen_proto")(第 115~123 行)。
也就是说,每个通过cc_grpc_library生成的 C++ gRPC 库,其依赖都被精确收敛到grpc++_codegen_proto这个最小集合。此时若构建的是一个多语言共享的proto_library(如 MongoDB 这类大型仓库中同时产出 C++/Python/JS 的 proto),C++ 侧只承担序列化桥接的少量编译开销,而非整套 gRPC C++ 的编译与链接成本。
此外,dist/third_party/BUILD(第 131~132 行)中还存在:
alias( name = "grpc++_codegen_proto", actual = "@com_github_grpc_grpc//:grpc++_codegen_proto", )这表明外部 Bazel 工程可以通过@com_github_grpc_grpc//:grpc++_codegen_proto直接引用这个最小依赖目标。
五、使用边界:用户代码应该怎么做
关联文档对使用者给出了两条明确的红线与一条建议:
- 禁止:用户代码不得从
include/grpcpp/impl/codegen/包含任何头文件; - 允许:只有生成代码与 gRPC 库自身代码可以包含该目录内容;
- 建议:用户代码应从主
grpcpp目录或其可访问的子组件(如grpcpp/support)包含所需头文件。
grpcpp/support/(目录列表)正是为此准备的"用户可访问"层,包含channel_arguments.h、error_details.h、global_callback_hook.h、validate_service_config.h等 25 个支持头文件,以及与 codegen 目录同名的一批基础类型。下表给出常见需求的正确与错误用法对照:
| 需求 | 错误用法(内部) | 正确用法(公共/支持层) |
|---|---|---|
| 获取完整 C++ API | #include <grpcpp/impl/codegen/...> | #include <grpcpp/grpcpp.h> |
使用grpc::Status | #include <grpcpp/impl/codegen/status.h> | #include <grpcpp/support/status.h> |
使用grpc::Slice/ByteBuffer | #include <grpcpp/impl/codegen/slice.h> | #include <grpcpp/support/slice.h> |
使用grpc::string_ref | #include <grpcpp/impl/codegen/string_ref.h> | #include <grpcpp/support/string_ref.h> |
违反了这条边界会带来两重风险:其一,这些头文件不保证 API 稳定,升级 gRPC 版本时可能直接编译失败;其二,它破坏了codegen目录赖以存在的依赖切分初衷,把最小依赖重新拉回整库。
六、未来展望与兼容性过渡
关联文档最后指出:如果该目录存在的动机不再强烈,gRPC 可能会整体移除impl/codegen。触发条件包括:
- 大多数用户迁移离开
proto_library目标类型; - 依赖整个 gRPC C++ 库的额外开销不再显著。
结合前文源码中的TODO(chengyuc): Remove this file after solving compatibility.注释可以推断,这一过渡目前仍在进行中:codegen目录内的转发头文件会随着下游兼容问题的解决而被逐步删除,最终该目录或将整体并入support/与impl/的既有结构。
对仓库使用者而言,这意味着两条长期有效的工程建议:
- 新代码一律走公共 API:
#include <grpcpp/grpcpp.h>或#include <grpcpp/support/xxx.h>,为未来目录调整留足迁移余地; - 构建层面锁定最小依赖:Bazel 场景下应通过
grpc++_codegen_proto(或本地等价目标)承载生成代码的依赖,避免无谓拉高编译规模。
七、小结
include/grpcpp/impl/codegen是 gRPC C++ 在"生成代码编译依赖"与"完整库依赖"之间做出的精细折中:它用一层仅约 50 个头的隔离目录,换来了 Bazel 多语言proto_library场景下 C++ 构建开销的大幅收敛。理解它的存在动机(依赖切分)、内容构成(序列化/调用链/拦截器等基础头)、落地方式(grpc++_codegen_proto目标与cc_grpc_library规则)以及使用红线(用户代码禁止 include),是正确使用 gRPC C++ 与组织相关构建依赖的前提。对 MongoDB 这类在 Bazel 体系中深度使用 gRPC 的大型项目,这条边界直接关系到构建系统的健康度。
gRPC C++impl/codegen目录解析:生成代码的最小依赖设计与使用边界
导读
在 MongoDB 仓库的第三方依赖中,gRPC C++ 提供了一个特殊的头文件目录include/grpcpp/impl/codegen。它既不是公共 API,也不是纯粹的内部实现,而是专门服务于proto 生成代码(由 protoc + gRPC C++ 插件自动产出的*.pb.h/*.grpc.pb.h)的"最小依赖集合"。本指南将结合仓库内的头文件实现与 Bazel 构建规则,剖析该目录存在的原因、它包含哪些内容、为什么用户代码必须绕开它,以及在 Bazel 多语言proto_library场景下它如何帮助控制构建规模。读完本文,你将理解 gRPC C++ 头文件分层的完整脉络,并掌握正确的 include 边界与构建依赖组织方式。
一、impl/codegen目录为什么存在
关联文档 src/third_party/grpc/dist/include/grpcpp/impl/codegen/README.md 开宗明义地给出了这个目录的定位:
该目录的存在,是为了让生成代码可以按需包含其依赖的少量头文件,而无需依赖整个 gRPC C++ 库。
这一设计对 Bazel 用户尤其重要,特别是使用多语言proto_library目标类型时。生成的代码只需要依赖与这些头文件对应的 gRPC C++ 目标,而不是整个 gRPC C++ 代码库——如果后者成立,那么这些目标的构建时间会急剧膨胀,尤其当这些 proto 目标本身甚至不是 C++ 专属时(例如同一份.proto还要产出 Java、Python、Go 代码)。
换句话说,codegen目录是 gRPC C++ 在依赖粒度上做的一层精细切分:把"生成代码编译所必需的、与 protobuf 序列化/反序列化相关的头文件"单独隔离出来,与"运行完整 gRPC 服务/客户端所需的全部实现"解耦。
从仓库目录布局看,gRPC C++ 的头文件体系实际被划分为四个层次:
| 层次 | 路径 | 性质 |
|---|---|---|
| 公共 API | include/grpcpp/(如grpcpp.h、channel.h、server_builder.h) | 用户代码应从这里 include |
| 内部实现(不稳定) | include/grpcpp/impl/ | 仅 gRPC 库内部使用,API 不稳定 |
| 生成代码专用 | include/grpcpp/impl/codegen/ | 仅生成代码与 gRPC 库代码使用 |
| 可访问支持层 | include/grpcpp/support/ | 用户代码可访问的辅助头文件 |
其中impl/目录自身的 README.md 明确警告:
该目录中的 API 不稳定!这些头文件需要被安装,但不属于公共 API,用户不应直接使用。
而codegen目录正是在这个不稳定内部层之下、专为编译期依赖切分而设的进一步细分。
二、codegen目录内容全景
该目录下实际包含约 50 个头文件,覆盖了生成代码编译时所需的几乎全部基础类型。从功能上可以归纳为以下几类(以下分类依据文件名与头文件内容推断,可对照目录逐一验证):
调用链与 RPC 模型:call.h、call_op_set.h、call_op_set_interface.h、call_hook.h、rpc_method.h、rpc_service_method.h、service_type.h
同步与异步流:sync.h、sync_stream.h、async_stream.h、async_unary_call.h、client_unary_call.h
回调风格 API:callback_common.h、client_callback.h、server_callback.h、server_callback_handlers.h
拦截器体系:interceptor.h、interceptor_common.h、client_interceptor.h、server_interceptor.h、intercepted_channel.h、delegating_channel.h
序列化与 protobuf 桥接:serialization_traits.h、proto_utils.h、proto_buffer_reader.h、proto_buffer_writer.h、byte_buffer.h、slice.h、message_allocator.h
上下文与状态:client_context.h、server_context.h、server_interface.h、channel_interface.h、status.h、status_code_enum.h、string_ref.h、time.h、metadata_map.h、stub_options.h
服务端框架:method_handler.h、method_handler_impl.h、async_generic_service.h、completion_queue.h、completion_queue_tag.h
配置与兼容层:config.h、config_protobuf.h、create_auth_context.h,以及子目录 security/auth_context.h。
值得注意的是,这份列表与grpcpp/support/下的支持头文件高度重合——async_stream.h、byte_buffer.h、status.h、string_ref.h、slice.h等在两层目录中同时存在,这正是下文要说的"兼容性转发"机制。
三、从源码看头文件分层的实际实现
3.1 公共 API 的入口:grpcpp.h
用户代码正确的人手点是 src/third_party/grpc/dist/include/grpcpp/grpcpp.h。它通过 IWYU 的begin_exports/end_exports标注,聚合导出了公共 API 的顶层头文件:
grpc/grpc.h grpcpp/channel.h grpcpp/client_context.h grpcpp/completion_queue.h grpcpp/create_channel.h grpcpp/create_channel_posix.h grpcpp/server.h grpcpp/server_builder.h grpcpp/server_context.h grpcpp/server_posix.h grpcpp/version_info.h并声明了grpc::Version()这一全局版本查询函数。用户代码包含<grpcpp/grpcpp.h>即可获得完整且稳定的 C++ API,而无需触及任何impl/codegen内部文件。
3.2 兼容性转发头文件
codegen目录中的一部分头文件当前扮演的是"转发占位"角色。例如 config.h 的完整实现是:
#ifndef GRPCPP_IMPL_CODEGEN_CONFIG_H #define GRPCPP_IMPL_CODEGEN_CONFIG_H // IWYU pragma: private /// TODO(chengyuc): Remove this file after solving compatibility. #include <grpcpp/support/config.h> #endif // GRPCPP_IMPL_CODEGEN_CONFIG_Hstatus.h 采用完全相同的模式,直接转发到<grpcpp/support/status.h>。这类文件上有两条关键信息:
IWYU pragma: private:告知 include-what-you-use 工具,该头文件是私有的,用户不应直接包含;TODO(chengyuc): Remove this file after solving compatibility.:说明这是一段过渡期的兼容层,一旦外部依赖(如旧版生成代码)迁移完毕,这些转发文件会被删除。
这从源码层面印证了关联文档的论断:codegen目录并非 gRPC C++ 的既定长期公共结构,而是为了兼容与依赖切分而存在的中间态。
四、Bazel 构建视角:最小依赖如何落地
4.1 生成代码的编译期依赖
在 Bazel 中,proto 生成代码的依赖切分体现在 gRPC 仓库根 BUILD 中的grpc++_codegen_proto目标(第 2561 行附近):
grpc_cc_library( name = "grpc++_codegen_proto", hdrs = ["include/grpcpp/impl/generic_serialize.h"], external_deps = [ "absl/strings:cord", "protobuf_headers", "protobuf", ], public_hdrs = [ "include/grpc++/impl/codegen/proto_utils.h", "include/grpcpp/impl/codegen/proto_buffer_reader.h", "include/grpcpp/impl/codegen/proto_buffer_writer.h", "include/grpcpp/impl/codegen/proto_utils.h", "include/grpcpp/impl/proto_utils.h", ], deps = [ "grpc++_config_proto", "grpc++_public_hdrs", "grpcpp_status", ], )可以看到,这个目标只向生成代码暴露与 protobuf 序列化、generic_serialize相关的最小头文件集合,外加 protobuf 与 absl 的外部依赖,而不是整个 gRPC 运行时(channel、server、completion queue 的实现全部被排除在外)。这正是关联文档所描述的"只依赖与这些头文件关联的目标"在 BUILD 层面的直接落地。
4.2cc_grpc_library的 codegen 子目标
生成代码的实际装配发生在 bazel/cc_grpc_library.bzl 的cc_grpc_library规则中(第 103~123 行附近)。其核心逻辑是:
- 若未设置
grpc_only,先分别产出proto_library与cc_proto_library目标(第 85~97 行); - 生成一个内部子目标
_<name>_grpc_codegen,通过generate_cc调用//src/compiler:grpc_cpp_plugin生成*.grpc.pb.h/.cc(第 104~113 行); - 最终
cc_library的srcs与hdrs均取自该 codegen 子目标,并在deps中只追加Label("//:grpc++_codegen_proto")(第 115~123 行)。
也就是说,每个通过cc_grpc_library生成的 C++ gRPC 库,其依赖都被精确收敛到grpc++_codegen_proto这个最小集合。此时若构建的是一个多语言共享的proto_library(如 MongoDB 这类大型仓库中同时产出 C++/Python/JS 的 proto),C++ 侧只承担序列化桥接的少量编译开销,而非整套 gRPC C++ 的编译与链接成本。
此外,dist/third_party/BUILD(第 131~132 行)中还存在:
alias( name = "grpc++_codegen_proto", actual = "@com_github_grpc_grpc//:grpc++_codegen_proto", )这表明外部 Bazel 工程可以通过@com_github_grpc_grpc//:grpc++_codegen_proto直接引用这个最小依赖目标。
五、使用边界:用户代码应该怎么做
关联文档对使用者给出了两条明确的红线与一条建议:
- 禁止:用户代码不得从
include/grpcpp/impl/codegen/包含任何头文件; - 允许:只有生成代码与 gRPC 库自身代码可以包含该目录内容;
- 建议:用户代码应从主
grpcpp目录或其可访问的子组件(如grpcpp/support)包含所需头文件。
grpcpp/support/(目录列表)正是为此准备的"用户可访问"层,包含channel_arguments.h、error_details.h、global_callback_hook.h、validate_service_config.h等 25 个支持头文件,以及与 codegen 目录同名的一批基础类型。下表给出常见需求的正确与错误用法对照:
| 需求 | 错误用法(内部) | 正确用法(公共/支持层) |
|---|---|---|
| 获取完整 C++ API | #include <grpcpp/impl/codegen/...> | #include <grpcpp/grpcpp.h> |
使用grpc::Status | #include <grpcpp/impl/codegen/status.h> | #include <grpcpp/support/status.h> |
使用grpc::Slice/ByteBuffer | #include <grpcpp/impl/codegen/slice.h> | #include <grpcpp/support/slice.h> |
使用grpc::string_ref | #include <grpcpp/impl/codegen/string_ref.h> | #include <grpcpp/support/string_ref.h> |
违反了这条边界会带来两重风险:其一,这些头文件不保证 API 稳定,升级 gRPC 版本时可能直接编译失败;其二,它破坏了codegen目录赖以存在的依赖切分初衷,把最小依赖重新拉回整库。
六、未来展望与兼容性过渡
关联文档最后指出:如果该目录存在的动机不再强烈,gRPC 可能会整体移除impl/codegen。触发条件包括:
- 大多数用户迁移离开
proto_library目标类型; - 依赖整个 gRPC C++ 库的额外开销不再显著。
结合前文源码中的TODO(chengyuc): Remove this file after solving compatibility.注释可以推断,这一过渡目前仍在进行中:codegen目录内的转发头文件会随着下游兼容问题的解决而被逐步删除,最终该目录或将整体并入support/与impl/的既有结构。
对仓库使用者而言,这意味着两条长期有效的工程建议:
- 新代码一律走公共 API:
#include <grpcpp/grpcpp.h>或#include <grpcpp/support/xxx.h>,为未来目录调整留足迁移余地; - 构建层面锁定最小依赖:Bazel 场景下应通过
grpc++_codegen_proto(或本地等价目标)承载生成代码的依赖,避免无谓拉高编译规模。
七、小结
include/grpcpp/impl/codegen是 gRPC C++ 在"生成代码编译依赖"与"完整库依赖"之间做出的精细折中:它用一层仅约 50 个头的隔离目录,换来了 Bazel 多语言proto_library场景下 C++ 构建开销的大幅收敛。理解它的存在动机(依赖切分)、内容构成(序列化/调用链/拦截器等基础头)、落地方式(grpc++_codegen_proto目标与cc_grpc_library规则)以及使用红线(用户代码禁止 include),是正确使用 gRPC C++ 与组织相关构建依赖的前提。对 MongoDB 这类在 Bazel 体系中深度使用 gRPC 的大型项目,这条边界直接关系到构建系统的健康度。
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考