第58章:Protobuf:使用CMake编译.proto文件生成C++代码

说实话,Protobuf 这东西,现在做后端或者网络通信的,基本绕不开。Google 出品,序列化快、体积小、跨语言,确实好用。但有个问题——怎么把 .proto 文件自动编译成 C++ 代码?

我早年刚接触 Protobuf 时,都是手敲命令行:

protoc --cpp_out=. person.proto

项目小还好说,项目一多,十几个 .proto 文件,每次改完还得记得重新生成。忘了?编译报错,找半天才发现是 proto 没更新。后来我学乖了,直接用 CMake 搞定这件事。

58.1 核心思路:把 protoc 当成自定义命令

说白了,CMake 本身不认识 .proto 文件。我们需要告诉它:

  • 用什么工具(protoc)
  • 输入文件是什么(.proto)
  • 输出文件放哪(.pb.h / .pb.cc)

CMake 里干这活的,就是 add_custom_commandadd_custom_target。我个人习惯用 add_custom_command,因为它能跟目标文件绑定,增量编译时不会重复执行。

核心命令:

add_custom_command(
    OUTPUT ${generated_files}
    COMMAND protoc --cpp_out=${output_dir} -I${proto_dir} ${proto_file}
    DEPENDS ${proto_file}
    COMMENT "Generating C++ code from ${proto_file}..."
)

58.2 实战:一个完整的 CMakeLists.txt

假设我们有这样一个目录结构:

project/
├── CMakeLists.txt
├── proto/
│   └── person.proto
└── src/
    └── main.cpp

person.proto 内容很简单:

syntax = "proto3";
package demo;

message Person {
    string name = 1;
    int32 age = 2;
    string email = 3;
}

好,重点来了。我们写 CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(ProtoDemo)

# 1. 找 protobuf 库
find_package(Protobuf REQUIRED)

# 2. 定义路径
set(PROTO_DIR "${CMAKE_SOURCE_DIR}/proto")
set(PROTO_FILE "${PROTO_DIR}/person.proto")
set(GENERATED_DIR "${CMAKE_BINARY_DIR}/generated")

# 3. 生成的文件名
set(GENERATED_HEADER "${GENERATED_DIR}/person.pb.h")
set(GENERATED_SOURCE "${GENERATED_DIR}/person.pb.cc")

# 4. 自定义命令:编译 proto
add_custom_command(
    OUTPUT ${GENERATED_HEADER} ${GENERATED_SOURCE}
    COMMAND ${CMAKE_COMMAND} -E make_directory ${GENERATED_DIR}
    COMMAND protobuf::protoc --cpp_out=${GENERATED_DIR} -I${PROTO_DIR} ${PROTO_FILE}
    DEPENDS ${PROTO_FILE}
    COMMENT "Generating C++ code from person.proto..."
)

# 5. 添加可执行文件
add_executable(proto_demo
    src/main.cpp
    ${GENERATED_SOURCE}
)

# 6. 包含路径
target_include_directories(proto_demo PRIVATE
    ${GENERATED_DIR}
    ${Protobuf_INCLUDE_DIRS}
)

# 7. 链接库
target_link_libraries(proto_demo PRIVATE protobuf::libprotobuf)

小提示: 我建议把生成的文件放到 CMAKE_BINARY_DIR 下,而不是源码目录。这样 make clean 时自动清理,不会污染源码树。

58.3 避坑指南:我踩过的几个坑

坑一:protoc 找不到

我曾经在 Ubuntu 上直接 apt install protobuf-compiler,结果 CMake 里 find_package(Protobuf) 死活找不到。后来发现是版本太老。建议用 find_package(Protobuf CONFIG) 或者指定路径:

set(Protobuf_DIR "/usr/local/lib/cmake/protobuf")
find_package(Protobuf REQUIRED)

坑二:生成的文件没被编译

如果你把 .pb.cc 加到 add_executable 里,但 CMake 没检测到依赖,编译时会报“找不到头文件”。解决办法:确保 add_custom_commandOUTPUTadd_executable 里的源文件路径完全一致。

坑三:多 proto 文件互相依赖

项目大了,proto 文件之间会有 import。这时候 -I 参数要包含所有 proto 目录。我习惯把所有 proto 放一个目录,省心。

58.4 进阶:用函数封装,复用代码

如果你有多个 .proto 文件,重复写 add_custom_command 很烦。我写了个函数:

function(protobuf_generate_cpp SRCS HDRS PROTO_DIR PROTO_FILES)
    set(GENERATED_DIR "${CMAKE_BINARY_DIR}/generated")
    set(OUTPUT_FILES "")

    foreach(PROTO_FILE ${PROTO_FILES})
        get_filename_component(FILENAME ${PROTO_FILE} NAME_WE)
        list(APPEND OUTPUT_FILES
            "${GENERATED_DIR}/${FILENAME}.pb.cc"
            "${GENERATED_DIR}/${FILENAME}.pb.h"
        )
    endforeach()

    add_custom_command(
        OUTPUT ${OUTPUT_FILES}
        COMMAND ${CMAKE_COMMAND} -E make_directory ${GENERATED_DIR}
        COMMAND protobuf::protoc --cpp_out=${GENERATED_DIR} -I${PROTO_DIR} ${PROTO_FILES}
        DEPENDS ${PROTO_FILES}
        COMMENT "Generating C++ code from proto files..."
    )

    set(${SRCS} ${OUTPUT_FILES} PARENT_SCOPE)
    set(${HDRS} ${OUTPUT_FILES} PARENT_SCOPE)
endfunction()

# 使用
file(GLOB PROTO_FILES "proto/*.proto")
protobuf_generate_cpp(GEN_SRCS GEN_HDRS "proto" "${PROTO_FILES}")

注意: file(GLOB) 在 CMake 重新配置时才会更新文件列表。如果你新增了 .proto 文件,记得重新运行 cmake。我一般用 file(GLOB_RECURSE) 配合 CONFIGURE_DEPENDS 来缓解这个问题。

58.5 知识体系:Protobuf + CMake 核心流程

下面这张图,是我梳理的整个流程。你看一眼就明白了:

Protobuf + CMake 编译流程 .proto 文件 protoc 编译器 person.pb.h person.pb.cc CMakeLists.txt C++ 编译器 (g++/clang) main.cpp 可执行文件

流程其实很清晰:CMake 调用 protoc 生成 .pb.h/.pb.cc,然后跟你的业务代码一起编译链接。整个过程自动化,你只需要 cmake .. && make 就行。

58.6 总结

嗯,这一章的内容就这些。说白了,用 CMake 编译 proto 文件,核心就是 add_custom_command + protoc。记住三点:

  • 生成文件放到 build 目录,别污染源码
  • find_package(Protobuf) 找库和工具
  • 多 proto 文件时,用函数封装,别重复造轮子

我在实际项目中,还会把 proto 生成的文件单独打包成一个静态库,这样业务代码链接起来更清爽。不过那是后话了,先把基础跑通再说。


公众号:蓝海资料掘金营,微信deep3321