概述
不讲具体的protobuf使用,主要介绍怎么引入到工程中并且怎么方便作为工程的一部分使用。因为当时去搜索引擎上查了一下并没有特别满意的答案,所以决定自己写一个。当然别忘了外面的summary:It just work!
,全文充斥着工程妥协的味道。
问题一:版本冲突
在之前有关protobuf的blog里面就介绍了当电脑里面存在两个版本的protobuf的时候会冲突,当时为什么没有用grpc自带的那个最新版本?因为我发现那个版本使用的时候absl会提示一堆找不到的报错,并且就算安装了absl的库后也是同理。
我当时以为是版本冲突的问题,后面单独使用的时候同样编译不了,提示缺少头文件,但是当配合grpc的时候又能够神奇的work了,所以想来想去反正grpc是动态库,直接在cmake里面加两行就行了。It just work!
find_package(Protobuf CONFIG REQUIRED)
# TODO:消除absl的报错要靠grpc,无语了
find_package(gRPC CONFIG REQUIRED)
link_libraries(gRPC::grpc++)
问题二:怎么集成进CMake项目
当然不是指的手动 protoc --proto_path=xxxx --cpp_out=xxxx xxx.proto
,这样不仅麻烦而且每次修改proto都要重新生成,如果我们有多个proto的话岂不是需要一个一个改?无法忍受这样,所以我们要配合CMake的add_custom_command来使用。
这里以grpc操作redis的项目为例:
# check thirdparty packages
set(protobuf_MODULE_COMPATIBLE true)
find_package(Protobuf CONFIG REQUIRED)
message(STATUS "Using Protobuf ${Protobuf_VERSION}")
find_package(gRPC CONFIG REQUIRED)
message(STATUS "Using gRPC ${gRPC_VERSION}")
find_package(hiredis CONFIG REQUIRED)
message(STATUS "Using hiredis ${hiredis_VERSION}")
# include thirdparty includes
include_directories(${hiredis_INCLUDE_DIRS})
include_directories(${PROTOBUF_INCLUDE_DIRS})
# # include thirdparty libs
set(HREDIS_LIB_DEPS
${GRPC_LIBRARIES}
${HIREDIS_LIBRARIES}
${Protobuf_LIBRARIES}
)
首先引入三方库,记得要把protobuf_MODULE_COMPATIBLE
设为true
,因为我们要在后续的命令中使用到protobuf的部分模块。后面就是如何去自动生成我们要的proto了,首先我们必然不想自动生成在我们的项目目录中,因为这些代码不应该作为提交的部分(作为一个项目不应把grpc的版本限定死,毕竟grpc仅仅拿来通信版本其实不重要),选择生成到build文件夹中,只要保证每次build的时候都能生成自然也都能找到。
# generate protobuf files
set(PROTO_PATH "${CMAKE_SOURCE_DIR}/protos")
set(HREDIS_PROTO "${PROTO_PATH}/hredis.proto")
set(GENERATED_PROTOBUF_PATH "${CMAKE_BINARY_DIR}/generated")
file(MAKE_DIRECTORY ${GENERATED_PROTOBUF_PATH})
set(HREDIS_PB_CPP_FILE "${GENERATED_PROTOBUF_PATH}/hredis.pb.cc")
set(HREDIS_PB_H_FILE "${GENERATED_PROTOBUF_PATH}/hredis.pb.h")
set(HREDIS_GRPC_PB_CPP_FILE "${GENERATED_PROTOBUF_PATH}/hredis.grpc.pb.cc")
set(HREDIS_GRPC_PB_H_FILE "${GENERATED_PROTOBUF_PATH}/hredis.grpc.pb.h")
set(HREDIS_GRPC_PB_PY_FILE "${GENERATED_PROTOBUF_PATH}/hredis_pb2.py")
set(_GRPC_CPP_PLUGIN_EXECUTABLE $)
set(_GRPC_PYTHON_PLUGIN_EXECUTABLE $)
add_custom_command(
OUTPUT "${HREDIS_PB_H_FILE}"
"${HREDIS_PB_CPP_FILE}"
"${HREDIS_GRPC_PB_H_FILE}"
"${HREDIS_GRPC_PB_CPP_FILE}"
"${HREDIS_GRPC_PB_PY_FILE}"
COMMAND ${PROTOBUF_PROTOC_EXECUTABLE}
ARGS "--proto_path=${PROTO_PATH}"
"--cpp_out=${GENERATED_PROTOBUF_PATH}"
"${HREDIS_PROTO}"
COMMAND ${PROTOBUF_PROTOC_EXECUTABLE}
ARGS "--proto_path=${PROTO_PATH}"
"--grpc_out=${GENERATED_PROTOBUF_PATH}"
"--plugin=protoc-gen-grpc=${_GRPC_CPP_PLUGIN_EXECUTABLE}"
"${HREDIS_PROTO}"
COMMAND ${PROTOBUF_PROTOC_EXECUTABLE}
ARGS "--proto_path=${PROTO_PATH}"
"--python_out=${GENERATED_PROTOBUF_PATH}"
"--grpc_out=${GENERATED_PROTOBUF_PATH}"
"--plugin=protoc-gen-grpc=${_GRPC_PYTHON_PLUGIN_EXECUTABLE}"
"${HREDIS_PROTO}"
)
set(GENERATED_PROTOBUF_FILES
${HREDIS_PB_H_FILE}
${HREDIS_PB_CPP_FILE}
${HREDIS_GRPC_PB_H_FILE}
${HREDIS_GRPC_PB_CPP_FILE}
${HREDIS_GRPC_PB_PY_FILE}
)
include_directories(${GENERATED_PROTOBUF_PATH})
link_libraries(gRPC::grpc++ ${PROTOBUF_LIBRARY} ${hiredis_LIBRARIES})
add_custom_command命令的基本语法
在CMake中,add_custom_command
命令的基本语法如下:
add_custom_command(
OUTPUT output1 [output2 ...]
COMMAND command1 [ARGS] [args1...]
[COMMAND command2 [ARGS] [args2...] ...]
[MAIN_DEPENDENCY depend]
[DEPENDS [depends...]]
[BYPRODUCTS [files...]]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[VERBATIM]
)
这个命令的主要作用是定义一条自定义的构建规则,这条规则可以在构建过程中执行一系列的命令。下面我们来详细解析这个命令的各个参数。
OUTPUT output1 [output2 ...]
:这个参数用于指定自定义命令的输出文件。这些文件在构建过程中会被生成,如果这些文件不存在,那么CMake就会执行这条自定义命令。COMMAND command1 [ARGS] [args1...]
:这个参数用于指定要执行的命令。你可以提供任何有效的命令,包括系统命令、脚本,或者其他的构建工具。ARGS
关键字后面可以跟随一系列的参数,这些参数会被传递给命令。MAIN_DEPENDENCY depend
:这个参数用于指定自定义命令的主要依赖。如果这个依赖的文件被修改,那么自定义命令就会被执行。DEPENDS [depends...]
:这个参数用于指定自定义命令的其他依赖。如果这些依赖的文件被修改,那么自定义命令也会被执行。BYPRODUCTS [files...]
:这个参数用于指定自定义命令的副产品。如果你指定了一个或多个文件作为副产品,那么这些文件将会被添加到构建系统的清理列表中。WORKING_DIRECTORY dir
:这个参数用于指定自定义命令的工作目录。如果你没有指定这个参数,那么自定义命令将会在当前的源码目录中执行。COMMENT comment
:这个参数用于指定一个注释,这个注释将会在执行自定义命令时被打印出来。VERBATIM
:这个参数用于控制命令参数的处理方式。如果你指定了VERBATIM
,那么命令参数将会被按照字面意义处理,而不会被解析为变量或表达式。
仍然有问题
这样就能在build目录下自动生成所需要的pb文件了。至于为什么又说是妥协,上面的cmake代码在仅生成一个目录下的可执行程序是没有问题的,在下面这种目录中,example和src中都依赖我们需要生成的pb文件,那么这个时候该怎么办。
我们的主CMAKE结构是这样的:
是不是以为在src
中生成就好了,因为代码是按顺序执行的,在src
中生成后才会去examples
里面执行,但现实并不是这样。你会发现ninja会略过src直接前往examples。因为add_custom_command
是懒加载的,直到需要的时候才会正常去执行,这也就导致我们缺少头文件预处理的时候就会报错,但是当你继续运行的时候cmake就会发现真的需要它了,此时又能够build了(多点几次)。
这种烦人的依赖问题该怎么解决呢?
怎么解决的
本质上是依赖问题,因为add_custom_command
是懒加载的,那么需要创建一个依赖去告诉CMake我们什么时候就需要这个PB文件,这时候我们就需要使用add_custom_target
,add_custom_target
用于添加自定义构建目标,这个目标通常不与实际的构建文件关联。它主要用于将一组自定义构建步骤组织成一个逻辑组,并在构建时执行这些步骤。这个命令通常与 add_custom_command
配合使用,以实现更复杂的构建过程。
add_custom_target(Name [ALL] [command1 [args1...]]
[COMMAND command2 [args2...] ...]
[DEPENDS depend depend depend ... ]
[BYPRODUCTS [files...]]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[JOB_POOL job_pool]
[VERBATIM] [USES_TERMINAL]
[COMMAND_EXPAND_LISTS]
[SOURCES src1 [src2...]])
示例:
add_custom_command(
OUTPUT generated_file.txt
COMMAND echo "Generated content" > generated_file.txt
DEPENDS some_input_file.txt
COMMENT "Generating a file"
)
add_custom_target(
my_custom_target
COMMAND echo "Hello from custom target"
DEPENDS generated_file.txt
COMMENT "Executing a custom target"
)
在这个例子中,add_custom_target
创建了一个名为 my_custom_target
的自定义构建目标,它执行一个命令,输出 “Hello from custom target”。这个目标依赖于前面 add_custom_command
中生成的 generated_file.txt
。
add_custom_command
主要用于定义在构建时执行的具体命令,通常与文件的生成或转换有关。add_custom_target
主要用于组织一组自定义构建步骤,并为它们创建一个逻辑目标。这个目标本身不与实际文件相关。
在实际使用中,这两个命令通常结合使用,以创建复杂的构建过程。例如,add_custom_command
生成文件,然后 add_custom_target
创建一个目标,该目标依赖于生成的文件,并可能执行一些其他操作。
问题三:加入到多目录CMake项目
既然有了生成的操作和添加依赖的方法,那么我们在src目录以及examples目录里面添加依赖即可:
# 执行需要的依赖
add_custom_target(
SRC_GEN_PROTO
DEPENDS ${GENERATED_PROTOBUF_FILES}
)
现在就建立了正常的依赖关系了,可喜可贺。
后记
发现有时候管用有时候不管用,非常奇怪,后面查阅很多资料以及试了很多后,发现跟dependencies
有关系,add_custom_target
在有些情况下是等价于add_dependencies
的,但是对于多目录结构下存在的依赖关系,这种add_custom_target
似乎就不能够正确判别了,所以我们应该像链接库一样,手动添加我们的依赖:add_dependencies(TAGETNAME DEPNAME)
。