orocos 類型系統分析

其實代碼分析是完整的,需要點耐心才能看完。。。

在 orocos 中所有類型的信息是通過一個叫 TypeInfo 的結構體來保存的,每增加一種類型就會 new 一個 TypeInfo 對象來表示該類型的信息:

/**
 * A class for representing a user type, and which can build
 * instances of that type. Once you get hold of a TypeInfo
 * object pointer, this pointer will be valid during the whole
 * lifetime of the process. 
 */
class RTT_API TypeInfo
{
public:
    typedef const std::type_info * TypeId;

    TypeInfo(const std::string& name) : mtypenames(1,name) {}  // 用戶自定義類型的名字

    ~TypeInfo();

    const std::string& getTypeName() const { return mtypenames[0]; }

    std::vector<std::string> getTypeNames() const;  // 返回 mtypenames

    void addAlias(const std::string& alias){  // 類型別名
    // only alias new names:
    if ( !alias.empty() && find(mtypenames.begin(), mtypenames.end(), alias) == mtypenames.end() )
        mtypenames.push_back(alias);
     }

    // Returns the compiler generated type id pointer.
    TypeId getTypeId() const { return mtid; }  // 獲得編譯器生成的 const std::type_info *


    // Returns the compiler generated type name (non portable accross compilers!).
    const char * getTypeIdName() const { return mtid_name; }


    void setTypeId(TypeId tid) { // 由於TypeInfo類與類型無關,所以需要顯示的setTypeId
        mtid = tid;
        mtid_name = tid->name();
    }

    ...

    std::vector<std::string> mtypenames;  // 初始化爲構造是時候傳入的名字(用戶自定義的名字), addAlias會push_back一個名字。
    const char* mtid_name;  // 這個是 std::type_info.name() 返回的字符串,編譯器生成,根據平臺不同。
    TypeId mtid;  // 這個是 const std::type_info * 類型
...
};

然後 orocos 中有一個單例的全局對象 TypeInfoRepository, 根據這個類的名字就可以知道該對象是一個包含所有類型信息的倉庫

class RTT_API TypeInfoRepository
{
    TypeInfoRepository();
    typedef std::map<std::string, TypeInfo*> map_t;
    map_t data;  // 保存所有類型信息的map容器, 即倉庫
    ...
}

在 orocos 中,用戶自定義類型的註冊過程是這樣的:
1. 定義類型
2. 添加到全局對象中

e.g:

class UserType  // 用戶定義類型
{}

TypeInfoRepository::Instance()->addType( new StdTypeInfo<UserType>("UserTypeName"));  // 註冊該類型&名字

其中 StdTypeInfo 派生至 TypeInfoGenerator,而TypeInfoGenerator 是 orocos 類型信息的基類,e.g:

StdTypeInfo : public ... : public TypeInfoGenerator
{}

在上面調用 addType 的時候,如下過程被調用:

bool TypeInfoRepository::addType(TypeInfoGenerator* t)
{
    if (!t)
        return false;
    std::string tname = t->getTypeName();
    TypeInfo* ti = t->getTypeInfoObject();  // 這個調用見下面分解

    {
        MutexLock lock(type_lock);
        if (ti && data.count(tname) && data[tname] != ti ) {
            log(Error) << "Refusing to add type information for '" << tname << "': the name is already in use by another type."<<endlog();
            return false;
        }
    }
    // Check for first registration, or alias:
    if ( ti == 0 )
        ti = new TypeInfo(tname);  // 第一次註冊的時候會 new
    else
        ti->addAlias(tname);

    if ( t->installTypeInfoObject( ti ) ) { // will return false, so this will not be deleted!
        delete t;
    }
    MutexLock lock(type_lock);
    // keep track of this type:
    data[ tname ] = ti;

    log(Debug) << "Registered Type '"<<tname <<"' to the Orocos Type System."<<Logger::endl;
    for(Transports::iterator it = transports.begin(); it != transports.end(); ++it)
        if ( (*it)->registerTransport( tname, ti) )
            log(Info) << "Registered new '"<< (*it)->getTransportName()<<"' transport for " << tname <<endlog();
    return true;
}

對上面代碼進行分解,其中 :

TypeInfo* ti = t->getTypeInfoObject() 是虛函數調用,實際調用的是其父類下面這個(帶類型信息的):

// 這個 T 就是上面的用戶自定義類型
TypeInfo* getTypeInfoObject() const {
    return TypeInfoRepository::Instance()->getTypeInfo<T>();  // 這裏又調用全局對象的 getTypeInfo<T>
        }


然後調用下面這個,使用 typeid(T) 來獲取其上面的類型 T 的 const std::type_info *:
TypeInfo* TypeInfoRepository::getTypeById(TypeInfo::TypeId type_id) const {  // TypeInfo::TypeId 就是 const std::type_info *
      if (!type_id)
          return 0;
      MutexLock lock(type_lock);
      // Ask each type for its type id name.
      map_t::const_iterator i = data.begin();
      for (; i != data.end(); ++i){
        if (i->second->getTypeId() && *(i->second->getTypeId()) == *type_id)  // 查看map中是否已經存在該類型信息,顯然第一次addType的時候該map爲空,所以返回下面的 0
          return i->second;
      }
      return 0;  // map中不存在,返回0
    }

