本文主要提供一种在 C\C++ 项目中嵌入 Protobuf/Protobuf-C proto 源文件进行编译的应用方法。

Protobuf 的 proto 源文件在 C\C++ 项目构建需要首先通过 protoc 工具生成对应的 C++ 源码文件,然后参与编译过程。将这个工具生成过程也集成到 CMake 构建中会方便 proto 定义变更的及时跟进,也可以将 proto 文件同时纳入版本管理中。

在 CMake 中可以使用 execute_process 来手动调用 protoc 来进行源码生成,但是这样在每次 cmake 命令时都重新生成源码文件,导致依赖的其他构建目标也重新编译,在 proto 定义未修改的情况下无端增加了编译时间。本文提供的方法可以避免这个问题。

主要思路如下:

  1. proto 文件构建作为 cmake 工程一个的 subdirectory。允许多个 proto 文件参与构建,同时将生成的源码编译为库作为其他目标的链接对象,而不是以源码形式参与其他目标的编译。
  2. 利用 cmake 的 add_custom_commandadd_custom_target 进行 proto 文件生成。将生成源码文件与 proto 源文件增加依赖关系,只有当 proto 文件更改时才需要重新生成。

在项目中,将项目使用的 proto 文件放到一个子目录比如 proto 中,在目录中同时增加一个 CMakeLists.txt 作为项目 subdirectory。项目主 CMakeLists.txtadd_subdirectory 这个 proto 目录,然后 include_directories 对应的头文件目录变量。

以下为 proto 中的 CMakeLists.txt示例,同时包括了 protobuf 和 protobuf-c 两个示例:

 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
# 获取编译器
find_program(
    PROTOC_C
    protoc-c
    DOC "Protobuf-c Compiler (protoc-c)"
    REQUIRED
)
find_program(
    PROTOC_CXX
    protoc
    DOC "Protobuf Compiler (protoc)"
    REQUIRED
)

# 需要编译的 proto 文件
file (GLOB PROTO_SOURCE_FILES
    "${CMAKE_CURRENT_SOURCE_DIR}/*.proto"
)

set(PROTO_PATH    "${CMAKE_CURRENT_SOURCE_DIR}")
set(PROTO_C_OUT   "${CMAKE_CURRENT_BINARY_DIR}/gen_c")
set(PROTO_CXX_OUT "${CMAKE_CURRENT_BINARY_DIR}/gen_cxx")

file(MAKE_DIRECTORY ${PROTO_C_OUT})
file(MAKE_DIRECTORY ${PROTO_CXX_OUT})

# 使用 protoc 处理 proto 文件
foreach(input_proto ${PROTO_SOURCE_FILES})
    get_filename_component(DIR ${input_proto} DIRECTORY)
    get_filename_component(FILE_NAME ${input_proto} NAME_WE)

    set(OUTPUT_C_HEADER     "${PROTO_C_OUT}/${FILE_NAME}.pb-c.h")
    set(OUTPUT_C_SOURCE     "${PROTO_C_OUT}/${FILE_NAME}.pb-c.c")
    list(APPEND OUTPUT_SOURCES_C
        ${OUTPUT_C_HEADER} ${OUTPUT_C_SOURCE})

    set(OUTPUT_CXX_HEADER   "${PROTO_CXX_OUT}/${FILE_NAME}.pb.h")
    set(OUTPUT_CXX_SOURCE   "${PROTO_CXX_OUT}/${FILE_NAME}.pb.cc")
    list(APPEND OUTPUT_SOURCES_CXX
        ${OUTPUT_CXX_HEADER} ${OUTPUT_CXX_SOURCE})
endforeach()

add_custom_command(
    OUTPUT  ${OUTPUT_SOURCES_C}
    COMMAND ${PROTOC_C} --c_out=${PROTO_C_OUT} --proto_path=${PROTO_PATH} ${PROTO_SOURCE_FILES}
    DEPENDS ${PROTO_SOURCE_FILES}
    WORKING_DIRECTORY ${PROTO_PATH}
    COMMENT "Generate C Protobuf Source Files"
)1

add_custom_command(
    OUTPUT  ${OUTPUT_SOURCES_CXX}
    COMMAND ${PROTOC_CXX} --cpp_out=${PROTO_CXX_OUT} --proto_path=${PROTO_PATH} ${PROTO_SOURCE_FILES}
    DEPENDS ${PROTO_SOURCE_FILES}
    WORKING_DIRECTORY ${PROTO_PATH}
    COMMENT "Generate Cpp Protobuf Source Files"
)

add_custom_target(
    compile_c_protos
    DEPENDS ${OUTPUT_SOURCES_C}
)
add_custom_target(
    compile_cxx_protos
    DEPENDS ${OUTPUT_SOURCES_CXX}
)

# 设置生成源文件包含目录变量供上层引用
set(PROTO_GEN_C_INCLUDE_DIRS ${PROTO_C_OUT} PARENT_SCOPE)
set(PROTO_GEN_CXX_INCLUDE_DIRS ${PROTO_CXX_OUT} PARENT_SCOPE)

# 将生成的文件打包为库 proto_gen_c
# 程序可以链接到该库
add_library(proto_gen_c	${OUTPUT_SOURCES_C})
target_link_libraries(proto_gen_c protobuf-c)
add_dependencies(proto_gen_c compile_c_protos)

add_library(proto_gen_cxx ${OUTPUT_SOURCES_CXX})
target_link_libraries(proto_gen_cxx protobuf)
add_dependencies(proto_gen_cxx compile_cxx_protos)

在项目主 CMakeLists.txt 中,如下引用:

1
2
3
add_subdirectory(proto)
include_directories(${PROTO_GEN_C_INCLUDE_DIRS})
include_directories(${PROTO_GEN_CXX_INCLUDE_DIRS})

在需要使用 proto 的目标 CMakeLists.txt 脚本中,只需要目标链接 proto_gen_cxxproto_gen_c 即可。