CMake项目使用protobuf
protobuf
Protocol Buffers(通常简称为 Protobuf)是由 Google 开发的一种轻量级、高效的序列化结构数据的方法。它主要用于数据的存储和网络传输,提供了一种跨语言、跨平台的方式来定义和处理数据结构。以下是 Protobuf的一些关键特性和概念:
- 1.数据定义语言
Protobuf使用一种简单的语言来定义数据结构,称为 .proto 文件。你可以在这些文件中定义消息(message),字段(field)及其类型。 - 2.高效的序列化
Protobuf将数据序列化为二进制格式,这样可以显著减少数据的大小和提高传输速度。相比于其他格式(如 JSON或XML),Protobuf 的二进制格式通常更小、更快。 - 3.跨语言支持
Protobuf 支持多种编程语言,包括但不限于C++、Java、Python、Go、C#、JavaScript、Ruby等,这使得使用Protobuf的系统可以轻松地在不同语言之间进行数据交换。 - 4.向后和向前兼容
Protobuf 设计的一个重要特点是其向后和向前兼容性。这意味着你可以在不破坏现有代码的情况下对数据结构进行修改。例如,可以添加新字段而不影响旧版本的应用程序。 - 5.性能
由于 Protobuf 使用紧凑的二进制格式,序列化和反序列化的速度非常快,适合高性能的应用场景。 - 6.生成代码
使用protoc编译器,可以从 .proto 文件生成目标编程语言的代码。这个代码包括序列化和反序列化的功能,使得你可以轻松地在应用程序中使用定义好的数据结构。 - 7.使用场景丰富
Protobuf 适用于许多场景,包括但不限于:网络通信:在微服务架构中,服务之间可以通过 Protobuf 进行高效的数据交换;数据存储:可以将数据以 Protobuf 格式存储在文件或数据库中;配置文件:可以用 Protobuf 定义复杂的配置结构。
更多相关介绍可以查看Protobuf的参考文档。
protoc
protoc是Protocol Buffers(Protobuf)的一部分,是一个用于编译.proto文件的命令行工具。它将定义的数据结构转换为特定编程语言的源代码,这些代码包含序列化和反序列化的功能,使得开发者能够在应用程序中轻松使用Protobuf定义的数据。
当使用--cpp_out=
命令行参数调用protoc时,编译器为每个指定的.proto
文件输入创生成对应的C++版本的头文件和实现文件。输出文件的名称是通过指定的参数和proto文件的路径计算得到,遵循一下的生成规则:
- 头文件或实现文件的扩展名(.proto)分别替换为.pb.h或.pb.cc。
- proto文件的参考路径 (用--proto_path=或-I命令行参数指定) 将被替换为输出路径 (用--cpp_out=命令行参数指定)。
如下示例:
protoc --proto_path=src --cpp_out=build/gen src/foo.proto src/bar/baz.proto
编译器将读取文件src/foo.proto
和src/bar/baz.proto
,并生成四个输出文件: build/gen/foo.pb.h
、build/gen/foo.pb.cc
、build/gen/bar/baz.pb.h
和build/gen/bar/baz.pb.cc
。其中,cpp_out
指定的输出目录必须已经存在。
CMake生成cpp源码
Protobuf提供了很好的CMake支持,并且cmake自身也可以使用很多自定义的功能或命令,这使得Protobuf可以很好地和CMake项目进行集成。本文提供两种在CMake项目中使用Protobuf。第一,使用官方提供的示例代码;第二,通过自定义命令和目标的方式实现自动生成protobuf的cpp源文件。
官方示例
根据官方示例,按照以下的目录组织示例项目工程。source中存放源代码,包含main.cpp和/proto/person.proto文件以及CMakeLists.txt文件,这里也展示了部分build目录,为了能够显示由proto文件生成的cpp源代码文件路径。
.
├── CMakeLists.txt
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ ├── build.ninja
│ ├── cmake_install.cmake
│ ├── compile_commands.json
│ ├── proto_demo
│ └── source
│ └── proto
│ ├── person.pb.cc
│ └── person.pb.h
└── source
├── main.cpp
└── proto
└── person.proto
以下为每个源文件的内容:
main.cpp
:
#include <iostream>
#include "source/proto/person.pb.h"
int main() {
Person person;
person.set_name("Alice");
person.set_age(25);
person.set_email("example@gmail.com");
std::cout << person.DebugString() << std::endl;
return 0;
}
proto/person.proto
:
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
string email = 3;
}
CMakeLists.txt
:
cmake_minimum_required(VERSION 3.20)
project(protobuf_example LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(Protobuf REQUIRED)
if(Protobuf_FOUND)
message(STATUS "protobuf found")
else()
message(FATAL_ERROR "protobuf not found")
endif()
set(SRC_FILES source/main.cpp)
set(PROTO_FILES source/proto/person.proto )
if(protobuf_MODULE_COMPATIBLE)
message(STATUS "protobuf_MODULE_COMPATIBLE is TRUE")
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${PROTO_FILES})
list(APPEND SRC_FILES ${PROTO_SRCS} ${PROTO_HDRS})
else()
message(STATUS "protobuf_MODULE_COMPATIBLE is FALSE")
endif()
add_executable(proto_demo ${SRC_FILES} ${PROTO_FILES})
target_include_directories(proto_demo PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
if(protobuf_MODULE_COMPATIBLE)
target_include_directories(proto_demo PRIVATE ${PROTOBUF_INCLUDE_DIRS})
target_link_libraries(proto_demo PRIVATE ${PROTOBUF_LIBRARIES})
else()
target_include_directories(proto_demo PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(proto_demo PRIVATE protobuf::libprotobuf protobuf::libprotoc protobuf::libprotobuf-lite)
protobuf_generate(TARGET proto_demo)
endif()
使用CMake编译上述工程,会自动生成proto对应的CPP文件,并且编译所有的源代码生成可执行文件。以下为可执行程序的输出结果:
name: "Alice"
age: 25
email: "example@gmail.com"
根据个人经验,官方提供的protobuf_generate_cpp
接口使用非常奇怪,经常会遇到路径解析错误的问题,导致最终无法正常生成源代码文件(直接调用该模块的接口)。使用官方示例中的写法有不够清晰简洁,并且不好实现灵活的控制。由此,可以根据项目需要自己使用cmake的相关命令实现对protoc编译过程的调用执行。
自定义command
示例项目的工程组织如下所示:
.
├── CMakeLists.txt
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ ├── build.ninja
│ ├── cmake_install.cmake
│ ├── compile_commands.json
│ ├── gen
│ │ ├── address.pb.cc
│ │ ├── address.pb.h
│ │ ├── person.pb.cc
│ │ └── person.pb.h
│ └── protobuf_demo
└── source
├── main.cpp
└── proto
├── address.proto
└── person.proto
源代码文件与上一个示例项目类似。同样地,这里也展示了部分build目录,为了能够显示由proto文件生成的cpp源代码文件路径。
以下为每个源文件的内容:
main.cpp
:
#include <iostream>
#include "person.pb.h"
int main() {
Person person;
person.set_name("Alice");
person.set_age(25);
person.set_email("example@gmail.com");
Address address;
address.set_street("123 Main St");
address.set_city("Anytown");
person.mutable_address()->CopyFrom(address);
std::cout << person.DebugString() << std::endl;
return 0;
}
proto/address.proto
:
syntax = "proto3";
message Address {
string street = 1;
string city = 2;
}
proto/person.proto
:
syntax = "proto3";
import "address.proto";
message Person {
string name = 1;
int32 age = 2;
string email = 3;
Address address = 4;
}
CMakeLists.txt
:
cmake_minimum_required(VERSION 3.20)
project(protobuf_example LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(Protobuf REQUIRED)
if(Protobuf_FOUND)
message(STATUS "protobuf found")
else()
message(FATAL_ERROR "protobuf not found")
endif()
# 设置protoc的参数
set(PROTO_DIR ${CMAKE_CURRENT_SOURCE_DIR}/source/proto)
set(PROTO_FILES "address.proto" "person.proto")
set(PROTO_GEN_DIR ${CMAKE_CURRENT_BINARY_DIR}/gen)
set(PROTO_GEN_FILES "")
# 创建生成目录
if(NOT EXISTS ${PROTO_GEN_DIR})
file(MAKE_DIRECTORY ${PROTO_GEN_DIR})
endif()
# 遍历proto文件列表,生成对应的.pb.cc和.pb.h文件
foreach(file ${PROTO_FILES})
get_filename_component(proto_name ${file} NAME_WE)
message("file name: ${file}, proto name: ${proto_name}")
add_custom_command(
OUTPUT ${PROTO_GEN_DIR}/${proto_name}.pb.cc ${PROTO_GEN_DIR}/${proto_name}.pb.h
COMMAND ${Protobuf_PROTOC_EXECUTABLE}
ARGS --proto_path=${PROTO_DIR} --cpp_out=${PROTO_GEN_DIR} ${PROTO_DIR}/${file}
DEPENDS ${PROTO_DIR}/${file}
COMMENT "Generating C++ code from ${file}"
)
list(APPEND PROTO_GEN_FILES ${PROTO_GEN_DIR}/${proto_name}.pb.cc)
list(APPEND PROTO_GEN_FILES ${PROTO_GEN_DIR}/${proto_name}.pb.h)
endforeach()
# 添加生成目录到include路径
add_executable(protobuf_demo source/main.cpp ${PROTO_GEN_FILES})
target_include_directories(protobuf_demo PRIVATE ${PROTO_GEN_DIR})
target_link_libraries(protobuf_demo PRIVATE protobuf::libprotobuf protobuf::libprotoc protobuf::libprotobuf-lite)
# 添加生成目标
add_custom_target(protobuf_gen DEPENDS ${PROTO_GEN_FILES})
add_dependencies(protobuf_demo protobuf_gen)
其中,CMakeLists.txt
文件中,主要是为每一个proto文件定义了一个生成命令用于生成自己的cpp源代码。实现思路是,设置好proto_path和cpp_out两个命令行参数的路径,然后定义一个对protoc编译器的自定义命令,生成源代码并进行收集。通过示例代码的工程组织目录可以看到,最终的源代码文件存放在build/gen
目录中。上述方法可以对生成过程和生成结果有更灵活的控制,也能够根据项目需求拆分不同子模块的编译和生成规则。
当然,可以根据上述思路将处理流程封装成一个cmake的函数存放在项目中,以便重复使用。
最后,上述示例工程的可执行程序输出结果如下:
name: "Alice"
age: 25
email: "example@gmail.com"
address {
street: "123 Main St"
city: "Anytown"
}
Comments | NOTHING