google protobuf 反射機制學習筆記

持續更新中。。。

何爲反射機制

基本概念

指程序可以訪問、檢測和修改它本身狀態或行爲的一種能力

程序集包含模塊,而模塊包含類型,類型又包含成員。反射則提供了封裝程序集、模塊和類型的對象。您可以使用反射動態地創建類型的實例,將類型綁定到現有對象,或從現有對象中獲取類型。然後,可以調用類型的方法或訪問其字段和屬性。

我(c++程序員)關注的問題

  • 如何在程序運行過程中通過類型名字(一個字符串,合法但是內容在編譯期間未知,比如是在配置文件中獲取的)創建出類型對象.
  • 如果在程序運行過程中通過對象和對象的屬性的名字(一個字符串,合法但是內容在編譯期間未知,比如是通過通訊包獲取的)獲取,修改對應屬性.
  • 如果在程序運行過程中通過對象和對象方法的名字(一個字符串,合法但是內容在編譯期間未知,比如是從用戶輸入獲取的)調用對應的方法.

protobuf 反射使用簡介

舉個例子 :
當你有一個test.proto 比如 :

package T;
message Test
{
    optional int32 id = 1;
}

通過類型名字創建出類型對象.

  • 預先編譯好proto模式
//! 利用類型名字構造對象.
/*!
 * @Param type_name 類型名字,比如 "Test.TestMessage".
 * @Return 對象指針,new 出來的,使用者負責釋放.
 */
#include <google/protobuf/descriptor.h>
#include <google/protobuf/message.h>
#include "cpp/test.pb.h" // 這是protoc給你生成的文件

int main()
{
    // 先獲得類型的Descriptor .
    auto descriptor = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName("T.Test");
    if (nullptr == descriptor)
    {
        return 0 ;
    }
    // 利用Descriptor拿到類型註冊的instance. 這個是不可修改的.
    auto prototype = google::protobuf::MessageFactory::generated_factory()->GetPrototype(descriptor);
    if ( nullptr == descriptor)
    {
        return 0 ;
    }
    // 構造一個可用的消息.
    auto message = prototype->New();
    // 只有當我們預先編譯了test消息並且正確鏈接才能這麼幹.
    auto test = dynamic_cast<T::Test*>(message);
    // 直接調用message的具體接口
    // 其實這些接口是語法糖接口.所以並沒有對應的反射機制來對應調用.
    // 反射機制實現了的Set/Get XXX系列接口,是屬於Reflection的接口,接收Message作爲參數.
    test->set_id(1);
    std::cout<<test->Utf8DebugString()<<std::endl;
    delete message ;
    return 0 ;
}
  • 直接解析proto 文件模式
#include <iostream>
#include <google/protobuf/compiler/importer.h>
#include <google/protobuf/dynamic_message.h>
int main()
{
    // 準備配置好文件系統
    google::protobuf::compiler::DiskSourceTree sourceTree;
    // 將當前路徑映射爲項目根目錄 , project_root 僅僅是個名字,你可以你想要的合法名字.
    sourceTree.MapPath("project_root","./");
    // 配置動態編譯器.
    google::protobuf::compiler::Importer importer(&sourceTree, NULL);
    // 動態編譯proto源文件。 源文件在./source/proto/test.proto .
    importer.Import("project_root/source_proto/test.proto");
    // 現在可以從編譯器中提取類型的描述信息.
    auto descriptor1 = importer.pool()->FindMessageTypeByName("T.Test");
    // 創建一個動態的消息工廠.
    google::protobuf::DynamicMessageFactory factory;
    // 從消息工廠中創建出一個類型原型.
    auto proto1 = factory.GetPrototype(descriptor1);
    // 構造一個可用的消息.
    auto message1= proto1->New();
    // 下面是通過反射接口給字段賦值.
    auto reflection1 = message1->GetReflection();
    auto filed1 = descriptor1->FindFieldByName("id");
    reflection1->SetInt32(message1,filed1,1);
    // 打印看看
    std::cout << message1->DebugString();
    // 刪除消息.
    delete message1 ;
    return 0 ;
}

