C++ 類的動態組件化技術


序言:

         N年前,我們曾在軟件開發上出現了這樣的困惑,用VC開發COM組件過於復雜,用VB開發COM組件發現效率低,而且不能實現面向對象的很多特性,例如,繼承,多態等。更況且如何快速封裝利用歷史遺留的大量C++代碼本身就是一個大的難題。

         當時,開發小組的成員通過共同努力,摸索了一套C++類的動態組件化技術,很好的解決了以上的問題,通過這個技術,我們繼承了大量的C++代碼,同時使這些C++程序以COM+組件的形式得以新生。通過這幾年在實際應用中的考驗,這個技術是成熟可靠的。

       也許新的系統大多數都完全構建在.Net 或者 Java 上,但是我覺得當時我們研究C++類對象的組件化的思路很好,從軟件工程的角度,解決了一個日常程序員可能遇到的難題。大家用一套組件編程的規范實現程序,也降低了代碼的難度,提高了維護性。這樣一般對COM技術不是很熟悉的程序員也能用C++做出優良的COM組件,並且能實現組件的繼承,多態等。

        現在我將這個技術的思路在博客上公開,如果還能對大家現在的工作有所幫助的話,實在榮幸,呵呵!

C++類的動態組件化技術

關鍵詞

COM組件   接口  生命周期    C++類  ATL組件類    C++基類  ATL模板基類   繼承

摘要

在組件化編程的時代,如何復用歷史累積的大量沒有組件特性的C++類?本文從工程的角度對這一問題進行探討,利用現有組件技術,提出了一套將C++類平滑過渡到COM組件的完整解決方案。

1.   問題的提出

自從Microsoft公布了COM(Component Object Model,組件對象模型,簡稱COM)技術以后,Windows平台上的開發模式發生了巨大的變化,以COM為基礎的一系列組件技術將Windows編程帶入了組件化時代,傳統的面向對象的軟件開發方法已經逐漸被面向組件的方法所取代。

COM標准建立在二進制可執行代碼級的基礎上,不論何種工具、語言開發的組件,只要符合COM規范,就可復用於VC、VB、Delphi、BC等各種開發環境中。COM的語言無關性將軟件復用的層次從源代碼級推進到了二進制級,復用更方便,也更安全。

然而,COM技術帶來全新的軟件設計和開發模式的同時,也帶來了新的問題。

許多軟件公司在開發自己的軟件產品過程中,都累積了大量C++類,這些代碼設計精良,功能完備,以面向對象的標准來檢驗無可挑剔。然而,這些代碼不支持COM,將無法在COM時代繼續被復用。如果它們在軟件組件化的趨勢中被淘汰,那對軟件公司和開發人員來說都是極大的損失。

COM專家Don Box曾說過,“COM is a super C++”。這給了我們一個啟示,是否可以實現一種技術,能夠動態的為普通C++類加上一層COM的封裝呢?這樣,既可以保持這些代碼自身的完整和特性,使它們能繼續應用於原來的系統,也可以在需要作為組件使用的時候,把它們動態轉變成組件,復用於新系統。

一個自然而然的想法是,為每一個C++類開發一個只暴露一個接口的COM組件,將原C++類的每個public方法都對應於該接口的一個方法,接口方法的實現可以簡單的調用相對應的C++類方法即可。並且一般情況下C++對象實例是以普通對象存在的,只有外部需要以組件的方式得到該C++對象實例的時候,程序會自動給該對象加上一個組件外殼。外部程序通過外殼的接口實現對該C++對象的操作。

這樣,程序邏輯由原有的C++類控制,但COM層的封裝則由組件提供。基本思路如下圖所示:

 

 

 

本文就這一技術展開討論,最終提供一套由普通C++類平滑過渡到COM組件的完整解決方案。我們選用ATL(Active Template Library,活動模板庫,簡稱ATL)作為COM組件的開發工具。如沒有特殊說明,下文中的“C++類”指沒有組件特性C++類,“C++對象”指C++類的實例;“ATL組件類”指用於包裝的ATL類,“ATL對象”指ATL組件類的實例。

2.   用ATL包裝C++類

按上述思路將C++對象動態組件化后,所得的組件實際上由兩部分組成:ATL組件對象和綁定的C++對象。兩者的生命周期互相牽制,但要保持一致。生命周期的管理是C++類動態組件化的首要難點。

C++類分為兩種,一種是簡單的C++類,一種是集合型的C++類。集合型的C++對象管理一組C++對象,負責其創建和刪除,維護它們的生命周期。下面,分別就簡單C++類和集合型C++類的組件化技術進行說明,展示解決方案的核心技術。

