zoukankan      html  css  js  c++  java
  • 在 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。

  • 相关阅读:
    UNIX环境高级编程——信号(API)
    UNIX环境高级编程——信号之kill、raise、killpg、alarm、pause、abort、sleep、usleep、nanosleep和setitimer函数
    UNIX环境高级编程——信号基本概述和signal函数
    UNIX环境高级编程——进程关系
    UNIX环境高级编程——system函数
    [Training Video
    [Selenium]重写拖拽dragWidgetToElementContainner()
    [Selenium]Grid模式下运行时打印出当前Case在哪台node机器上运行
    [Java]通过java获取计算机名
    [Selenium]点击下拉框之后,从下拉列表选择元素进行点击很容易失败
  • 原文地址:https://www.cnblogs.com/zzk0/p/15583430.html
Copyright © 2011-2022 走看看