简介
protobuf 只需要我们定义 .proto 格式的数据结构,然后使用 protobuf 编译器生成指定语言的代码,然后我们就可以在指定的语言中使用这个数据结构了。protobuf 的一大好处就是数据结构的序列化和反序列化,这些自定义的数据结构经过序列化之后就可以通过网络、本地系统等方式传给其他进程使用,并且因为 protobuf 有多语言支持,这些数据结构还可以通过序列化和反序列化来支持混合语言编程(比如 C++ 底层和 python 前端)。
为了用上 protobuf 有几种方式:
- 手动调用 protoc 来编译文件,然后引入自己的项目。
- 使用 CMake 提供的 find_package 脚本找到 protobuf,得到一些变量。
- 使用 CMake 下载指定版本 protobuf,源码编译 protobuf,然后用编译生成的 protoc 来编译。
第一种方法,不够自动,手动的要素太多;第二种方法,使用系统安装的 protoc,会存在版本差异,另外 ubuntu 上 pip 安装的是 3.0.0,之前还遇到过编译成 Java 后出现 “局部变量” 和 message 的属性冲突的情况。因此,本文将会介绍如何使用第三种方法在 CMake 中引入 protobuf。本文使用的代码主要是从 oneflow 复制粘贴过来的hhh.
实施
第三种方法分为四个步骤。
- 源码编译 protobuf 的依赖:zlib
- 源码编译 protobuf,前两步使用 ExternalProject_Add 指令来编译
- 使用编译生成的 protoc 来编译 .proto 文件,oneflow 里面写了一个函数来编译所有 .proto 文件,函数里面通过 add_custom_command 来调用 protoc 进行编译
- 将所有 .proto 生成的文件编译成一个静态链接库,再将编译可执行文件,将静态链接库链接进去
代码地址:https://github.com/zzk0/cmake_cpp_cuda/tree/master/cpp/protobuf
代码结构如下所示。我是在一个大的 CMake 项目中,通过 add_sub_directory 来加入这个子项目。如果要单独用这个子项目,需要加上 cmake 最低版本的指令。其中 third_party 下面是使用了第三方的依赖,通过 ExternalProject_Add 指令来下载、校验、解压、编译。proto2cpp.cmake 里面是一个函数,将 .proto 编译成 .cpp 文件,这个函数会通过 set 指令设置 PARENT_SCOPE 中的变量,从而导出相关的依赖。
编译链接可执行文件
我们主要看看最外面的 CMakeLists.txt,其他三个文件就需要你具体去看代码了,其实就是调用 ExternalProject_Add 和函数。
我们将项目的 .proto 文件编译成 .cpp 之后,再编译一次成静态链接库。需要特别注意的是需要链接 Threads,如果不链接会导致 core_dump。
project(protobuf-cpp)
set(THIRD_PARTY_DIR "${PROJECT_BINARY_DIR}/third_party_install"
CACHE PATH "Where to install third party headers and libs")
# include 指令里面的 set 操作的变量作用域就是在这个文件,
# 可以类比 c++ 的 include 相当于把那里面的东西 include 进来
set(cmake_dir ${PROJECT_SOURCE_DIR}/cmake)
list(APPEND CMAKE_MODULE_PATH ${cmake_dir})
list(APPEND CMAKE_MODULE_PATH ${cmake_dir}/third_party)
# 最好设置代理, 需要从 github 下载源代码
include(zlib)
include(protobuf)
include(proto2cpp)
# protobuf 需要 link threads, 否则会报错
find_package(Threads)
file(GLOB PROTO_FILES ${PROJECT_SOURCE_DIR}/*.proto)
foreach(proto_name ${PROTO_FILES})
file(RELATIVE_PATH proto_rel_name ${PROJECT_SOURCE_DIR} ${proto_name})
list(APPEND REL_PROTO_FILES ${proto_rel_name})
endforeach()
PROTOBUF_GENERATE_CPP(PROTO_SRCS PROTO_HDRS ${PROJECT_SOURCE_DIR} ${REL_PROTO_FILES})
add_library(proto_lib STATIC ${PROTO_SRCS} ${PROTO_HDRS})
# 这里设置为 PUBLIC 是因为在链接生成 exe 的时候, 需要这些 include
# include 的本质就是将那些东西复制进来, 所以 main.cpp 上面就会 include PROTOBUF_INCLUDE_DIR
# 因此需要设置为 PUBLIC 才行
target_include_directories(proto_lib PUBLIC ${PROTOBUF_INCLUDE_DIR})
target_link_libraries(proto_lib PRIVATE ${PROTOBUF_STATIC_LIBRARIES} Threads::Threads)
add_executable(${PROJECT_NAME} main.cpp)
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(${PROJECT_NAME} PRIVATE proto_lib)
protobuf 简介
protobuf 的一大特点就是通过 “代码生成” 数据结构类的方式来序列化、反序列化二进制数据。这些数据结构类可以实例化,里面还提供了一些方法用于获取数据、设置数据等。
例子
以 Google 官方的教程为例子。这个文件定义了 AddressBook,一个 AddressBook 是由多个 Person 组成的,每个 Person 有若干种属性:名字、号码、邮箱、多个手机号。下面的例子基本展示了 protobuf 数据定义的语法,和 C++ Java 是相似的。
syntax = "proto2";
package tutorial;
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
代码生成规则
地址:https://developers.google.com/protocol-buffers/docs/reference/cpp-generated
操作 protobuf 对象的时候,看返回值和方法前面大概就知道是干嘛的了。比如有的会返回指针,那么你可以修改它,比如 mutable 开头的方法,或者 repeated 属性才有的 add 开头的方法;有的方法是 const 方法,这意味着你只能读取数据。
protobuf 序列化和反序列化都是二进制数据,所以即使是 ParseFromString 方法,也是要二进制 string 才行,不可以使用 DebugString(),或者你可以看懂的 string。