通過對象和對象的屬性的名字獲取,修改對應屬性.

首先定義mesage :
對於上文提到的 test.proto

#include "cpp/test.pb.h"
#include <iostream>

int main()
{
    // 拿到一個對象,不在乎怎麼拿到,可以是通過反射拿到。
    // 這裏簡單直接的創建一個.
    T::Test p_test ;
    // 拿到對象的描述包.
    auto descriptor = p_test.GetDescriptor() ;
    // 拿到對象的反射配置.
    auto reflecter = p_test.GetReflection() ;
    // 拿到屬性的描述包.
    auto field = descriptor->FindFieldByName("id");
    // 設置屬性的值.
    reflecter->SetInt32(&p_test , field , 5 ) ;
    // 獲取屬性的值.
    std::cout<<reflecter->GetInt32(p_test , field)<< std::endl ;
    return 0 ;
}

通過對象和對象方法的名字調用對應的方法.

//TODO

protobuf 反射實現解析.

基本概念

Descriptor系列.

::google::protobuf::Descriptor 系列包括:

  • Descriptor – 用來描述 消息
  • FieldDescriptor – 用來描述 字段
  • OneofDescriptor – 用來描述 聯合體
  • EnumDescriptor – 用來描述 枚舉
  • EnumValueDescriptor – 用來描述 枚舉值
  • ServiceDescriptor – 用來描述 服務器
  • MethodDescriptor – 用來描述 服務器方法
  • FileDescriptor – 用來描述 文件

這些 Descriptor系列的數據則是由 DescriptorProto系列的數據 , 利用DescriptorBuilder工具類來填充的 .
有興趣的可以查閱 https://github.com/google/protobuf/blob/master/src/google/protobuf/descriptor.cc

DescriptorProto系列是一些用protobuf定義的,用來描述所有由protbuf產生的類型的類型信息包.
對應的proto文件在 : https://github.com/google/protobuf/blob/master/src/google/protobuf/descriptor.proto
Descriptor 系列最大的message是 FileDescriptor . 每個文件會生成一個包含本文件所有信息的FileDescriptor包.

舉個例子 :
對於上文提到的 test.proto , protoc 會給就會自動填裝一個描述包,類似於:

::google::protobuf::FileDescriptorProto file;
file.set_name("test.proto");
file.set_packet("T")
auto desc = file.add_message_type() ;
desc->set_name("T.Test");
auto id_desc  = desc->mutable_field();
id_desc->set_name("id");
id_desc->set_type(::google::protobuf::FieldDescriptorProto::TYPE_INT32);
id_desc->set_number(1);
//...

然後保存起來.
如果你讀protoc生成的 test.pb.cc文件 你會看到這樣的代碼 :

::google::protobuf::DescriptorPool::InternalAddGeneratedFile(
    "\n\013test.proto\022\001T\"\022\n\004Test\022\n\n\002id\030\001 \001(\005", 36);

其實就是在protoc生成代碼中hard code 了對應proto文件的FileDescriptor包序列化之後的數據. 作爲參數直接使用.

offset

任何一個對象最終都對應一段內存,有內存起始(start_addr)和結束地址,
而對象的每一個屬性,都位於 start_addr+$offset ,所以當對象和對應屬性的offset已知的時候,
屬性的內存地址也就是可以獲取的。


//! 獲取某個屬性在對應類型對象的內存偏移.
#define GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(TYPE, FIELD)    \
  static_cast<int>(                                           \
      reinterpret_cast<const char*>(                          \
          &reinterpret_cast<const TYPE*>(16)->FIELD) -        \
      reinterpret_cast<const char*>(16))

DescriptorDatabase

DescriptorDatabase是一個純虛基類,描述了一系列符合通過名字(文件名,符號名。。。) 來獲取FileDescriptorProto的接口 :

