部署protobuf和grpc的C++开发环境时遇到的问题和解决方法

本文最后更新于 2026年8月25日 晚上

在第一次部署protobuf和grpc的开发环境时常常会遇到不少麻烦,特别是对于C++开发者,所以在本篇文章中就列举一下我遇到过的问题,希望能帮助大伙少踩一些坑。

版本问题

在跟着教程第一次尝试使用protoc compiler通过proto文件生成对应的.pb.cc、.pb.h文件时,有可能会遇到protobuf版本大于或小于编译时使用的库期望版本的情况。多数情况下,这是因为系统有两个或以上不同版本的protobuf造成的,一般是系统的包管理器安装了一个,用户自行或通过其他的包管理器又安装了一个且将路径添加到PATH中,可以通过:

  • which protoc #查看系统目前使用的protoc路径

  • whereis protoc #查看可以在PATH中寻找到的所以protoc路径

  • /path/to/protoc --version #查看特定路径下protoc的版本

使用符合库需要的版本去生成对应文件就可以,或者在cmake中通过include_directorieslink_directories指定希望优先查找的头文件和库的路径。

不同版本的grpc编译时也依赖于不同版本的protobuf,当然如果你使用包管理器那就不用考虑这个问题,因为依赖问题会被自动解决。但一旦包管理器无法解决依赖问题,比如遇到依赖冲突:protobuf或grpc依赖某个库x的某个版本,你已安装的x库不是期望的版本,你尝试移除当前版本的x库但发现重要软件或库依赖x;到此大部分人会放弃这条路或者去找符合当前x库的protobuf包版本,小部分人会无视风险忽略依赖卸载x库,运气好的卸载后没有任何异常并成功安装了protobuf,而运气不好的那就各有各的不幸了。

不过就算使用包管理器成功安装了protobuf和grpc,部分包管理器也不会提供用于在cmake中导入包的find-protobuf.cmake模块文件,这就需要手动在target_link_lib时指明需要链接的库,并自行编写protobuf_generate()函数(如果需要的话)。

不过自行编译的话也有疑问:我怎么知道编译这个版本的grpc需要哪个版本的protobuf?这确实是个问题,因为我直到编译错误前都不知道grpc需要特定版本的protobuf,所以后来我每次编译前都参考conan下grpc包的依赖列表,不过一般来说应该只需要考虑其中abseil和protobuf的版本。

版本问题目前我想起来的就这些,以后遇到其他的会再来补充。

与cmake集成

是否觉得每次使用修改proto后,都得手动用protoc去生成对应的文件有点麻烦呢?所以在cmake里可以手动集成protobuf和grpc,当build时可以自动调用protoc生成需要的文件并根据target编译和链接;如果可以用find-protobuf.cmake导入模块,那就可以用内置protobuf_generate函数去完成上面的功能。

我在以下目录结构给出一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
├── CMakeLists.txt
├── CMakeUserPresets.json
├── conanfile.py
└── src
├── helper.cc
├── helper.h
├── proto
│ ├── CMakeLists.txt
│ ├── router.proto
│ └── rou_t.proto
├── route_guide_callback_server.cc
└── route_guide_server.cc

我们会在父目录中通过add_subdirectory()将proto目录作为子模块添加,在proto的cmakelists中,我们定义了一个静态库,将protobuf和grpc生成的文件编译到该库中,之后我们只需要在父cmakelists中将需要使用grpc依赖的文件链接到该库即可,以下是proto中cmakelists.txt的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
cmake_minimum_required(VERSION 3.15.0)
project(protoc_gen_cxx VERSION 0.1.0 LANGUAGES C CXX)

if(protobuf_FOUND) #check if protobuf not found, on some models this argument is not set or set as Protobuf_FOUND ,PROTOBUF_FOUND
message(STATUS "protobuf found")
else()
message(FATAL_ERROR "protobuf not found")
endif()

if(gRPC_FOUND) #check if gRPC not found, on some models this argument is not set or set as grpc_FOUND ,GRPC_FOUND
message(STATUS "gRPC found")
get_property(grpc_cpp_plugin_location TARGET gRPC::grpc_cpp_plugin PROPERTY LOCATION)
else()
message(FATAL_ERROR "gRPC not found")
endif()

set(PROTO_CXX_OUT "${CMAKE_BINARY_DIR}/gen_cxx")
message(STATUS "using PROTO_CXX_OUT: ${PROTO_CXX_OUT}")


if(NOT EXISTS ${PROTO_CXX_OUT})
file(MAKE_DIRECTORY ${PROTO_CXX_OUT})
endif()

file(GLOB_RECURSE PROTO_FILES "${CMAKE_CURRENT_SOURCE_DIR}/*.proto")
message(STATUS "using PROTO_FILES: ${PROTO_FILES}")