2.1. 簡單C++類的組件化

為使ATL組件類可以自由調用C++類的方法,需要:

l  為ATL組件類安插一個指針成員變量,指向C++類

l  提供ATL對象和C++對象的綁定機制

 

我們可以在ATL組件類初始化時創建一個C++類,用成員變量m_pCPPObj記錄,在析構時刪除,從而實現ATL組件類和C++類的天然綁定。但出於靈活性考慮,使得ATL組件對象可以綁定任意C++類的對象,我們為ATL組件類添加一個綁定函數Link2CPPObj(CImplement* pObj)。

ATL組件類的構造函數內,創建一個C++對象,用m_pCPPObj記錄。如果調用了Link2CPPObj,則將m_pCPPObj指向的對象刪除,改用傳入的C++對象。在ATL組件類的的析構函數內,刪除其綁定的C++對象。由構造函數和Link2CPPObj函數的定義可知,m_pCPPObj指針總是有意義的。簡單C++類組件化的思想如下圖所示:

 

 

2.2. 集合型C++類的組件化

集合型C++類是以數組(array)、列表(list)、映射表(map)等容器形式管理其它C++對象。集合對象和它管理的元素對象如果用上述方法分別包裝成組件后,集合型ATL對象可能調用一個“Destroy”方法,期望刪除集合型ATL對象及其對應的元素ATL對象。但這一操作的實質卻是,在調用集合型ATL對象的“Destroy”方法時,集合型C++對象的“Destroy”方法被調用,將元素C++對象刪除了,而元素C++對象對應的ATL對象卻沒有刪除。這一操作的結果導致了元素的ATL對象存在,而其綁定的C++對象卻被刪除的情況,兩者的生命周期出現了不一致。

為了解決這個問題,我們需要在C++對象被刪除時,能將ATL對象同時刪除;而在ATL對象的引用計數為0需要刪除自身時,也能把C++對象刪除。可行的解決方案是:

l  在C++類中保存一個接口指針,指向綁定在一起的ATL對象;為該接口指針賦值的最佳地點顯然是提供綁定機制的Link2CPPObj函數內部,為此,還需要給Link2CPPObj添加一個IUnknown*參數

l  在C++類的析構函數中,判斷該接口指針是否為空,如果不為空,則Release對接口的引用,引發ATL對象自身的析構

 

現在,技術方案如下圖所示:

 

 

 

2.3. 內部創建的組件和外部創建的組件

集合型C++類組件化后仍然是集合型ATL組件,它可以創建、刪除自己管理的組件。這樣,組件的創建就可能有兩種情況:

l  由客戶直接創建

l  由客戶調用集合型組件的接口方法間接創建

創建方式的不同導致了組件生命周期管理的復雜性。一般說來,組件的創建者負責維護組件的生命周期。上述兩種情況下,分別由客戶和集合型組件維護被創建組件的生命周期。當然還有有一種情況是,客戶創建了一個組件,然后送交一個集合型組件管理,現在維護組件生命周期的責任就由客戶轉交給了集合型組件。

我們的解決方案必須提供這樣的健壯性和靈活性,以維護各種情況下組件的生命周期。我們為ATL組件類添加一個BOOL成員m_bInnerManage,作為組件的維護標識。內部維護意味着組件的生命周期由其它組件(集合型組件)維護;外部維護則是由客戶維護。

 

 

缺省情況下,組件是外部創建並維護的,在組件的構造函數內設置外部維護標識。集合型組件創建元素時,需要為元素分別創建一個C++對象和一個ATL對象,然后調用ATL對象的Link2CPPObj函數將兩者綁定在一起,在Link2CPPObj函數內修改維護標識。對於第三種情況,可以在外部創建組件由客戶轉交給集合型組件時,在集合型組件相應方法內重新設置維護標識。

2.4. C++基類

為了對現有C++類的改動最小,我們設計一個基類封裝需要為C++類添加的功能。所有需要動態組件化的C++類都必須從這個基類派生,以保證動態組件化中C++對象與ATL對象生命周期的一致。如下圖示:

 

 

實現代碼如下所示:

class CCPP2ATLObjBase

{

       CCPP2ATLObjBase ();

public:

       // IUnknown指針,反指向封裝該CPP類的接口

       IUnknown*    m_pAssociATLUnk;

protected:

       virtual ~ CCPP2ATLObjBase ();

};

CCPP2ATLObjBase::CCPP2ATLObjBase()

