簡介
protobuf 只需要我們定義 .proto 格式的數據結構,然后使用 protobuf 編譯器生成指定語言的代碼,然后我們就可以在指定的語言中使用這個數據結構了。protobuf 的一大好處就是數據結構的序列化和反序列化,這些自定義的數據結構經過序列化之后就可以通過網絡、本地系統等方式傳給其他進程使用,並且因為 protobuf 有多語言支持,這些數據結構還可以通過序列化和反序列化來支持混合語言編程(比如 C++ 底層和 python 前端)。
為了用上 protobuf 有幾種方式:
- 手動調用 protoc 來編譯文件,然后引入自己的項目。
- 使用 CMake 提供的 find_package 腳本找到 protobuf,得到一些變量。
- 使用 CMake 下載指定版本 protobuf,源碼編譯 protobuf,然后用編譯生成的 protoc 來編譯。
第一種方法,不夠自動,手動的要素太多;第二種方法,使用系統安裝的 protoc,會存在版本差異,另外 ubuntu 上 apt 安裝的是 3.0.0,之前還遇到過編譯成 Java 后出現 “局部變量” 和 message 的屬性沖突的 BUG,更新了版本之后就沒有問題了。因此,本文將會介紹如何使用第三種方法在 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。