set(CUSTOM_PROTOC_EXECUTABLE "") # if you want to use a custom protoc executable, set it here
if(EXISTS ${CUSTOM_PROTOC_EXECUTABLE})
set(Protobuf_PROTOC_EXECUTABLE ${CUSTOM_PROTOC_EXECUTABLE} CACHE STRING "Path to the protoc executable" FORCE)
message(STATUS "set custom protoc location: ${Protobuf_PROTOC_EXECUTABLE}")
endif()
message(STATUS "using protobuf_version: ${protobuf_VERSION}")


set(GRPC_CPP_PLUGIN_EXECUTABLE "") # if you want to use a custom grpc_cpp_plugin executable, set it here
if(EXISTS ${GRPC_CPP_PLUGIN_EXECUTABLE})
set(grpc_cpp_plugin_location ${GRPC_CPP_PLUGIN_EXECUTABLE} CACHE STRING "Path to the grpc_cpp_plugin executable" FORCE)
message(STATUS "set custom grpc_cpp_plugin location: ${grpc_cpp_plugin_location}")
endif()



add_library(${PROJECT_NAME} STATIC)

set(USE_CUSTOM_COMMAND OFF)

if(USE_CUSTOM_COMMAND)
set(PROTO_CXX_GEN "")
message(STATUS "Using custom commands to generate protobuf and grpc files")
foreach(file ${PROTO_FILES})
get_filename_component(proto_name ${file} NAME_WE)
add_custom_command(
OUTPUT ${PROTO_CXX_OUT}/${proto_name}.pb.cc ${PROTO_CXX_OUT}/${proto_name}.pb.h
COMMAND ${Protobuf_PROTOC_EXECUTABLE}
ARGS --proto_path=${CMAKE_CURRENT_SOURCE_DIR} --cpp_out=${PROTO_CXX_OUT} ${file}
DEPENDS ${file}
COMMENT "Generating proto code from ${file}"
VERBATIM
)
list(APPEND PROTO_CXX_GEN ${PROTO_CXX_OUT}/${proto_name}.pb.cc)
list(APPEND PROTO_CXX_GEN ${PROTO_CXX_OUT}/${proto_name}.pb.h)
endforeach()

foreach(file ${PROTO_FILES})
get_filename_component(proto_name ${file} NAME_WE)
add_custom_command(
OUTPUT ${PROTO_CXX_OUT}/${proto_name}.grpc.pb.cc ${PROTO_CXX_OUT}/${proto_name}.grpc.pb.h
COMMAND ${Protobuf_PROTOC_EXECUTABLE}
ARGS --proto_path=${CMAKE_CURRENT_SOURCE_DIR} --grpc_out=${PROTO_CXX_OUT} --plugin=protoc-gen-grpc=${grpc_cpp_plugin_location} ${file}
DEPENDS ${file}
COMMENT "Generating gRPC code from ${file}"
VERBATIM
)
list(APPEND PROTO_CXX_GEN ${PROTO_CXX_OUT}/${proto_name}.grpc.pb.cc)
list(APPEND PROTO_CXX_GEN ${PROTO_CXX_OUT}/${proto_name}.grpc.pb.h)
endforeach()
message(STATUS "Generated files: ${PROTO_CXX_GEN}")
add_custom_target(proto_gen_cxx ALL DEPENDS ${PROTO_CXX_GEN})
target_sources(${PROJECT_NAME} PRIVATE ${PROTO_CXX_GEN})
else()
message(STATUS "Using protobuf_generate to generate protobuf and grpc files")
protobuf_generate(
TARGET ${PROJECT_NAME}
LANGUAGE cpp
PROTOS ${PROTO_FILES}
PROTOC_OUT_DIR "${PROTO_CXX_OUT}"
)

protobuf_generate(
TARGET ${PROJECT_NAME}
LANGUAGE grpc
PROTOS ${PROTO_FILES}
PROTOC_OUT_DIR "${PROTO_CXX_OUT}"
GENERATE_EXTENSIONS .grpc.pb.h .grpc.pb.cc
PLUGIN "protoc-gen-grpc=${grpc_cpp_plugin_location}"
)
endif()


target_include_directories(${PROJECT_NAME}
PUBLIC
$<BUILD_INTERFACE:${PROTO_CXX_OUT}>
$<INSTALL_INTERFACE:include>
)
target_link_libraries(${PROJECT_NAME}
PUBLIC
protobuf::protobuf
grpc::grpc
)

一般来说推荐使用protobuf_generate去生成需要的文件,如果希望custom command上面也提供了示例;需要注意的是protobuf还提供了另一个函数protobuf_generate_cpp,但该函数有很多坑不建议使用,关于protobuf_generate的更多参数使用请参考官方文档
在此仅介绍一个关键变量:Protobuf_PROTOC_EXECUTABLE,该变量控制protobuf_generate默认使用的protoc位置,一般来说该变量在引入protobuf模块时就已经被预先定义,但当使用非系统的包管理器安装的protobuf,其中的protoc经常因为依赖库问题而无法直接使用,因此需要修改,我会在下一节介绍该问题。