{

       // 將IUnknown指針初始化為0

       m_pAssociATLUnk = NULL;

}

CCPP2ATLObjBase::~CCPP2ATLObjBase()

{

       // CPP類的對象析構時,Release對接口的引用

       if (m_pAssociATLUnk)

              m_pAssociATLUnk->Release();

}

然后,修改現有各個C++類,使之從CCPP2ATLObjBase派生,如下面代碼片斷所示:

class CImplement : public CCPP2ATLObjBase

{

       ……

};

2.5. ATL模板基類

通過以上分析,我們發現,所有的ATL組件類都需要實現一些相同的功能:

l  保留一個指向其綁定C++對象的指針

l  提供一個Link2CPPObj函數

l  在構造函數中創建一個綁定C++類的對象

為了減化編碼,我們定義一個帶參數的模板基類,實現上述公共功能,模板參數就是綁定的C++類。然后,所有的ATL組件類都從模板基類中派生。現在的技術方案如下圖所示:

 

 

實現代碼如下所示:

template <class T>

class CCPP2ATLTemplateBase :

{

protected:

       // C++類指針

       T*          m_pCPPObj;

       // 標識繼承該模板的ATL對象是否由內部維護

       BOOL     m_bInnerManage;

public:

       /**********************************************************

         模板的構造函數,實現如下功能:

         1、new一個C++實現類對象

         2、缺省情況下,ATL對象由外部維護,將內部維護標識設為FALSE

         3、將C++類中對ATL接口的反指指針設置為空

       **********************************************************/

       CAtlCPP2ATLTemplateBase()

       {

              m_pCPPObj = new T;

              m_bInnerManage = FALSE;

              m_pCPPObj->m_pAssociATLUnk = NULL;

       }

 

       /**********************************************************

         析構ATL對象時,如果該ATL對象是由外部創建的,

         則顯式的刪除C++對象

         如果ATL對象由內部維護,那么什么事都不用做

       **********************************************************/

       virtual ~CAtlCPP2ATLTemplateBase()

       {

              if (!m_bInnerManage) {

                     if (m_pCPPObj)

                            delete m_pCPPObj;

              }

       }

 

       /**********************************************************

         Link2CPPObj函數,負責綁定C++對象和ATL接口

         1、刪除構造函數中new的C++對象,而使用外部傳入的C++對象

         2、將ATL對象的內部維護標識設為TRUE

         3、設置C++基類中的接口指針成員

         4、因為ATL接口傳送給外部使用,需要增加引用計數

       **********************************************************/

       virtual void Link2CPPObj(T* pObj, IUnknown* pUnk)

       {

              ASSERT(pObj != NULL);

              ASSERT(pUnk != NULL);

 

              if (m_pCPPObj)

                     delete m_pCPPObj;

              m_pCPPObj = pObj;

              m_bInnerManage = TRUE;

              m_pCPPObj->m_pAssociATLUnk = pUnk;

              m_pCPPObj->m_pAssociATLUnk->AddRef();

       }

};

然后,每個ATL類都從該模板類派生,如下代碼片斷所示:

class ATL_NO_VTABLE CATLXX :

       ……,

       // 添加ATL模板基類

       public CCPP2ATLTemplateBase<CImplementXX>

{

       ……

}

3.   C++參數類型的自動化包裝

在本文的技術方案中,C++類的public方法與ATL組件接口中的方法一一對應;相應的,C++類中方法的參數類型也要轉換為COM規范所允許的數據類型。

在基於COM的自動化(Automation)技術中,Microsoft提供了一套自動化兼容的數據類型VARIANT所有簡單數據類型都可以在VARIANT中找到對應的定義,但在多數的基於C++的系統設計中,方法參數不會僅僅出現簡單數據類型,類對象、對象引用、對象指針被頻繁的作為參數來傳遞。

以類對象、對象引用或對象指針形式存在的參數,我們稱為復雜類型參數。在技術方案中,所有復雜類型參數在ATL接口方法中一律對應接口指針,我們需要提供C++對象(或引用、指針)和ATL接口指針之間的動態轉換功能。

復雜類型在傳入時,如何將ATL接口方法獲取的接口指針轉變為C++對象指針呢?對於ATL對象,可以直接取得m_pCPPObj變量,而接口指針卻不能。所以,需要提供一種途徑,從ATL接口指針獲取ATL組件的m_pCPPObj變量值。

我們的設計是,為每個ATL組件提供一個基接口ICPPObjSeeker,實現對綁定C++對象指針(即m_pCPPObj)的查詢方法HandleCPPObj。任意ATL接口都從該基接口派生,都可以調用HandleCPPObj方法。