https://github.com/google/protobuf/blob/master/src/google/protobuf/descriptor_database.h

// 這裏我幹掉了裏面的英文註釋.
class LIBPROTOBUF_EXPORT DescriptorDatabase {
    public:
        inline DescriptorDatabase() {}
        virtual ~DescriptorDatabase();
        virtual ~DescriptorDatabase();
        // 通過文件名字找.
        virtual bool FindFileByName(const string& filename,
                FileDescriptorProto* output) = 0;
        // 通過符號名字找.
        virtual bool FindFileContainingSymbol(const string& symbol_name,
                FileDescriptorProto* output) = 0;
        // 通過擴展信息找.
        virtual bool FindFileContainingExtension(const string& containing_type,
                int field_number,
                FileDescriptorProto* output) = 0;
        // 通過擴展信息的tag數字找...
        virtual bool FindAllExtensionNumbers(const string& /* extendee_type */,
                vector<int>* /* output */) {
            return false;
        }
    private:
        GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(DescriptorDatabase);
};

核心的兩個派生類是 :

  • EncodedDescriptorDatabase

    • 支持DescriptorDatabase的全部接口
    • 接收序列化之後的FileDescriptorProto, 保存在map中備查.
    • 這個類對應着預先編譯鏈接好的那些類型的反射機制。
  • SourceTreeDescriptorDatabase

    • 僅支持DescriptorDatabaseFindFileByName接口。其餘直接返回false.
    • 每次查詢某個文件都是從磁盤讀入proto的源文件,編譯解析後返回對應的FileDescriptorProto .
    • 這個類對應着動態編譯proto源文件的時候的反射機制.

這裏我不探究protobuf 是如何運行時編譯proto源文件.

DescriptorPool

任何時候想要查詢一個Descriptor , 都是去DescriptorPool裏面查詢。
DescriptorPool 實現了這樣的機制 :

  • 緩存所有查詢的文件的Descriptor
  • 查找Descriptor的時候,如果自身緩存查到就直接返回結果,
    否則去自帶的DescriptorDatabase中查FileDescriptorProto
    查到就轉化成Descriptor, 返回結果並且緩存.

https://github.com/google/protobuf/blob/master/src/google/protobuf/descriptor.h


class LIBPROTOBUF_EXPORT  DescriptorPool{
 public:
  // Create a normal, empty DescriptorPool.
  DescriptorPool();

  // 幹掉一個灰常長的註釋,核心是下面兩條加一些注意事項.
  // 構造一個帶着DescriptorDatabase的Pool 。
  // 這樣查找的時候,優先從Pool中查找,找不到就到fallback_database中找.
  class ErrorCollector;
  explicit DescriptorPool(DescriptorDatabase* fallback_database,
                          ErrorCollector* error_collector = NULL);

  ~DescriptorPool();

  // 這個獲取編譯進入二進制的那些消息的pool 。這個接口就是我們獲取預先編譯鏈接好
  // 的消息的入口。
  // Get a pointer to the generated pool.  Generated protocol message classes
  // which are compiled into the binary will allocate their descriptors in
  // this pool.  Do not add your own descriptors to this pool.
  static const DescriptorPool* generated_pool();


  // Find a FileDescriptor in the pool by file name.  Returns NULL if not
  // found.
  const FileDescriptor* FindFileByName(const string& name) const;
  // .... 一系列Find XXX By XXX  接口 ... , 不全部複製了.

  // Building descriptors --------------------------------------------

  class LIBPROTOBUF_EXPORT ErrorCollector {
  // 不關心這個錯誤收集類...
  };

  // 這個是用FileDescriptorProto 填充FileDescriptor的接口.
  const FileDescriptor* BuildFile(const FileDescriptorProto& proto);

  // Same as BuildFile() except errors are sent to the given ErrorCollector.
  const FileDescriptor* BuildFileCollectingErrors(
    const FileDescriptorProto& proto,
    ErrorCollector* error_collector);
 // 依賴相關接口.
  void AllowUnknownDependencies() { allow_unknown_ = true; }
  void EnforceWeakDependencies(bool enforce) { enforce_weak_ = enforce; }