个人的最佳实践

最推荐的方式还是不要依赖任何包管理器,因为使用包管理器的目的是方便,但很多麻烦恰巧是包管理器引起的。自行编译一个合适的版本再通过cmake导入模块,也可以将环境部署在docker中方便迁移和再部署。

micromamba+conan快速部署

虽然上面多次建议大家自行编译,但我自己本地的环境却是通过包管理器部署的(但也踩了不少坑,偷懒的代价)……

最开始我使用conan获取protobuf和grpc包,在只链接库和头文件的情况下工作良好;但当我试图让cmake用包提供的proto和grpc_cpp_plugin自动编译proto文件时,麻烦就来了:protoc可以正常工作,但如果使用了grpc_cpp_plugin就会提示无法找到absl、protobuf等动态库。

经过无用一番排查,最后用readelf发现原来是conan在编译protoc时,使用了rpath指定了要链接的动态库的路径,但在编译grpc_cpp_plugin时却没有使用rpath,导致grpc_cpp_plugin在执行时会去LD_LIBRARY_PATH中寻找需要库的。

最开始我把缺失的库也作为依赖通过conan导入,并在protobuf_generate中通过environment参数传入absl_library,protobuf_library等,但protobuf_generate似乎并不支持该参数(也不报错),不过在add custom command中是可用的;之后我也试图通过设置set(ENV{ld_library_path} “……”),但并不生效。

之后,我又想到既然protoc和grpc_cpp_plugin只是负责代码生成,那我通过包管理器装一个同样版本的用来去生成代码,链接库则还是用conan不就好了吗?

于是就看上了micromamba,而且conda-forge正好有我需要的版本而且依赖也是正确的,即libgrpc-1.54.3依赖于protobuf-3.21.12,安装后检查二者的可执行文件也都是通过rpath链接库的。

至于修改cmake的protobuf_generate默认使用的protoc,如果你的cmake版本 >= 4.0,那么protobuf_generate提供protoc_exe参数可以指定使用protoc路径,否则就需要修改Protobuf_PROTOC_EXECUTABLE,而且必须是:

1
set(Protobuf_PROTOC_EXECUTABLE "path" CACHE STRING "Path to protoc executable" FORCE)

否则即使你修改了也会默认使用缓存中的位置,即便你删除缓存重新生成也不会生效,因为模块中的Protobuf_PROTOC_EXECUTABLE总会先于你设置的被加载。

相较之下,grpc_cpp_plugin的位置就不用纠结这么多,直接传入就可以。

不过可能有人会想,为什么不直接用micromamba的包呢?原因有二:

  • micromamba是conda的C++实现,其核心功能围绕对Python包进行管理,虽然提供了很多C++包,但不如conan、vcpkg这种与cmake深度集成且专为C++包管理而编写的应用,而且很多包不提供对应的cmake模块文件,比如libprotobuf就不提供
  • 从micromamba链接一个库,该库通常使用rpath链接micromamba安装的其他库,可能会有这样一种情况:你从micromamba链接了一个库A,又从系统链接了一个库B,且二者都会链接到一个库C,如果它们需要的库C版本恰好兼容,那么一切安好,但如果需要libC的不同版本,比如libC.so.1与libC.so.2,那因为soname不同也可以正常工作,但是如果二者需要不同的libC版本且soname相同,比如都是libC.so或libC.so.1,那就极有可能导致运行时符号缺失或者其他问题;如果你完全使用micromamba提供的工具链则可以避免上述问题

基于以上原因,我建议只使用micromamba的二进制可执行文件,而不要使用micromamba所提供的库,除非你非常了解micromamba的工作细节。

关于conan的一点提示

conan的protobuf或者grpc的某些版本可能无法正确被编译,特别是新版本,到我写文章时,最新版本为5.27.0,其无法在我的设备上正确编译,所以我目前使用的protobuf版本为3.21.12,grpc版本为1.54.3。

在较新的linux内核上(具体哪个版本不太清楚了,我目前的内核版本是6.10),如果安装grpc-1.54.3,其在编译libsystemd-255时会报错,提示无法识别的文件系统Bcachefs,这个需要去到conan2的目录里找到libsystemd的源码(不过在报错时会给出对应的文件路径),在提示报错的文件末尾按着上面的序号把不识别的文件系统添加上再重新编译就好了。


部署protobuf和grpc的C++开发环境时遇到的问题和解决方法
http://example.com/2024/04/19/proto_grpc_env/
作者
Bluearc
发布于
2024年4月19日
许可协议