在前文就生命周期管理進行討論時,曾提到這樣一種情況:客戶創建了一個組件,然后送交集合型組件管理。在集合型組件獲取外部創建的組件的同時,需要:

l  取得后者的C++對象指針。集合型組件對元素組件管理的實質是通過集合型C++對象對元素的C++對象進行管理,而集合型ATL對象和元素ATL對象之間並沒有直接聯系

l  修改新加入元素組件的維護標識

因此,我們為ICPPObjSeeker接口添加PostCPPObj方法,用於實現以上功能。

復雜類型在傳出時只需從C++指針轉換為接口指針即可。

4.   接口的繼承與多態

C++類的繼承應用十分廣泛,動態化后的組件應該保留原C++類之間的繼承關系。在我們的技術方案中,C++類和接口一一對應,C++類的繼承關系也應該體現在各個接口上,如下圖所示:

 

 

實現接口繼承的實質是為派生ATL類添加基接口,而為一個ATL類添加接口的實質則是:

l  修改IDL文件,體現接口的繼承關系

l  在ATL類中提供接口實現

修改IDL文件很簡單,只需要更改派生接口的基接口即可。在ATL類中添加基接口的實現倒頗費思量,我們的做法是:

l  擴展ATL模板基類的意義,每一個ATL組件類都對應一個模板基類,都從該模板基類派生

l  派生類的模板基類,從基類的模板基類中派生;CCPP2ATLTemplateBase是模板派生樹的根節點,所有的模板都派生自CCPP2ATLTemplateBase

l  所有的接口方法,都在對應的模板基類中實現

ATL派生類繼承自它對應的模板基類,這個模板基類又繼承自ATL基類對應的模板基類,而在ATL基類的模板基類中提供了基接口的實現。所以,ATL派生類最終繼承了基接口的實現。C++類、ATL類、各模板基類的繼承關系如下圖所示:

 

 

在實現接口的繼承后,要展現接口的多態性就很容易了,只需在ATL派生類聲明的接口映射表中添加基接口表項即可。

5.   總結

為了普通C++類平滑過渡到COM組件,首先利用ATL組件包裝C++類,在包裝C++類時在C++類中添加了一個指向ATL組件接口的指針來解決ATL組件和C++類生命周期的一致性問題,隨后我們利用C++基類和ATL模板基類簡化了這些工作的實現;然后我們對C++參數類型進行了自動化包裝;最后我們利用模板實現了接口的繼承和多態。至此,我們就完整地實現了將普通C++類平滑過渡到COM組件。在上述實現中,C++基類CCPP2ATLObjBase、ATL模板基類CCPP2ATLTempBase和基接口ICPPObjSeeker是本方案中的關鍵點。CCPP2ATLObjBase配合CCPP2ATLTempBase,完善了組件對象生命周期的管理機制;通過基接口ICPPObjSeeker,我們可以從任意接口反向查詢C++對象;CCPP2ATLTempBase提供了C++對象和ATL組件的自由綁定功能,封裝了IDispatch接口的實現,而進一步定義的ATL模板基類繼承體系則極大的方便了接口的自由繼承。

當然,我們不得不特別提到Microsoft的“.Net FrameWork”。“.Net”開發框架的推出,的確解決了COM技術的許多困惑,也包括本技術方案所要解決的一些技術問題。然而“.Net Framework”是一個“改朝換代”的變化,要想一步將原來基於C++的系統(尤其是大型系統)完全移植到“.Net”平台上的工作量是不可想象的,不亞於重新開發的工作量,所以Microsoft特別推薦從COM技術到“.Net”平台的平滑移植。由此看來,本文提出的動態組件化的技術更顯得可貴,它從工程化的角度,着眼於實際應用,解決了從面向對象的C++到基於組件的COM技術的許多問題,既充分保護了原有系統的積累,又為這些系統搭上日益發展的“.Net”快車提供了可能。

參考文獻

l  《COM原理與應用》,潘愛民 著,清華大學出版社

l  《COM本質論(Essential COM)》,Don Box 著,潘愛民 譯,中國電力出版社

l  《深入解析ATL(ATL Internals)》,Brent Rector、Chris Sells 著,潘愛民、新語 譯,中國電力出版社

l  《設計模式-可復用面向對象軟件的基礎(Design Patterns-Elements of Reusable Object-Oriented Software)》,Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides 著,李英軍、馬曉星、蔡敏、劉建中 等譯


免責聲明!

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



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