  // Internal stuff --------------------------------------------------
    // 一系列實現細節的接口。。 不復制。。。
 private:
    // 一系列實現細節的接口。。 不復制。。。

  // 當從pool本身的table找不到的時候,試圖從database中查找的接口。
  bool TryFindFileInFallbackDatabase(const string& name) const;
  bool TryFindSymbolInFallbackDatabase(const string& name) const;
  bool TryFindExtensionInFallbackDatabase(const Descriptor* containing_type,
                                          int field_number) const;

    // 一系列實現細節的接口。。 不復制。。。

  // See constructor.
  DescriptorDatabase* fallback_database_; // 持有的datebase
  scoped_ptr<Tables> tables_;   // Pool自身的table。 會緩存所有查過的文件內容.

    // 一系列實現細節的接口。。 不復制。。。
  GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(DescriptorPool);
}

核心的查找接口

https://github.com/google/protobuf/blob/master/src/google/protobuf/descriptor.cc


Symbol DescriptorPool::Tables::FindByNameHelper(
    const DescriptorPool* pool, const string& name) {
  MutexLockMaybe lock(pool->mutex_);
  known_bad_symbols_.clear();
  known_bad_files_.clear();
  //先從緩存中查詢.
  Symbol result = FindSymbol(name);

  // 這個是內部實現的細節 不要在意
  if (result.IsNull() && pool->underlay_ != NULL) {
    // Symbol not found; check the underlay.
    result =
      pool->underlay_->tables_->FindByNameHelper(pool->underlay_, name);
  }

  if (result.IsNull()) {
    // 這裏去數據庫嘗試獲取數據.
    // Symbol still not found, so check fallback database.
    if (pool->TryFindSymbolInFallbackDatabase(name)) {
        // 再次剛剛數據庫更新數據之後的緩存中獲取數據.
        result = FindSymbol(name);
    }
  }

  return result;
}

MessageFactory

任何時候想要獲取一個類型的instance , 都要去MessageFactory裏面獲取。
MessageFactory 是一個純虛的基類,定義了通過Descripor來獲取對應類型instance的接口.

{
 public:
  inline MessageFactory() {}
  virtual ~MessageFactory();
  // 了通過Descripor來獲取對應類型instance 的接口
  virtual const Message* GetPrototype(const Descriptor* type) = 0;

  // 這個是獲取編譯鏈接好的那些類型的factory單例的入口.
  static MessageFactory* generated_factory();
  // 這個是對應的像上面哪個單例內填裝數據的接口,protoc自動生成的文件都會有調用.
  static void InternalRegisterGeneratedMessage(const Descriptor* descriptor,
                                               const Message* prototype);
 private:
  GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(MessageFactory);
}

同樣有兩個核心的派生類

  • GeneratedMessageFactory
    • 一個map , 保存着DescriptorMessage *
    • 這個類對應着預先編譯鏈接好的那些類型的反射機制。
  • DynamicMessageFactory
    • 有簡單的緩存,保存自己解析過的Descriptor`` </li>
      <li>可以通過
      Descriptor“,動態的基於內存構造出一個Message !!!

解決問題的辦法

通過類型名字創建出類型對象 — 預編譯proto並且鏈接進入二進制.

查表!!
是的,你沒猜錯,就是查表!!!

  • 數據存儲在哪裏
    所有的Descriptor存儲在單例的DescriptorPool 中。google::protobuf::DescriptorPool::generated_pool()來獲取他的指針。
    所有的instance 存儲在單例的MessageFactory中。google::protobuf::MessageFactory::generated_factory()來獲取他的指針。

  • 將所有的Descriptor & instance 提前維護到表中備查

在protoc 生成的每個cc文件中, 都會有下面的代碼(protobuf V2 版本) :


