CMake编译protobuf生成c++代码

本文为个人使用cmake编译protobuf生成源码的经验总结,下面将介绍三种生成protobuf源码的cmake编写方式

1. protobuf_generate_cpp生成源码

cmake提供了FindProtobuf模块,可以通过find_package命令查找Protobuf进行使用,官网给的使用示例如下:

find_package(Protobuf REQUIRED)
include_directories(${Protobuf_INCLUDE_DIRS})
include_directories(${CMAKE_CURRENT_BINARY_DIR})
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS foo.proto)
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS EXPORT_MACRO DLL_EXPORT foo.proto)
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS DESCRIPTORS PROTO_DESCS foo.proto)
protobuf_generate_python(PROTO_PY foo.proto)
add_executable(bar bar.cc ${PROTO_SRCS} ${PROTO_HDRS})
target_link_libraries(bar ${Protobuf_LIBRARIES})

这里使用protobuf_generate_cpp命令将foo.proto文件生成源码,使用PROTO_SRC,PROTO_HARS变量分别指代生成的cpp和h文件并可用于连接到target和设置include
不过这种方法有两个缺点:

  • 要求protobuf_generate_cpp命令和生成add_executable()add_library() 的命令必须在同一个CMakeList中.
  • 该方法(当前3.18)仍无法设置源码的生成路径,只能默认在相应的build-tree中生成

2.使用execute_process命令生成源码

为解决方法一中的缺点,可以使用cmake中的execute_process命令调用protoc程序来自定义生成源码的方法,示例如下:

find_package(Protobuf 3 REQUIRED)

#设置输出路径
(MESSAGE_DIR ${CMAKE_BINARY_DIR}/message)
if(EXISTS "${CMAKE_BINARY_DIR}/message" AND IS_DIRECTORY "${CMAKE_BINARY_DIR}/message")
        SET(DST_DIR ${MESSAGE_DIR})
else()
        file(MAKE_DIRECTORY ${MESSAGE_DIR})
        SET(DST_DIR ${MESSAGE_DIR})
endif()

#设置protoc的搜索路径
LIST(APPEND PROTO_FLAGS -I${CMAKE_SOURCE_DIR}/msg/message)

