第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_command 和 add_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_command 的 OUTPUT 和 add_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 核心流程
下面这张图,是我梳理的整个流程。你看一眼就明白了:
流程其实很清晰:CMake 调用 protoc 生成 .pb.h/.pb.cc,然后跟你的业务代码一起编译链接。整个过程自动化,你只需要 cmake .. && make 就行。
58.6 总结
嗯,这一章的内容就这些。说白了,用 CMake 编译 proto 文件,核心就是 add_custom_command + protoc。记住三点:
- 生成文件放到 build 目录,别污染源码
- 用
find_package(Protobuf)找库和工具 - 多 proto 文件时,用函数封装,别重复造轮子
我在实际项目中,还会把 proto 生成的文件单独打包成一个静态库,这样业务代码链接起来更清爽。不过那是后话了,先把基础跑通再说。
公众号:蓝海资料掘金营,微信deep3321