再次回到最上面的 addType 函數調用中:

if ( ti == 0 )
    ti = new TypeInfo(tname);  // 這裏生成一個用戶自定義類型的TypeInfo對象。

if ( t->installTypeInfoObject( ti ) ) {  // 這個函數與其看起來的意思相反,反而是類似 ti->set(t) 這種效果。
    delete t;  // 哈,由於installTypeInfoObject永遠返回false,所以 TypeInfoGenerator 也就不會被delete了(而是在內部變成了一個shared_ptr)。看起來怪怪的...
}


//在 t->installTypeInfoObject( ti ) 內部,不僅把 t 變成了一個shared_ptr
//而且同時還根據模板展開定義一個 internal::DataSourceTypeInfo<T> 類,該類有一些靜態函數可以調用,以獲取針對類型 T 的信息:

bool installTypeInfoObject(TypeInfo* ti) {
    // Install the factories for primitive types
    ti->setValueFactory( this->getSharedPtr() );
if (use_ostream)
ti->setStreamFactory( this->getSharedPtr() );

    // Install the type info object for T
    internal::DataSourceTypeInfo<T>::value_type_info::TypeInfoObject = ti;  // 模板展開,定義與類型T相關的類 DataSourceTypeInfo<T>
    ti->setTypeId( &typeid(T) );  // 構造的時候只有一個名字,這裏再setTypeId,至此, ti 的信息就完整了。

    // Clean up reference to ourselves.
    mshared.reset();  // 解決t自身內部的shared_ptr,否則t永遠也不會被銷燬
    // Don't delete us, we're memory-managed.
    return false;  // 永遠返回 false
}

前面介紹了這麼多關於這個類型系統,但是如何使用這個類型系統呢?

這就得從 orocos 的數據表示開始了:

orocos 內部的所有數據都是派生自 DataSourceBase ,只要是使用orocos 內部數據,其基類必定是 DataSourceBase,例如 RTT::Property<T>;該數據結構是類型無關的,且只定義了一些接口,其中就有 getTypeInfo() 這個接口:

  class RTT_API DataSourceBase
  {
  protected:
      // 這裏有關於使用 boost::intrusive_ptr 而不是 shared_ptr 的解釋
      /**
        *We keep the refcount ourselves.  We aren't using
        *boost::shared_ptr, because boost::intrusive_ptr is better,
        *exactly because it can be used with refcounts that are stored
        *in the class itself.  Advantages are that the shared_ptr's for
        *derived classes use the same refcount, which is of course very
        *much desired, and that refcounting happens in an efficient way,
        *which is also nice :)
      */

      mutable os::AtomicInt refcount;  // boost::intrusive_ptr 的引用計數

      /** the destructor is private.  You are not allowed to delete this
       * class RTT_API yourself, use a shared pointer !
       */
      virtual ~DataSourceBase();

  public:

      typedef boost::intrusive_ptr<DataSourceBase> shared_ptr;

      ...

      virtual const types::TypeInfo* getTypeInfo() const = 0;  // 就是這個接口

DataSource<T> 結構派生自 DataSourceBase,是有類型信息的:

template<typename T>
class DataSource
  : public base::DataSourceBase
{
protected:
    virtual ~DataSource();

public:

    ...

    virtual const types::TypeInfo* getTypeInfo() const  // override 基類 的 getTypeInfo,其中又調用下面這個 類 靜態函數
    {
        return GetTypeInfo();
    }

    static const types::TypeInfo* GetTypeInfo();  // DataSource<T>類的靜態函數,見下文分析.

DataSource<T>類的靜態函數的實現如下:

template< typename T>
const types::TypeInfo* DataSource<T>::GetTypeInfo() { return DataSourceTypeInfo<T>::getTypeInfo(); }  // 由此可知,調用的DataSourceTypeInfo<T>靜態對象的getTypeInfo()

這個全局靜態對象(類型相關) DataSourceTypeInfo<T> 是最開始的時候 installTypeInfoObject(ti) 內部生成的。

過程就是:

基類(類型無關) -> 派生類(類型相關) -> 派生類靜態函數(類型相關) -> 全局DataSourceTypeInfo<T>類的靜態成員函數(類型相關) -> 單例對象的getTypeInfo<T>(類型無關) -> 目標類型的TypeInfo對象指針

這裏有個疑問,爲什麼需要這麼設計?需要獲得特定類型的TypeInfo對象指針也沒那麼複雜啊。。。難道爲了解耦整個系統的類型註冊這部分代碼,爲了新增加類型後還可以分模塊編譯(而不是所有代碼放在一起才能編譯鏈接)?

仔細想了一下,好像是這麼回事。orocos中所有類型可以分爲兩種,第一種是在類型系統中註冊過的類型,第二種是沒有在類型系統中註冊過的(廢話。。。)。這兩部分代碼都是模板類,理論上需要共用同一套代碼,但是怎樣設計能夠讓這兩部分看起來能夠單獨工作互不影響,但是又能夠聯繫在一起呢?就是通過設計這些全局的模板類以及其靜態成員(變量和函數)來實現。因爲根據 ODR 規則,模板函數可以存在很多份定義,因爲模板類是針對每一種類型T存在一份定義,而不是每一個cpp中實例化的時候存在一份定義,這也就是說不同模塊的庫文件在鏈接的時候(或者動態加載的時候)不會出現鏈接重定義的錯誤。see SO link:
>

The linker will remove all but one copy of each instantiation. But the compiler can’t know what other modules you will link with, so it has to compile whatever instantiations you use, unless you provide a promise to provide it at link time, using extern. Templates, like inline functions, are exempted from the multiple definitions conflict; the linker will just pick one copy to use. The ODR rule still applies, all copies with the same name must have identical behavior, so that which one the linker picks doesn’t matter.

通過 DataSourceTypeInfo<T> 這個橋樑, DataSource<T>TypeInfoRepository 就能單獨編譯並加載,並且動態庫加載後就能夠找到唯一一份DataSourceTypeInfo<T>的實現。

更新,見鏈接測試https://github.com/fengzhuye/cpp-learn/tree/master/dynamic_load_with_template

如果將 class 的 visibility 定義爲 “hidden”,或者編譯可執行程序的時候沒有加上-rdynamic則可執行程序和動態鏈接庫中都自己保存有一份 template class static member,這個 static member 不是全局共享的,當前模塊中。但是如果 visibility 爲 public(默認) 並且編譯可執行程序的時候加上了 -rdynamic 選項,則這個 static member是全局共享的(動態加載的時候兩個模塊內的這個對象會鏈接到同一個地址上)。

例如定義如下模板類:

#define TEST_API __attribute__((visibility("hidden")))

template<class T>
struct TEST_API Blast
{
    static double b;
    static double getB()
    {
        return b;
    }
};

在動態庫 liba.so 中賦值 Blast<char>::b = 1.23 ; 在動態庫 libb.so 中賦值 Blast<char>::b = 3.21 ; 那麼在 liba.so 中調用 getB 獲得的永遠是 1.23, 在 libb.so 中調用 getB 獲得的永遠是 3.21,與調用順序無關。但是如果這樣定義:

#define TEST_API __attribute__((visibility("default")))

template<class T>
struct TEST_API Blast
{
    static double b;
    static double getB()
    {
        return b;
    }
};

main程序加載動態庫,並且編譯main程序的時候加上 -rdynamic 則 Blast::b 是所有模塊全局共享的。


所以,歸根結底,基類是通過一堆不同類型全局對象的靜態成員函數來獲取其父類類型信息的 TypeInfo 對象的。獲得了TypeInfo對象就能夠調用針對該特定類型的一些操作(例如,實現針對 該類型 的 cast,串行化,序列化等等操作)。


大概過程總結就是:
先創建用戶自定義類型,並將該類型信息添加到全局單例對象TypeInfoRepository 中。 在代碼運行過程中,如果我獲得了一個數據的基類指針(類型無關的DataSourceBase),我需要調用虛函數getTypeInfo來獲得其父類(類型相關)的TypeInfo*對象,而其父類通過將getTypeInfo綁定到一個特定類型的全局的靜態對象上,該全局靜態對象再調用單例對象TypeInfoRepository的方法來獲得特定TypeInfo* 的信息。

因爲系統中每一個類型 T 都對應一個全局類定義 DataSourceTypeInfo<T>,所以存在很多個針對不同類型 DataSourceTypeInfo<T> 類定義,這些不同的 DataSourceTypeInfo<T> 裏頭全是靜態成員和靜態成員函數,有一個靜態成員就保存着有一份與該類型相關的TypeInfo對象的指針。至於爲什麼需要用到全局類DataSourceTypeInfo<T>,這就是orocos類型系統插件形式的強大之處了。

獲得了對應的對象的TypeInfo 後,就可以調用針對該對象的類型的一些操作,例如,static_cast,ostream輸出, 文本化,串行化,序列化等等,這個是關鍵。

這個設計模式很強大,關於插件機制的架構,可以google Boost.DLL

see ref:
http://en.cppreference.com/w/cpp/types/type_info/hash_code
https://en.wikipedia.org/wiki/One_Definition_Rule
http://www.boost.org/doc/libs/1_64_0/doc/html/boost_dll/tutorial.html
https://stackoverflow.com/questions/19373061/what-happens-to-global-and-static-variables-in-a-shared-library-when-it-is-dynam

發佈了147 篇原創文章 · 獲贊 77 · 訪問量 63萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章