在 CMake 項目中使用 protobuf


簡介

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.

實施

第三種方法分為四個步驟。

  1. 源碼編譯 protobuf 的依賴:zlib
  2. 源碼編譯 protobuf,前兩步使用 ExternalProject_Add 指令來編譯
  3. 使用編譯生成的 protoc 來編譯 .proto 文件,oneflow 里面寫了一個函數來編譯所有 .proto 文件,函數里面通過 add_custom_command 來調用 protoc 進行編譯
  4. 將所有 .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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM