CMake与Protobuf教程&踩坑日记


概述

不讲具体的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了(多点几次)。

suprise

这种烦人的依赖问题该怎么解决呢?

怎么解决的

本质上是依赖问题,因为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)


文章作者: JoyTsing
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 JoyTsing !
评论
  目录