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.protosrc/bar/baz.proto,并生成四个输出文件: build/gen/foo.pb.hbuild/gen/foo.pb.ccbuild/gen/bar/baz.pb.hbuild/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"
}

当珍惜每一片时光~