// xxx 應該替換爲文件名,比如test.proto的test.

namespace {

//! 將本文件內的全部類型的instance註冊進入MessageFactory的接口.
void protobuf_RegisterTypes(const ::std::string&) {
   // 初始化本文件的reflection數據.
  protobuf_AssignDescriptorsOnce();
  ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage(
    Test_descriptor_, &Test::default_instance());
}
//! 本文件的初始接口.
void protobuf_AddDesc_xxx_2eproto() {
  static bool already_here = false;
  if (already_here) return;
  already_here = true;
  GOOGLE_PROTOBUF_VERIFY_VERSION;
  // 註冊本文件的Descriptor包. 這樣就可以用名字通過generated_pool獲取對應的Descriptor。
  ::google::protobuf::DescriptorPool::InternalAddGeneratedFile(
    "\n\013xxx.proto\022\001T\"\022\n\004Test\022\n\n\002id\030\001 \001(\005", 36);
  // 將本文件的類型instance註冊接口註冊給MessageFactory.
  // 這裏註冊接口是爲了實現類型的lazy註冊。如果沒有使用請求某個文件的類型,就不註冊對應文件的類型。
  ::google::protobuf::MessageFactory::InternalRegisterGeneratedFile(
    "xxx.proto", &protobuf_RegisterTypes);
  // 構造並且初始化全部instance.
  Test::default_instance_ = new Test();
  Test::default_instance_->InitAsDefaultInstance();
  // 註冊清理接口.
  ::google::protobuf::internal::OnShutdown(&protobuf_ShutdownFile_xxx_2eproto);
}
//! 下面利用全局變量的構造函數確保main函數執行之前數據已經進行註冊.
struct StaticDescriptorInitializer_xxx_2eproto {
  StaticDescriptorInitializer_xxx_2eproto() {
    protobuf_AddDesc_xxx_2eproto();
  }
} static_descriptor_initializer_xxx_2eproto_;
}

通過類型名字創建出類型對象 — 運行時態編譯proto

這裏要引入 Importer類.

class LIBPROTOBUF_EXPORT Importer {
 public:
     // 需要使用SourceTree來構造,
     // 不過SourceTree最終是用來構造SourceTreeDescriptorDatabase的。
  Importer(SourceTree* source_tree,
           MultiFileErrorCollector* error_collector);
  ~Importer();

  // 這個就是運行時態加載proto源文件的接口.
  // 多次調用同一個文件只有第一次有效。
  const FileDescriptor* Import(const string& filename);
  // 拿到DescriptorPool 的接口. 
  // 每個Importer都有自己的DescriptorPool。
  inline const DescriptorPool* pool() const {
    return &pool_;
  }
  // 下面是咱不在意的接口.
  void AddUnusedImportTrackFile(const string& file_name);
  void ClearUnusedImportTrackFiles();

 private:
  // 這兩個數據成員很好的解釋了Importer如何工作 :
  // 有 SourceTreeDescriptorDatabase 構造一個DescriptorPool,這樣
  // 每當有文件被查找的時候,如果緩存中有,直接返回,如果沒有,
  // SourceTreeDescriptorDatabase 自然會去加載解析源文件.
  // Import接口則是提前將proto解析加載進入緩存的途徑.
  SourceTreeDescriptorDatabase database_;
  DescriptorPool pool_;

  GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(Importer);
}

通過對象和對象的屬性的名字獲取,修改對應屬性.

  • GeneratedMessageReflection 的填裝和獲取
    對於每一個message , 都有一個對應的GeneratedMessageReflection 對象.
    這個對象保存了對應message反射操作需要的信息.

