Protobuf 動態載入 .proto 檔案並操作 Message

mkckr0發表於2021-12-25

Google Protocol Buffer 的常規用法需要使用 protoc.proto 編譯成 .pb.h.pb.cc,這樣做效率非常高,但是耦合性也很高。在某些追求通用性而不追求效能的場景下,需要使用 .proto 直接操作 protobuf 資料。

本例使用的 .proto 檔案來自 https://developers.google.com/protocol-buffers/docs/cpptutorial ,但是把它拆成了兩個 .proto 檔案

// ./proto/person.proto
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;
}
// ./proto/person.proto
syntax = "proto2";
package tutorial;
import "person.proto";

message AddressBook {
  repeated Person people = 1;
}=

示例程式碼

#include <iostream>
#include <google/protobuf/compiler/importer.h>
#include <google/protobuf/dynamic_message.h>
#include <google/protobuf/util/json_util.h>

using namespace google::protobuf;

/*
構造 Importer 必須指定 error_collector 用於處理錯誤資訊
AddError 是純虛擬函式,必須 override
*/
class MyMultiFileErrorCollector : public compiler::MultiFileErrorCollector
{
      virtual void AddError(const std::string& filename, int line, int column, const std::string& message) override
      {
          std::cout << "file: " << filename << ", line: " << line << ", col: " << column<< ", message: " << message << std::endl; 
      }
};

int main()
{
    /*
    構造 DiskSourceTree,並新增虛擬路徑。protobuf 使用 Importor 匯入 .proto 檔案時,會虛擬路徑進行查詢
    在本例中,匯入 addressbook.proto 時會使用 ./proto/addressbook.proto
    */
    compiler::DiskSourceTree disk_source_tree;
    disk_source_tree.MapPath("", "proto");

    MyMultiFileErrorCollector error_collector;

    /*
    匯入 addressbook.proto 時,會自動匯入所有依賴的 .proto 檔案
    在本例中,person.proto 也會被自動匯入
    */
    compiler::Importer importer(&disk_source_tree, &error_collector);
    const FileDescriptor* file_descriptor = importer.Import("addressbook.proto");
    if (!file_descriptor) {
        exit(-1);
    }

    // 把 addressbook.proto 和 person.proto 都列印出來
    std::cout << "====== all .proto files ======" << std::endl;
    std::cout << file_descriptor->DebugString() << std::endl;
    for (int i = 0; i < file_descriptor->dependency_count(); ++i) {
        std::cout << file_descriptor->dependency(i)->DebugString() << std::endl;
    }
    
    // 構造 DynamicMessageFactory 用於動態構造 Message
    DynamicMessageFactory message_factory(importer.pool()->generated_pool());  

    /*
    查詢 Person 的 Descriptor 
    不能使用 file_descriptor 查詢,它只包含 addresssbook.proto ,只能找到 AddressBook,而 DescriptorPool 包含了所有資料
    在使用 DescriptorPool 查詢時需要使用全名,如:tutorial.Person
    在使用 FileDescritor 查詢需要使用頂級名字,如 AddressBook,而不是 tutorial.AddressBook
    */
    const Descriptor* person_descriptor = importer.pool()->FindMessageTypeByName("tutorial.Person");
    
    /*
    使用工廠建立預設 Message,然後構造一個可以用來修改的 Message
    這個 Message 的生命週期由 New 呼叫者管理
    */
    const Message* default_person = message_factory.GetPrototype(person_descriptor);
    Message* person = default_person->New();

    // 使用 Reflection 修改 Message 的資料
    const Reflection* reflection = person->GetReflection();
    reflection->SetString(person, person_descriptor->FindFieldByName("name"), "abc");
    reflection->SetInt32(person, person_descriptor->FindFieldByName("id"), 123456);
    reflection->SetString(person, person_descriptor->FindFieldByName("email"), "abc@163.com");

    // 把動態設定的 Message 的資料以 JSON 格式輸出
    util::JsonPrintOptions json_options;
    json_options.add_whitespace = true;
    json_options.preserve_proto_field_names = true;
    string output;
    util::MessageToJsonString(*person, &output, json_options);
    std::cout << "====== Person data ======" << std::endl;
    std::cout << output;

    // 析構 person
    delete person;
}


輸出

====== all .proto files ======
syntax = "proto2";

import "person.proto";
package tutorial;

message AddressBook {
  repeated .tutorial.Person people = 1;
}


syntax = "proto2";

package tutorial;

message Person {
  message PhoneNumber {
    optional string number = 1;
    optional .tutorial.Person.PhoneType type = 2 [default = HOME];
  }
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;
  repeated .tutorial.Person.PhoneNumber phones = 4;
}


====== Person data ======
{
 "name": "abc",
 "id": 123456,
 "email": "abc@163.com"
}

https://developers.google.com/protocol-buffers/docs/reference/cpp

相關文章