#获取需要编译的proto文件
file(GLOB_RECURSE MSG_PROTOS ${CMAKE_SOURCE_DIR}/msg/message/*.proto)
set(MESSAGE_SRC "")
set(MESSAGE_HDRS "")
foreach(msg ${MSG_PROTOS})
        get_filename_component(FIL_WE ${msg} NAME_WE)

        list(APPEND MESSAGE_SRC "${PROJECT_BINARY_DIR}/message/${FIL_WE}.pb.cc")
        list(APPEND MESSAGE_HDRS "${PROJECT_BINARY_DIR}/message/${FIL_WE}.pb.h")
        
        # 生成源码
        execute_process(
            COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} ${PROTO_FLAGS} --cpp_out=${DST_DIR} ${msg}
            )
endforeach()
set_source_files_properties(${MESSAGE_SRC} ${MESSAGE_HDRS} PROPERTIES GENERATED TRUE)

在示例中PROTOBUF_PROTOC_EXECUTABLE指代protobuf生成源码的可执行程序protoc,其使用格式为

protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto

其中

  • -I=$SRC_DIR 为编译时搜索proto的根目录
  • –cpp_out=$DST_DIR为源码输出路径
  • $SRC_DIR/addressbook.proto 为需要编译的proto文件

在示例代码中

  • 首先设置了源码输出路径DST_DIR并且当DST_DIR不存在时生成该目录;
  • 之后通过*LIST(APPEND PROTO_FLAGS -I${CMAKE_SOURCE_DIR}/msg/message)*对protoc的搜索路径进行设置;
  • 最后execute_process命令遍历中的每个proto文件执行protoc命令,并将生成的源码分别追加到变量MESSAGE_SRCMESSAGE_HDRS中;
  • MESSAGE_SRCMESSAGE_HDRS可以用于连接target和设置include_directories

这种方法仍然存在缺点:每次执行cmake后,都会重新生成proto源码,导致make时会因为源码变动(内容未变,只是重新生成)而重新编译程序

3.使用add_custom_target与add_custom_command生成源码

为解决方法二中重新编译的问题,在该方法中引入add_custom_target命令生成一个自定义target,并令该target依赖于生成源码的add_custom_command命令

find_package(Protobuf 3 REQUIRED)

#设置输出路径
SET(MESSAGE_DIR ${CMAKE_BINARY_DIR}/message)
if(EXISTS "${CMAKE_BINARY_DIR}/message" AND IS_DIRECTORY "${CMAKE_BINARY_DIR}/message")
        SET(PROTO_META_BASE_DIR ${MESSAGE_DIR})
else()
        file(MAKE_DIRECTORY ${MESSAGE_DIR})
        SET(PROTO_META_BASE_DIR ${MESSAGE_DIR})
endif()

#设置protoc的搜索路径
LIST(APPEND PROTO_FLAGS -I${CMAKE_SOURCE_DIR}/msg/message)
#获取需要编译的proto文件
file(GLOB_RECURSE MSG_PROTOS ${CMAKE_SOURCE_DIR}/msg/message/*.proto)
set(MESSAGE_SRC "")
set(MESSAGE_HDRS "")

foreach(msg ${MSG_PROTOS})
        get_filename_component(FIL_WE ${msg} NAME_WE)

        list(APPEND MESSAGE_SRC "${PROJECT_BINARY_DIR}/message/${FIL_WE}.pb.cc")
        list(APPEND MESSAGE_HDRS "${PROJECT_BINARY_DIR}/message/${FIL_WE}.pb.h")

		# 使用自定义命令
        add_custom_command(
          OUTPUT "${PROJECT_BINARY_DIR}/message/${FIL_WE}.pb.cc"
                 "${PROJECT_BINARY_DIR}/message/${FIL_WE}.pb.h"
          COMMAND  ${PROTOBUF_PROTOC_EXECUTABLE}
          ARGS --cpp_out  ${PROTO_META_BASE_DIR}
            -I ${CMAKE_SOURCE_DIR}/msg/message
            ${msg}
          DEPENDS ${msg}
          COMMENT "Running C++ protocol buffer compiler on ${msg}"
          VERBATIM
        )
endforeach()

# 设置文件属性为 GENERATED
set_source_files_properties(${MESSAGE_SRC} ${MESSAGE_HDRS} PROPERTIES GENERATED TRUE)

# 添加自定义target
add_custom_target(generate_message ALL
                DEPENDS ${MESSAGE_SRC} ${MESSAGE_HDRS}
                COMMENT "generate message target"
                VERBATIM
                )

可以看到本方法的前一部分与方法二相似,不同点只在于使用了add_custom_command替换了execute_process命令,将add_custom_command的OUTPUT与add_custom_target的DEPENDS绑定,即可依据绑定的target是否变动来决定源码生成命令是否执行(绑定方法:add_custom_command的OUTPUT为MESSAGE_SRCMESSAGE_HDRS,同时MESSAGE_SRCMESSAGE_HDRSadd_custom_target的DENPENDS)

其中值得主要的有两点:

  • 设置生成的源码文件属性GENERATED为TRUE,否则cmake时会因找不到源码而报错
  • 使用add_custom_target添加目标时要设置ALL关键字,否则target将不在默认编译列表中

这样就能实现proto生成源码配置定制化并避免不必要的重新编译了

4.总结

如果只有少量proto文件且在同一文件夹下,可使用方法一的protobuf_generate_cpp生成源码,若proto文件较多且层次复杂,建议使用方法三.
具体使用可以参考该项目https://github.com/mingjitianming/transmit_asio中CMakeLists的使用方式

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