//!初始化本文件的所有GeneratedMessageReflection對象.
void protobuf_AssignDesc_xxx_2eproto() {
  protobuf_AddDesc_xxx_2eproto();
  const ::google::protobuf::FileDescriptor* file =
    ::google::protobuf::DescriptorPool::generated_pool()->FindFileByName(
      "xxx.proto");
  GOOGLE_CHECK(file != NULL);
  Test_descriptor_ = file->message_type(0);
  static const int Test_offsets_[1] = {
    //這裏在計算屬性的內存偏移.
    GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Test, id_),
  };
  // 這裏是個test包填裝的GeneratedMessageReflection對象.
  Test_reflection_ =
    new ::google::protobuf::internal::GeneratedMessageReflection(
      Test_descriptor_,
      Test::default_instance_,
      Test_offsets_,
      GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Test, _has_bits_[0]),
      GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Test, _unknown_fields_),
      -1,
      ::google::protobuf::DescriptorPool::generated_pool(),
      ::google::protobuf::MessageFactory::generated_factory(),
      sizeof(Test));
}
inline void protobuf_AssignDescriptorsOnce() {
  ::google::protobuf::GoogleOnceInit(&protobuf_AssignDescriptors_once_,
                 &protobuf_AssignDesc_xxx_2eproto);
}

// message.h 中 message的基本接口.
virtual const Reflection* GetReflection() const {
    return GetMetadata().reflection;
}
// 每個message獲取自己基本信息的接口.
::google::protobuf::Metadata Test::GetMetadata() const {
  protobuf_AssignDescriptorsOnce();
  ::google::protobuf::Metadata metadata;
  metadata.descriptor = Test_descriptor_;
  metadata.reflection = Test_reflection_;
  return metadata;
}
  • GeneratedMessageReflection 操作具體對象的屬性

按照offset數組的提示,註解獲取操作對應內存,這裏以int32字段的SetInt32接口爲例子.

    • 接口定義
#undef DEFINE_PRIMITIVE_ACCESSORS
#define DEFINE_PRIMITIVE_ACCESSORS(TYPENAME, TYPE, PASSTYPE, CPPTYPE)
void GeneratedMessageReflection::Set##TYPENAME(                            \
      Message* message, const FieldDescriptor* field,                        \
      PASSTYPE value) const {                                                \
    USAGE_CHECK_ALL(Set##TYPENAME, SINGULAR, CPPTYPE);                       \
    if (field->is_extension()) {    /*先不要在意這個*/                       \
      return MutableExtensionSet(message)->Set##TYPENAME(                    \
        field->number(), field->type(), value, field);                       \
    } else {
      /*一般的字段走這裏*/\
      SetField<TYPE>(message, field, value);                                 \
    }                                                                        \
  }

DEFINE_PRIMITIVE_ACCESSORS(Int32 , int32 , int32 , INT32 )
#undef DEFINE_PRIMITIVE_ACCESSORS
    • 內存賦值.

// 找到對應的內存地址,返回合適類型的指針.
template <typename Type>
inline Type* GeneratedMessageReflection::MutableRaw(
    Message* message, const FieldDescriptor* field) const {
  int index = field->containing_oneof() ?
      descriptor_->field_count() + field->containing_oneof()->index() :
      field->index();
  void* ptr = reinterpret_cast<uint8*>(message) + offsets_[index];
  return reinterpret_cast<Type*>(ptr);
}
// 設置protobuf的標誌bit.
inline void GeneratedMessageReflection::SetBit(
    Message* message, const FieldDescriptor* field) const {
  if (has_bits_offset_ == -1) {
    return;
  }
  MutableHasBits(message)[field->index() / 32] |= (1 << (field->index() % 32));
}
// 設置某個字段的值
template <typename Type>
inline void GeneratedMessageReflection::SetField(
    Message* message, const FieldDescriptor* field, const Type& value) const {
  if (field->containing_oneof() && !HasOneofField(*message, field)) {
    ClearOneof(message, field->containing_oneof()); // V3 oneof 類型的清理。
  }
  *MutableRaw<Type>(message, field) = value; // 先直接覆蓋
  field->containing_oneof() ?
      SetOneofCase(message, field) : SetBit(message, field); // 添加標記bit
}

通過對象和對象方法的名字調用對應的方法.

//TODO

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章