• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            posts - 126,  comments - 73,  trackbacks - 0
            本文假設你熟悉C++和COM。

            摘要:
            ??? ATL——活動模板庫(The Active Template Library),其設計旨在讓人們用C++方便靈活地開發COM對象。ATL本身相當小巧靈活,這是它最大的優點。用它可以創建輕量級的,自包含的,可復用的二進制代碼,不用任何附加的運行時DLLs支持。
            ??? 由于COM技術良好的口碑,越來越多的程序員已經走進或正在走進COM的編程世界。它就像盛夏里的冰鎮啤酒,從來不會讓你失望??上ё鳛橐粋€C++程序員來說,C++從不與我分享COM的極致以及我對COM的情有獨鐘。
            ??? C++與COM之間若即若離,和平共處,一次又一次在每個對象中用同樣簡潔的幾行代碼實現IUnknown。我敢肯定將來C++編譯器和鏈接器會實現C++對象和COM對象之間自然 的無意識的對應和映射,目前這個環境只存在于實驗室中,因此它肯定不是一個你我今天可以購買的產品。眼下可得到的最接近這個環境的東西就是活動模板庫——ATL。

            為什么使用ATL?
            ???
            ATL是在單層(single-tier)應用逐漸過時,分布式應用逐漸成為主流這樣一個環境中誕生的, 它最初的版本是在四個C++頭文件中,其中有一個還是空的。它所形成的出色的構架專門用于開發現代分布式應用所需的輕量級COM組件。作為一個模塊化的標準組件,ATL不像MFC有厚重的基礎結構,省時好用的庫使得成百上千的程序員一次又一次輕松實現IUnknown 和IClassFactory。
            ??? ATL的構架并不打算包羅萬象,無所不能。其第一個版本對實現IUnknown,IClassFactory,IDispatch,IconnectionPointContainer及COM枚舉提供非常 到位的支持。第二個版本除了可以編寫ActiveX控件外,還對最初的第一個版本中ATL類進行了增強。ATL不提供集合(collections)和串(strings)的處理 ,它假設你用標準的C++庫進行這些處理;不支持ODBC——這個世界正在轉移到基于COM的不需要包裝的數據存取方式;不支持WinSock打包類--sockets本身也是新的東西;ATL也不支持完整的Win32 API打包類——ATL2.0的實現機制提供了對話框和WndProcs支持。此外ATL中沒有MFC中的文檔/視圖模型。取而代之的是ATL那更具伸縮性和靈活 性的通過COM接口(如ActiveX控件)與基于UI的對象之間的溝通模式。
            ??? 使用正確的工具非常關鍵。如果你正在編寫一個不可見的COM組件,那么ATL與MFC比起來,從開發效率,可伸縮性,運行時性能以及可執行文件大小各方面來看,ATL可能 都是最好的選擇。對于現代基于ActiveX控件的用戶界面,ATL所產生的代碼也比MFC更小更快。另一方面,與MFC的類向導相比,ATL需要更多的COM知識。ATL與STL一樣,對于單層應用沒什么幫助,而MFC在這方面保持著它的優勢。
            ??? ATL的設計在很大程度上來自STL的靈感,STL與所有ANSI/ISO兼容的C++編譯器一起已經被納入成為標準C++庫的一部分。像STL一樣,ATL大膽使用C++模板。模板是C++中眾多具有爭議的特性之一。每每使用不當都會導致執行混亂,降低性能 和難以理解的代碼。明智地使用模板所產生的通用性效果和類型安全特性則是其它方法所望塵莫及的。ATL與STL一樣陷入了兩個極端。幸運的是 在L大膽使用C++模板的同時,編譯器和鏈接器技術也在以同樣的步伐向前發展。為當前和將來的開發進行STL和ATL的合理選擇。
            ??? 盡管模板在內部得到廣泛的使用,但是在用ATL技術時,你不用去敲入或關心那些模板中的尖括弧。因為ATL本身帶有ATL對象向導(參見圖一):






            圖一 ATL 對象向導


            ??? 對象向導產生大量基于ATL模板類缺省的對象實現代碼(即框架代碼)。這些缺省的對象類型如附表一所列。ATL對象向導允許任何人 快速建立COM對象并且在分分鐘之內讓它運行起來,不用去考慮COM或ATL的細節問題。當然,為了能充分駕馭ATL,你必須掌握C++,模板和COM編程技術。對于大型的對象類,只要在ATL對象向導所產生的缺省實現(框架代碼)中加入方法實現來輸出定制接口,這也是大多數開發人員開始實現COM對象時的重點所在。
            ??? 初次接觸ATL時,其體系結構給人的感覺是神秘和不可思議。HelloATL是一個最簡單的基于ATL的進程內服務器源代碼 以及用SDK(純粹用C++編寫)實現的同樣一個進程內服務器源代碼。在真正構建出一個COM組件之前,代碼需要經過反反復復多次斟酌和修改。對于想加速開發COM組件速度的主流組件開發人員來說,ATL體系結構并不是什么大問題,因為對象向導產生了所需要的全部框架代碼,只 要你加入方法定義即可。對于認真的COM開發人員和系統編程人員來說,ATL提供了一個用C++建立COM組件的高級的,可擴展的體系結構。一旦你理解和掌握了這個體系結構并能駕馭對象向導,你就會看到ATL的表現能力和強大的功能 ,它完全可以和原始的COM編程技術媲美。
            ??? 另外一個使用ATL開發COM組件的理由是Visual C++ 5.0+集成開發環境(IDE)對ATL的高度支持。 微軟在Visual C++ 5.0+中將ATL所要用到的接口定義語言(IDL)集成到了C++編輯器中。步篇

            ??? 在本文的第一部分,我們簡要介紹了ATL的一些背景知識以及ATL所面向的開發技術和環境。在這一部分 將開始走進ATL,講述ATL編程的基本方法、原則和必須要注意的問題。
            ??? 理解ATL最容易的方法是考察它對客戶端編程的支持。對于COM編程新手而言,一個棘手的主要問題之一是正確管理接口指針的引用計數。COM的引用計數法則是沒有運行時強制 性的,也就是說每一個客戶端必須保證對對象的承諾。
            ??? 有經驗的COM編程者常常習慣于使用文檔中(如《Inside OLE》)提出的標準模式。調用某個函數或方法,返回接口指針,在某個時間范圍內使用這個接口指針,然后釋放它。下面是使用這種模式的代碼例子:
            void f(void) {
               IUnknown *pUnk = 0;
               // 調用 
               HRESULT hr = GetSomeObject(&pUnk);
               if (SUCCEEDED(hr)) {
               // 使用
                 UseSomeObject(pUnk);
             // 釋放
                 pUnk->Release();
               }
            }
                
            ??? 這個模式在COM程序員心中是如此根深蒂固,以至于他們常常不寫實際使用指針的語句,而是先在代碼塊末尾敲入Release語句。這很像C程序員使用switch語句時的條件反射一樣,先敲入break再說。
            ??? 其實調用Release實在不是什么可怕的負擔,但是,客戶端程序員面臨兩個相當嚴重的問題。第一個問題與獲得多接口指針有關。如果某個函數需要在做任何實際工作之前獲得三個接口指針,也就是說在第一個使用指針的語句之前必須要由三個調用語句。在書寫代碼時,這常常意味著程序員需要寫許多嵌套條件語句,如:
            void f(void) {
              IUnknown *rgpUnk[3];
              HRESULT hr = GetObject(rgpUnk);
              if (SUCCEEDED(hr)) {
                hr = GetObject(rgpUnk + 1);
                if (SUCCEEDED(hr)) {
                  hr = GetObject(rgpUnk + 2);
                  if (SUCCEEDED(hr)) {
                    UseObjects(rgpUnk[0], rgpUnk[1],
                                 rgpUnk[2]);
                    rgpUnk[2]->Release();
                  }
                  rgpUnk[1]->Release();
                }
                rgpUnk[0]->Release();
              }
            }
                
            ??? 像這樣的語句常常促使程序員將TAB鍵設置成一個或兩個空格,甚至情愿使用大一點的顯示器。但事情并不總是按你想象的那樣,由于種種原因項目團隊中的COM組件編程人員往往得不到 所想的硬件支持,而且在公司確定關于TAB鍵的使用標準之前,程序員常常求助于使用有很大爭議但仍然有效的“GOTO”語句:
            void f(void) {
               IUnknown *rgpUnk[3];
               ZeroMemory(rgpUnk, sizeof(rgpUnk));
               if (FAILED(GetObject(rgpUnk))) 
                 goto cleanup;
               if (FAILED(GetObject(rgpUnk+1))) 
                 goto cleanup;
               if (FAILED(GetObject(rgpUnk+2))) 
                 goto cleanup;
             
               UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]);
             
             cleanup:
               if (rgpUnk[0]) rgpUnk[0]->Release();
               if (rgpUnk[1]) rgpUnk[1]->Release();
               if (rgpUnk[2]) rgpUnk[2]->Release();
            }    
            這樣的代碼雖然不那么專業,但至少減少了屏幕的水平滾動。
            使用以上這些代碼段潛在著更加棘手的問題,那就是在碰到C++異常時。如果函數UseObjects丟出異常,則釋放指針的代碼被完全屏蔽掉了。 解決這個問題的一個方法是使用Win32的結構化異常處理(SEH)進行終結操作:
            void f(void) {
               IUnknown *rgpUnk[3];
               ZeroMemory(rgpUnk, sizeof(rgpUnk));
               __try {
                if (FAILED(GetObject(rgpUnk))) leave;
                if (FAILED(GetObject(rgpUnk+1))) leave;
                if (FAILED(GetObject(rgpUnk+2))) leave;
             
                UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]);
               } __finally {
                if (rgpUnk[0]) rgpUnk[0]->Release();
                if (rgpUnk[1]) rgpUnk[1]->Release();
                if (rgpUnk[2]) rgpUnk[2]->Release();
            }
            
            ??? 可惜Win32 SHE在C++中的表現并不如想象得那么好。較好的方法是使用內建的C++異常處理模型,同時停止使用沒有加工過的指針。標準C++庫有一個類:auto_ptr,在其析構函數中定 死了一個操作指針的delete調用(即使在出現異常時也能保證調用)。與之類似,ATL有一個COM智能指針,CComPtr,它的析構函數會正確調用Release。
            ??? CComPtr類實現客戶端基本的COM引用計數模型。CComPtr有一個數據成員,它是一個未經過任何加工的COM接口指針。其類型被作為模板參數傳遞:
                  CComPtr<IUnknown> unk;
                  CComPtr<IClassFactory> cf;
            
            ??? 缺省的構造函數將這個原始指針數據成員初始化為NULL。智能指針也有構造函數,它的參數要么是原始指針,要么是相同類型的智能參數。不論哪種情況,智能指針都調用AddRef控制引用。CComPtr的賦值操作符 既可以處理原始指針,也可以處理智能指針,并且在調用新分配指針的AddRef之前自動釋放保存的指針。最重要的是,CComPtr的析構函數釋放保存的接口(如果非空)。請看下列代碼:
            void f(IUnknown *pUnk1, IUnknown *pUnk2) {
                // 如果非空,構造函數調用pUnk1的AddRef 
                CComPtr unk1(pUnk1);
                // 如果非空,構造函數調用unk1.p的AddRef
                CComPtr unk2 = unk1;
                // 如果非空,operator= 調用unk1.p的Release并且
                //如果非空,調用unk2.p的AddRef
                unk1 = unk2;
                //如果非空,析構函數釋放unk1 和 unk2
            }
            
            ??? 除了正確實現COM的AddRef 和 Release規則之外,CComPtr還允許實現原始和智能指針的透明操作,參見附表二所示。也就是說下面的代碼按照你所想象的方式運行:
            void f(IUnknown *pUnkCO) {
                
                CComPtr cf;
                
                HRESULT hr;
                
                // 使用操作符 & 獲得對 &cf.p 的存取
                hr = pUnkCO->QueryInterface(IID_IClassFactory,(void**)&cf);
                if (FAILED(hr)) throw hr;
                
                CComPtr unk;
                
                // 操作符 -> 獲得對cf.p的存取
                // 操作符 & 獲得對 &unk.p的存取
                hr = cf->CreateInstance(0, IID_IUnknown, (void**)&unk);
                
                if (FAILED(hr)) throw hr;
                
                // 操作符 IUnknown * 返回 unk.p
                UseObject(unk);
                
                // 析構函數釋放unk.p 和 cf.p
            }
            
            ??? 除了缺乏對Release的顯式調用外,這段代碼像是純粹的COM代碼。有了CComPtr類的武裝,前面所遇到的麻煩問題頓時變得簡單了:
            void f(void) {
            CComPtr<IUnknown> rgpUnk[3];
            if (FAILED(GetObject(&rgpUnk[0]))) return;
            if (FAILED(GetObject(&rgpUnk[1]))) return;
            if (FAILED(GetObject(&rgpUnk[2]))) return;
            UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]);
            }
            由于CComPtr對操作符重載用法的擴展,使得代碼的編譯和運行無懈可擊。
            ??? 假定模板類知道它所操縱的指針類型,你可能會問:那為什么智能指針不能在它的功能操作符或構造函數中自動調用QueryInterface,從而更有效地包裝IUnknown呢?在Visual C++ 5.0出來以前,沒有辦法將某個接口的GUID與它的本身的C++類型關聯起來——Visual C++ 5.0用私有的declspec將某個IID與一個接口定義綁定在一起。因為ATL的設計 考慮到了它要與大量不同的C++編譯器一起工作,它需要用與編譯器無關的手段提供GUID。下面我們來探討另一個類——CComQIPtr類。
            ??? CComQIPtr與CComPtr關系很密切(實際上,它只增加了兩個成員函數)。CComQIPtr必須要兩個模板參數:一個是被操縱的指針類型 ,另一個是對應于這個指針類型的GUID。例如,下列代碼聲明了操縱IDataObject 和IPersist接口的智能指針:
                  CComQIPtr<IDataObject, &IID_IDataObject> do;
                  CComQIPtr<IPersist, &IID_IPersist> p;
            
            ??? CComQIPtr的優點是它有重載的構造函數和賦值操作符。同類版本(例如,接受相同類型的接口)僅僅AddRef右邊的賦值/初始化操作。這實際上就是CComPtr的功能。異類版本(接受類型不一致的接口)正確調用QueryInterface來決定是否這個對象確實支持所請求的接口:
              
                  void f(IPersist *pPersist) {
                      CComQIPtr<IPersist, &IID_IPersist> p;
                      // 同類賦值 - AddRef''s
                      p = pPersist;
                
                      CComQIPtr<IDataObject, &IID_IDataObject> do;
                      // 異類賦值 - QueryInterface''s
                      do = pPersist;
                  }
             
            ??? 在第二種賦值語句中,因為pPersist是非IDataObject *類型,但它是派生于IUnknown的接口指針,CComQIPtr通過pPersist調用QueryInterface來試圖獲得這個對象的IDataObject接口指針。如果QueryInterface調用成功,則此智能指針將含有作為結果的原始IDataObject指針。如果QueryInterface調用失敗,則do.p將被置為null。如果QueryInterface返回的HRESULT值很重要,但又沒有辦法從賦值操作獲得其值時,則必須顯式調用QueryInterface。
            ??? 既然有了CComQIPtr,那為什么還要CComPtr呢?由幾個理由:首先,ATL最初的發布版本只支持CComPtr,所以它就一直合法地保留下來了。其二(也是最重要的理由),由于重載的構造函數和賦值操作,對IUnknown使用CComQIPtr是非法的。因為所有COM接口的類型定義都必須與IUnknown兼容。
                  CComPtr<IUnknown> unk;
             
            從功能上將它等同于
                  CComQIPtr<IUnknown, &IID_IUnknown> unk;
             
            前者正確。后者是錯誤的用法。如果你這樣寫了,C++編譯器將提醒你改正。
            ??? 將CComPtr作為首選的另外一個理由可能是一些開發人員相信靜悄悄地調用QueryInterface,沒有警告,削弱了C++系統的類型。畢竟,C++在沒有進行強制類型轉換的情況下不允許對類型不一致的原始指針 進行賦值操作,所以為什么要用智能指針的道理也在這,幸運的是開發人員可以選擇最能滿足需要的指針類型。
            ??? 許多開發人員將智能指針看成是對過于的復雜編程任務的簡化。我最初也是這么認為的。但只要留意它們使用COM智能指針的方法。就會逐漸認識到它們引入的潛在危險與它們解決的問題一樣多。
            關于這一點,我用一個現成的使用原始指針的函數為例:
             
            void f(void) {
               IFoo *pFoo = 0;
               HRESULT hr = GetSomeObject(&pFoo);
               if (SUCCEEDED(hr)) {
                  UseSomeObject(pFoo);
                  pFoo->Release();
               }
            } 
            將它自然而然轉換到使用CComPtr。
              void f(void) {
               CComPtr<IFoo> pFoo = 0;
               HRESULT hr = GetSomeObject(&pFoo);
               if (SUCCEEDED(hr)) {
                  UseSomeObject(pFoo);
                  pFoo->Release(); 
               }
            }
             
            ??? 注意CComPtr 和 CComQIPtr輸出所有受控接口成員,包括AddRef和Release。可惜當客戶端通過操作符->的結果調用Release時,智能指針很健忘 ,會二次調用構造函數中的Release。顯然這是錯誤的,編譯器和鏈接器也欣然接受了這個代碼。如果你運氣好的話,調試器會很快捕獲到這個錯誤。
            ??? 使用ATL智能指針的另一個要引起注意的風險是類型強制轉換操作符對原始指針提供的訪問。如果隱式強制轉換操作符的使用存在爭議。當 ANSI/ISO C++ 委員會在決定采用某個C++串類時,他們明確禁止隱式類型轉換。而是要求必須顯式使用c_str函數在需要常量char *(const char *)的地方傳遞標準C++串。ATL提供了一種隱含式的類型轉換操作符順利地解決了這個問題。通常,這個轉換操作符可以根據你的喜好來使用,允許你將智能指針傳遞到需要用原始指針的函數。
             void f(IUnknown *pUnk) { 
                CComPtr unk = pUnk;
                // 隱式調用操作符IUnknown *()
                CoLockObjectExternal(unk, TRUE, TRUE);
            }
             
            這段代碼能正確運行,但是下面的代碼也不會產生警告信息,編譯正常通過:
            HRESULT CFoo::Clone(IUnknown **ppUnk) { 
               CComPtr unk;
               CoCreateInstance(CLSID_Foo, 0, CLSCTX_ALL,
               IID_IUnknown, (void **) &unk);
               // 隱式調用操作符IUnknown *()
               *ppUnk = unk;
               return S_OK;
            }
            
            ??? 在這種情況下,智能指針(unk)對原始值針**ppUnk的賦值觸發了與前面代碼段相同的強制類型轉換。在第一個例子中,不需要用AddRef。在第二個例子中,必須要用AddRef。
            ??? 有關使用智能指針的更詳細一般信息,請參見Scott Meyer的《More Effective C++》(Addison-Wesley, 1995年出版)。國內目前還沒有這本書的中譯本或影印本。有關COM智能指針的更多特定信息,請參見Don Box的一篇關于智能指針的專題文章

            ?第一部分:為什么要使用ATL。
            ??? 第二部分:起步篇。

            實現IUnknown

            ??? 用純粹的C++實現IUnknown相對來說比較簡單。IUnknown實現之間的主要差別重點在于QueryInterface中將給出哪些接口。請看下列接口定義:
            interface IMessageSource : IUnknown {
               HRESULT GetNextMessage([out] OLECHAR **ppwsz);
            }
            interface IPager : IUnknown {
               HRESULT SendMessage([in] const OLECHAR *pwsz);
            }
            interface IPager2 : IPager {
               HRESULT SendUrgentMessage(void);
            }
            
            這些C++類定義實現了三個接口:
             class CPager : public IMessageSource, public IPager2 {
               LONG m_dwRef;
             public:
               CPager(void) :    
                m_dwRef(0) {}
               virtual ~CPager(void) {}
               STDMETHODIMP 
               QueryInterface(REFIID,  
                              void**);
               STDMETHODIMP_(ULONG) 
               AddRef(void);
               STDMETHODIMP_(ULONG) 
               Release(void);
               STDMETHODIMP GetNextMessage(OLECHAR **ppwsz);
               STDMETHODIMP SendMessage(const COLECHAR * pwsz);
               STDMETHODIMP SendUrgentMessage(void);
            };
            
            ?? 如果在堆中創建對象(也就是說用new操作符在內部創建)并且只用單線程公寓(STA)模式運行,下面是合理的AddRef 和Release實現:
            STDMETHODIMP_(ULONG) CPager::AddRef() {
               return ++m_dwRef; 
            }
            STDMETHODIMP_(ULONG) CPager::Release(){
               ULONG result = -m_dwRef;
               if (result == 0)
                 delete this;
               return result;
            }
            
            ??? 如果輸出的對象是以多線程公寓(MTA)模式運行,則++和--操作符就必須用Win32的原子增量和減量(Increment/Decrement)例程調用來代替:
            STDMETHODIMP_(ULONG) CPager::AddRef() {
               return InterlockedIncrement(&m_dwRef); 
            }
             STDMETHODIMP_(ULONG) CPager::Release(){
               ULONG result = InterlockedDecrement(&m_dwRef);
               if (result == 0)
                 delete this;
               return result;
            }
            
            無論哪一種線程模式,下面的QueryInterface實現都是正確的:
            STDMETHODIMP CPager::QueryInterface(REFIID riid, void **ppv) {
               if (riid == IID_IUnknown)
                 *ppv = (IMessageSource*)this;
               else if (riid == IID_IMessageSource)
                 *ppv = (IMessageSource*)this;
               else if (riid == IID_IPager)
                 *ppv = (IPager*)this;
               else if (riid == IID_IPager2)
                 *ppv = (IPager2*)this;
               else
                 return (*ppv = 0), E_NOINTERFACE;
               ((IUnknown*)*ppv)->AddRef();
               return S_OK; 
            }
            
            QueryInterface的最后四行代碼對所有的對象都一樣。其余的部分則根據這個對象類型層次上的類不同而有所不同。
            ??????? 如果IUnknown的實現能形成規律,那么ATL便可以提供一種機制將這些具有共性的程序語句從代碼中提取出來。實際上ATL做到了這一點,方法是通過提供靈活和可擴展的類層次,使得開發人員只要正確地說明所使用的類集,就可確定線程,服務器鎖定和對象生命期行為。
            ????? 如果看一看ATL實現的IUnknown類層次,你碰到的第一個參數化行為就是線程。ATL允許你構造被優化的對象和服務器,在相互轉換STA和MTA的用法時,不用修改源代碼。缺省情況下,ATL工程構造的是安全的MTA(MTA-safe)對象,除了要具備構造單STA對象所需的一切外,還需要附加代碼和狀態管理。通過定義適當的預處理指令,你能改變缺省的線程行為,從而支持單STA或多個基于STA的工程。
            使用如下這個指令:
                  /D _ATL_SINGLE_THREADED
            來編譯工程可以改變服務器缺省的線程模型,讓它只支持一個基于STA的線程。它適合于進程外的且不創建自擁有線程的基于STA的服務器情況,當你用這個選項時,所有對ATL全局狀態的存取將都是不加鎖的,并發的。盡管此選項似乎很有效,但它實質上限制了ATL服務器只能是一個單線程的。
            使用如下這個指令:
                  /D _ATL_APARTMENT_THREADED
            來編譯工程可以改變服務器缺省的線程模型支持多個基于STA的線程。它適合于建立注冊表項ThreadingModel=Apartment的進程內服務器。如果要創建基于STA的進程外服務器且還要建立附加的基于STA的線程,那么這個指令也是必須的。使用這個選項導致ATL用能安全存取線程的臨界區來保護它的全局狀態。
            使用如下這個指令:
                  /D _ATL_FREE_THREADED
            可以創建與任何線程環境兼容的服務器。也就是說ATL的全局狀態將在臨界區中被鎖定,并且每個對象將擁有它自己的私有臨界區來保護它的實例狀態。如果沒有定義這些指令,則ATL頭文件假設為使用_ATL_FREE_THREADED。
            ??? 為了在ATL中正確使用線程,必須理解ATL的線程模型概念。ATL定義了三種線程模型類來實現線程安全行為所需的少數內聯操作。每一種線程模型都輸出兩個靜態函數,Increment 和Decrement。它們都有一個長整指針參數,并且對指定的線程模型實現最快的,合法的增減操作。每一種線程模型都輸出兩個嵌套類型定義(typedefs),即AutoCriticalSection 和CriticalSection,它們要么打包Win32的CRITICAL_SECTION,要么就是出于兼容性考慮的空類,沒有實際實現。記住,所用臨界區實際使用的實際類型依賴于特定的線程模型。
            ??? ATL實現的三種線程模型分別是CComMultiThreadModel,CComSingleThreadModel和 CComMultiThreadModelNoCS。CComMultiThreadModel使用InterlockedIncrement/InterlockedDecrement和實的CRITICAL_SECTIONS。CComSingleThreadModel使用更有效的++和-操作符及虛的CRITICAL_SECTIONS。
            混合的CComMultiThreadModelNoCS除了使用虛的CRITICAL_SECTIONS外,還有InterlockedIncrement/InterlockedDecrement。第三種模型對于存在于MTAs中,但不需要任何數據成員的鎖定的對象很有用。
            ??? ATL提供了兩個類型定義,CComObjectThreadModel 和 CComGlobalsThreadModel,通過條件編譯來保證對象和全局變量各自的效率及行為安全。依據所定義的三種預編譯指令之一,每一類型名對應著以上描述的三種線程模型類之一。下表說明了這種對應關系,它依賴于ATL所使用的預處理指令。
            ATL類型定義_ATL_SINGLE_THREADED_ATL_APARTMENT_THREADED_ATL_FREE_THREADED
            CComGlobalsThreadModelCComSingleThreadModelCComMultiThreadModelCComMultiThreadModel
            CComObjectThreadModelCComSingleThreadModelCComSingleThreadModelCComMultiThreadModel

            只要給定了上述的線程模型類型層次,你就能將相應的參數化線程行為添加到任何COM類。請看下列代碼:

              參數化的線程
            
             class CPager : public IPager {
               LONG m_dwRef;
               typedef CComObjectThreadModel _ThreadModel;
               _ThreadModel::CComAutoCriticalSection m_critsec;
                 :   :   :   :
               STDMETHODIMP_(ULONG) CPager::AddRef() {
                 return _ThreadModel::Increment(&m_dwRef); 
               }
               STDMETHODIMP_(ULONG) CPager::Release(){
                 ULONG res = _ThreadModel::Decrement(&m_dwRef);
                 if (res == 0)
                   delete this;
                 return res;
               }
               STDMEHTHODIMP SendUrgentMessage() {
             // 保證只有一個線程  
                 m_critsec.Lock(); 
             // 實現任務
                 this->GenerateMessage();
                 this->WakeUpUser();
             // 允許其它線程
                 m_critsec.Unlock();
                 return S_OK;
               }
             }; 
            使用缺省選項(_ATL_FREE_THREADED)的編譯則將一個實臨界區添加到對象,并執行Lock和Unlock方法將內聯調用映射到EnterCriticalSection/LeaveCriticalSection API函數。同時,AddRef和Release方法將使用InterlockedIncrement/InterlockedDecrement來安全地改變這個對象的引用計數。
            ????? 如果前面的代碼是用_ATL_APARTMENT_THREADED 或者 _ATL_SINGLE_ THREADED選項編譯的,則m_critsee數據成員將為空,Lock和Unlock內聯例程將變成虛操作,并且AddRef和Release方法將使用++和--操作符。如果這個對象是一個ActiveX控件且其線程模型為Apartment (ThreadingModel=Apartment)的進程內服務器,則這將是小而快的代碼。
            ????? 有時構造以MTA模式(即它的AddRef和Release必須是線程安全的)運行,但不需要附加鎖定的對象很有用。對于這種類型的對象,混合型的CComMultiThreadModelNoCS很適合。
            通過將類的類型定義從:
                  typedef CComObjectThreadModel _ThreadModel;
            細化到
                  typedef CComMultiThreadModelNoCS _ThreadModel;
            那么針對每一個對象,你不必付出CRITICAL_SECTION的開銷(CComAutoCriticalSection 會映射到 CComFakeCriticalSection)就可以得到線程安全的AddRef和Release(將Increment 和 Decrement方法映射到InterlockedIncrement和InterlockedDecrement)。

            實現接口

            ??? 現在你已經積累了一些關于ATL線程模型方面的知識,下面我們來討論ATL如何實現IUnknown。ATL最不直觀的(同時也是最強大的)一個方面就是你要實現的類事實上都是不能被直接實例化的抽象類。實現一個從通用的IUnknown派生的C++類。但是在確定對象的運行環境之前,QueryInterface,AddRef 和 Release是不會有實質性代碼的。這種靈活性使開發人員能實現對象的關鍵功能,如COM的聚合支持,tear-offs,堆和棧分配,服務器鎖定等等。下圖展示了一個典型的基于ATL的類層次。

            ?????????????????????????? 圖四 典型的基于ATL的類層次

            從下面的代碼可以看出,ATL中實現IUnknown的關鍵在于CComObjectRootBase 和 CComObjectRootEx。
            ?? CComObjectRoot?


            ?class CComObjectRootBase {
            ?public:
            ?// C++ 構造函數
            ?? CComObjectRootBase() { m_dwRef = 0L; }
            ?
            ?// ATL 偽構造函數和偽析構函數
            ?? HRESULT FinalConstruct() { return S_OK; }?
            ?? void FinalRelease() {}
            ?
            ?// 內部Unknown函數(由派生類提供的InternalAddRef/Release)
            ?? static HRESULT WINAPI InternalQueryInterface(void* pThis,?
            ???????????? const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject) {
            ???? HRESULT hRes = AtlInternalQueryInterface(pThis,pEntries,iid,ppvObject);
            ???? return _ATLDUMPIID(iid, pszClassName, hRes);
            ?? }
            ?
            ?// 外部Unknown函數
            ?? ULONG OuterAddRef() { return m_pOuterUnknown->AddRef(); }
            ?? ULONG OuterRelease() { return m_pOuterUnknown->Release(); }
            ?? HRESULT OuterQueryInterface(REFIID iid, void ** ppvObject)?
            ?? { return m_pOuterUnknown->QueryInterface(iid, ppvObject); }
            ?
            ?// ATL 創建者的鉤子例程
            ?? void SetVoid(void*) {}
            ?? void InternalFinalConstructAddRef() {}
            ?? void InternalFinalConstructRelease() {}
            ?
            ?// ATL 接口映射輔助函數
            ?? static HRESULT WINAPI _Break(?????? void*, REFIID, void**, DWORD);
            ?? static HRESULT WINAPI _NoInterface( void*, REFIID, void**, DWORD);
            ?? static HRESULT WINAPI _Creator(???? void*, REFIID, void**, DWORD);
            ?? static HRESULT WINAPI _Delegate(??? void*, REFIID, void**, DWORD);
            ?? static HRESULT WINAPI _Chain(?????? void*, REFIID, void**, DWORD);
            ?? static HRESULT WINAPI _Cache(?????? void*, REFIID, void**, DWORD);
            ?
            ?// 實際的引用計數或者指針返回到真實的Unknown
            ?? union {
            ???? long m_dwRef;
            ???? IUnknown* m_pOuterUnknown;
            ?? };
            ?};
            ?
            ?template <class ThreadModel>
            ?class CComObjectRootEx : public CComObjectRootBase {
            ?public:
            ?? typedef ThreadModel _ThreadModel;
            ?? typedef _ThreadModel::AutoCriticalSection _CritSec;
            ?
            ?// 內部 Unknown 函數(InternalQueryInterface 由 CComObjectRootBase提供)
            ?? ULONG InternalAddRef()? { return _ThreadModel::Increment(&m_dwRef); }
            ?? ULONG InternalRelease()??????? { return _ThreadModel::Decrement(&m_dwRef); }
            ?
            ?// 對象級的鎖定操作
            ?? void Lock() {m_critsec.Lock();}
            ?? void Unlock() {m_critsec.Unlock();}
            ?private:
            ?? _CritSec m_critsec;
            ?};

            這兩個類提供了三個方法:OuterQueryInterface,OuterAddRef 和 OuterRelease,它們被用來將IUnknown的功能委派給外部實現。當實現COM的聚合和tear-offs時要用到這些方法。其它三個方法--InternalQueryInterface,InternalAddRef和 InternalRelease的作用是實現本身的引用計數以及對象接口的查詢或導航。
            ??? CComObjectRootEx是個模板類,允許你針對這個類指定使用哪種ATL線程模型。(如果你想要進行條件編譯,則使用CComObjectRoot就可以了,它是一個針對CComObjectRootEx<CComObjectThreadModel>的類型定義。)CComObjectRootEx從CComObjectRootBase中派生其大多數功能,它是個相當袖珍的類,只包含一個聯合類型的數據成員:
             union {
               long m_dwRef; 
               IUnknown *m_pOuterUnknown;
             }; 
            ? 根據使用這個類的實際方式,聯合中的成員將被用于保存給定類實例的生命周期。大多數情況下要用到m_dwRef,m_pOuterUnknown只有在支持聚合或tear-offs時用到。CComObjectRootBase提供了OuterQueryInterface,OuterAddRef和OuterRelease方法,通過m_pOuterUnknown成員轉發IUnknown請求。
            ? 反過來,CComObjectRootEx提供InternalAddRef 和InternalRelease方法根據模板參數傳遞的線程模型來實際增減m_dwRef變量得值。注意這些例程只是增減這個變量,而沒有真正刪除這個對象。這是因為此對象的分配策略將由派生類中提供,派生類將使用這些例程來調整引用計數。
            ? CComObjectRoot層次最引人注目的是它的QueryInterface實現函數,它被作為CComObjectRootBase的方法(InternalQueryInterface)輸出:
            static HRESULT WINAPI 
             CComObjectRootBase::InternalQueryInterface(void *pThis, 
                                  const _ATL_INTMAP_ENTRY *pEntries, 
                                  REFIID riid, void **ppv);
                  
                 
            使用ATL實現IUnknown的每一個類必須制定一個接口映射來提供InternalQueryInterface。ATL的接口映射是IID/DWORD/函數指針數組,它指示當QueryInterface請求一個給定的IID時要采取什么樣的行動。其類型都是_ATL_INTMAP_ENTRY。
            struct _ATL_INTMAP_ENTRY {
               const IID* piid;  // 接口ID (IID)
               DWORD dw;         // 多用途值
               HRESULT (*pFunc)(void*, REFIID, void**, DWORD); 
            };      
            

            這個結構的第三個成員pFunc的取值有三種情況。如果pFunc等于常量_ATL_SIMPLEMAPENTRY,則結構成員dw為對象偏移量,這時不需要函數調用,并且InternalQueryInterface完成下列操作:

             (*ppv = LPBYTE(pThis) + pEntries[n].dw)->AddRef();
            
            這個偏移量的初始化通常參照基類接口的偏移量。如果pFunc非空且不等于_ATL_SIMPLEMAPENTRY,則它指向的函數將被調用,將這個指針作為第一個參數傳遞給對象而第二個參數是多用途值dw。
            return pEntries[n].pFunc(pThis, riid, ppv, 
                                      pEntries[n].dw);
            
            這個接口映射的最后一個入口將使用pFunc值,指示映射的結束。 如果沒有在映射中發現任何接口則InternalQueryInterface 會返回E_NOINTERFACE。 接口映射通常為ATL的接口映射宏。ATL提供17個不同的宏,它們支持大多數用于實現接口的普通技術(多繼承,嵌套類,聚合或者tear-offs。)這些宏及其相應的原始代碼請參見附表三。下面是一個使用CComObjectRootEx和接口映射實現IPager2 和IMessageSource類的例子:
            class CPager 
              : public IMessageSource, public IPager2,
                public CComObjectRootEx<CComMultiThreadModel>{ 
             public:
               CPager(void) {}
               virtual ~CPager(void) {}
             
             BEGIN_COM_MAP(CPager)
               COM_INTERFACE_ENTRY(IMessageSource)
               COM_INTERFACE_ENTRY(IPager2)
               COM_INTERFACE_ENTRY(IPager)
             END_COM_MAP()
             
               STDMETHODIMP GetNextMessage(OLECHAR **ppwsz);
               STDMETHODIMP SendMessage(const COLECHAR * pwsz);
               STDMETHODIMP SendUrgentMessage(void);
             };
            
            前面的代碼產生的接口映射如下:
            { &IID_IMessageSource, 0, _ATL_SIMPLEMAPENTRY },
            { &IID_IPager2, 4, _ATL_SIMPLEMAPENTRY },
              { &IID_IPager, 4, _ATL_SIMPLEMAPENTRY},
              { 0, 0, 0 }
            
            ?? 在建立接口映射時,ATL假設映射中第一個入口是個簡單映射入口并用它來滿足對IID_IUnknown.的請求。 除了支持IUnknown外,ATL提供大量缺省的COM接口實現。ATL用一個簡單的命名規范來為這些實現命名,它們大多數都是作為模板類來實現的,帶有一個模板參數,而這些模板參數才是是既要實現的類。 一個簡單的例子是IObjectWithSite接口,它一般用于為某個對象提供一個指向激活現場的指針。ATL為這個指針提供了一個缺省的實現:IObjectWithSiteImpl。此類提供了一個IObjectWithSite兼容的二進制簽名并且實現了所有的IObjectWithSite方法。為了使用ATL內建的實現,你只需要添加基類實現(用適當的模板參數),然后在接口映射中添加一個入口輸出經過QueryInterface實現的接口。 例如,為了使用ATL的IObjectWithSite實現,按照如下步驟來做:
             class CPager 
              : public CComObjectRootEx,
                public IPager,
                public IObjectWithSiteImpl
              {
             public:
             BEGIN_COM_MAP(CPager)
               COM_INTERFACE_ENTRY(IPager)
               COM_INTERFACE_ENTRY_IMPL(IObjectWithSite)
             END_INTERFACE_MAP()
               STDMETHODIMP SendMessage(const COLECHAR * pwsz);
             };
            由于使用了ATL內建的實現類,也就有了COM_INTERFACE_ENTRY_IMPL宏。之所以要用只個宏是因為許多ATL的缺省實現不從它們實現的接口派生。這樣的話就導致標準的COM_ INTERFACE_ENTRY宏返回不正確的偏移量。例如,因為CPager不從IObjectWithSite派生,用于計算偏移量的強制類型轉換就不會在對象中反映,而是用起始位置代替。 在這個例子中,IObjectWithSiteImpl沒有基類。而是按照在IObjectWithSite中一樣的順序聲明它的虛函數,產生全兼容的vtable(虛表)結構。ATL使用這個有點不可思議的技術,原因是它允許缺省實現支持接口的引用計數,這一點使用常規多繼承技術是很難做到的。 IDispatchImpl也是一個常用的ATL缺省實現。這個類實現用于雙接口的四個IDispatch方法,由你的類實現IDispatch::Invoke所不能完成的方法。不像大多數其它的ATL實現,這個類實際上是從一個COM接口派生的,有幾個模板參數:
             template <
               class T,            // 雙接口
               const IID* piid,    // 雙接口IID
               const GUID* plibid, // 包含類型庫TypeLib
               WORD wMajor = 1,    // 類型庫的版本
               WORD wMinor = 0,    //類型庫的版本
               class tihclass = CComTypeInfoHolder
             >
             class IDispatchImpl : public T { ... };
            
            假設兩個接口是DIPager 和 DIMessageSource。這個類的使用如下:
            class CPager 
             : public CComObjectRootEx<CComMultiThreadModel>,
               public IDispatchImpl<DIMessageSource,
                        &IID_DIMessageSource, &LIBID_PagerLib>,
               public IDispatchImpl<DIPager,
                        &IID_DIPager, &LIBID_PagerLib>
             {
             public:
             BEGIN_COM_MAP(CPager)
               COM_INTERFACE_ENTRY(DIMessageSource)
               COM_INTERFACE_ENTRY(DIPager)
             // 下一個接口指定DIPager為缺省 [default]
               COM_INTERFACE_ENTRY2(IDispatch, DIPager)
             END_INTERFACE_MAP()
               STDMETHODIMP SendMessage(BSTR pwsz);
               STDMETHODIMP GetNextMessage(BSTR *ppwsz);
             };
            ATL的第一個版本使用CComDualImpl名字,現在它只是IDispatchImpl預處理的一個別名,以便允許1.x版本和2.x版本的工程能在一起編譯。

            不要過分抽象

            ??? ATL最不直觀的一個方面是你所定義和實現的C++類仍然是抽象基類。沒錯,在ATL的模板類和宏上辛苦了半天,卻仍然得不到一個可以實例化的類。即使你從 CComObjectRootEx 派生,其結果同從一個或更多的ATL接口實現繼承一樣。從技術上講,你的對象不提供 IUnknown 三個核心方法(QueryInterface,AddRef 和 Release)的實現。如果你檢查現有ATL之前的 COM 實現,如果不是全部,那么也是大多數的方法實現并不在乎這個類是不是被用于COM聚合或tear-off,是不是被用于獨立的對象或一個包含在內的數據成員,是不是要作為基于堆的對象或作為全局變量,以及是不是對象存在時,一直要保持服務器運行。為了允許最大限度的靈活性,所有這些方面分別通過ATL家族中的十個類屬的 CComObject 之一來說明。參見下表:

            類名服務器是否加鎖是否代理IUnknown是否刪除對象備注
            CComObjectYesNoYes常規情況
            CComObjectCachedYes(在第二次AddRef之后)NoYes用于通過內部指針控制的對象
            CComObjectNoLockNoNoYes用于不控制服務器運行的對象
            CComObjectGlobalYes(在第一次AddRef之后)NoNo用于全程變量
            CComObjectStackNoNoNo用于不能被增加引用計數的基于堆棧的變量
            CComContainedObjectNoYesNo用于MFC風格的嵌套類
            CComAggObjectYesYesYes僅用于聚合實現
            CComPolyObjectYesYes(如果聚合)Yes用于聚合/非聚合實現
            CComTearOffObjectNoYes(僅用于QueryInterface)Yes用于每次請求所創建的tear-offs?
            CComCachedTearOffObjectNoYes(通過第二個IUnknown)Yes用于在第一次請求和緩存時所創建的tear-offs

            ??? 每一個 CComObject 類用派生來提供正確的 QueryInterface,AddRef 和 Release 實現。如果QueryInterface,AddRef 和 Release的語義正確,則所有 CComObject 類用你的類名作為模板參數并創建一個從你的類派生的新類。在這些類中,CComObjectNoLock 是最容易理解的一個類。請看其代碼:
             template 
             class CComObjectNoLock : public Base {
             public:
                     typedef Base _BaseClass;
                     CComObjectNoLock(void* = NULL){}
                     ~CComObjectNoLock() {m_dwRef = 1L; FinalRelease();}
             
                     STDMETHOD_(ULONG, AddRef)() {return InternalAddRef();}
                     STDMETHOD_(ULONG, Release)() {
                             ULONG l = InternalRelease();
                             if (l == 0)
                                     delete this;
                             return l;
                     }
                     STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject)
                     {return _InternalQueryInterface(iid, ppvObject);}
             };
             
             template 
             class CComObject : public Base {
             public:
                     typedef Base _BaseClass;
                     CComObject(void* = NULL) { _Module.Lock(); }
                     ~CComObject() {m_dwRef = 1L; FinalRelease(); _Module.Unlock();
                     }
             
                     STDMETHOD_(ULONG, AddRef)() {return InternalAddRef();}
                     STDMETHOD_(ULONG, Release)() {
                             ULONG l = InternalRelease();
                             if (l == 0)
                                     delete this;
                             return l;
                     }
                     STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject)
                     {return _InternalQueryInterface(iid, ppvObject);}
                     static HRESULT WINAPI CreateInstance(CComObject** pp);
             };      
                  
            ?? 它假設你的對象將在堆中分配,也就是說最終調用 Release 時,將觸發這個對象的 delete 操作。CComObjectNoLock 假設你的對象不是可聚合的,并且在服務器運行時,對象并不一直存在(因此有后綴 NoLock)。
            ??? 為了在堆中分配基于 ATL 類的 CPager 實例,只要對這個類名進行 CComObject 模板包裝即可:
             IPager *p = new CComObjectNoLock();
                  
            ??? CComObjectNoLock 類從 CPager 派生,并且添加了由 CPager 提供的使用 InternalQueryInterface,InternalAddRef 和 InternalRelease 的 QueryInterface,AddRef 和 Release實現。因為此對象是基于堆的,所以對 delete 的調用將發生在 CComObjectNoLock 類的 Release 實現中,此時InternalRelease 返回零。
            ??? CComObject 類通常被用于基于堆的對象,這個對象只要存在,則服務器一直要運行。與許多 CComObject 家族中的其它類一樣,CComObject提供了一個全程變量,_Module,它有兩個方法,Lock 和 Unlock。這些方法與 MFC 的 AfxOleLockApp 和AfxOleUnLockApp 很相似。CComObject 的構造函數調用_Module 的 Lock 方法,而 CComObject 的析構函數則調用_Module的Unlock 方法。ATL提供了一個 CComModule 類,它以適當的方式為進程內服務器實現了這些方法。當建立進程外服務器時,某個派生類(CExeModule)必須改寫默認的 Lock / Unlock方法,以便以某種適當的方式下掉服務器?;?AppWizard 的 ATL工程自動會有一個類用 PostThreadMessage 終止主線程的消息循環。

            輸出你的類

            ?? 實現了 CComObject ,你就有足夠的條件用 C++ new 操作符創建 COM 對象。不過這樣做沒有什么實用價值,因為畢竟外部客戶端使用 CoCreateInstance 或 CoGetClassObject 創建類實例。也就是說,你必須為每個外部類輸出類對象。幸運的是ATL分別在它的 CComClassFactory 和 CComClassFactory2 類中提供了缺省的 IClassFactory 和 IClassFactory2接口實現。
            ??? CComClassFactory 不是模板驅動類,但其中有一個函數指針作為數據成員,使用這個函數可以創建對象。ATL提供了一個類模板家族,它們都有一個單獨的靜態方法 CreateInstance,由 Creators 調用,Creators 提供正確的語義來從 CComClassFactory 創建基于 CComObjectRoot 的對象。下面的這段代碼展示了缺省的創建機制:CComCreator,它產生一個模板化的類實例,并用 ATL 中標準的 FinalConstruct 來順序初始化對象。

             ATL Creator 
            
            
             template  class CComCreator {
             public:
                 static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, LPVOID* ppv) {
                             HRESULT hRes = E_OUTOFMEMORY;
                             T1* p = NULL;
                             ATLTRY(p = new T1(pv))
                             if (p != NULL) {
                                     p->SetVoid(pv);
                                     p->InternalFinalConstructAddRef();
                                     hRes = p->FinalConstruct();
                                     p->InternalFinalConstructRelease();
                                     if (hRes == S_OK)
                                             hRes = p->QueryInterface(riid, ppv);
                                     if (hRes != S_OK)
                                             delete p;
                             }
                             return hRes;
                     }
             };
             
             template  class CComFailCreator {
             public:
                     static HRESULT WINAPI CreateInstance(void*, REFIID, 
                                                          LPVOID*)
                 { return hr; }
             };
             
             template  class CComCreator2 {
             public:
                     static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, 
                                                          LPVOID* ppv) {
                             HRESULT hRes = E_OUTOFMEMORY;
                             if (pv == NULL)
                                     hRes = T1::CreateInstance(NULL, riid, ppv);
                             else
                                     hRes = T2::CreateInstance(pv, riid, ppv);
                             return hRes;
                     }
             };     
                  
            ??? 因為 ATL 利用 Visual C++ 中的__declspec(novtable) 優化,所以在很大程度上依賴兩層構造。declspec 取消掉了在抽象基類的構造函數中必須對 vptr 進行的初始化,因為抽象基類中的任何的 vptr 會在派生類中被重寫。之所以要進行這種優化,是因為初始化從未被使用過的 vptr 毫無意義。另外,因為不需要為抽象基類分配vtable,從而減少了代碼的大小。
            ??? 使用這種技術的類(包括大多數 ATL 基類)需要當心,不要調用構造器中的虛函數。但是,為了在初始化時允許對虛函數的調用,ATL 的 Creators 調用 FinalConstruct 方法,在這個方法中進行所有重要的初始化工作。在 FinalConstuct 中,從C++的角度看,你的類已經完全構造好了,也就是說你的所有對象的 vptr 完全被派生化。同時,基于 CComObject 的打包器也同時構造好了,允許你存取在 COM 聚合或 tear-off 情況下無法知道的控制。
            ??? 如果在調試器中單步順序執行 Creator 調用,你將注意到在缺省情況下對 InternalFinalConstructAddRef 和 InternalFinalConstructRelease 的調用什么也沒做,但是,如果你打算在你的 FinalConstruct 實現中創建 COM 聚合,你可能會臨時增加一次對象的引用計數,以防止它過早銷毀(這發生在某個聚合對象調用 QueryInterface時)。你能通過添加下面的類定義行進行自我保護:
             DECLARE_PROTECT_FINAL_CONSTRUCT()
                  
            這一行代碼重新定義了類的 InternalFinalConstructAddRef 和 InternalFinalConstructRelease 來增減引用計數,從而安全地傳遞可能調用 QueryInterface 的對象指針。
            ??? 每一個基于ATL的工程都包含著一個 CComModule 派生類的實例。除了實現前面提到過的服務器生命期行為外,CComModule 還維持著一個 CLSID 到 ClassObject 的映射(叫做對象映射 Object Map)向量來提供所有外部可創建類。這個對象映射被用于實現進程內服務器的 DllGetClassObject,并且它為進程外服務器每次調用 CoRegisterClassObject 提供參數。雖然能直接顯式地使用 CComClassFactory 和 Creator 類,但通常都是在 ATL 對象映射基礎的上下文中使用。 ATL Object Map 是一個_ATL_OBJMAP_ ENTRY結構數組:
               
             struct _ATL_OBJMAP_ENTRY {
               const CLSID* pclsid;
               HRESULT (*pfnUpdateRegistry)(BOOL bRegister);
               HRESULT (*pfnGetClassObject)(void* pv, 
                                   REFIID riid, LPVOID* ppv);
               HRESULT (*pfnCreateInstance)(void* pv, 
                                   REFIID riid, LPVOID* ppv);
               IUnknown* pCF;
               DWORD dwRegister;
               LPCTSTR  (* pfnGetObjectDescription)(void);
             };   
            pfnGetClassObject成員的調用是在第一次需要創建新的類對象時。這個函數被作為 Creator 函數(pfnCreateInstance)的第一個參數傳遞,并且返回的結果接口指針被緩存在pCF成員中。通過按需要創建類對象,而不是靜態地實例化變量,就不再需要使用帶虛函數的全局對象,使得基于 ATL 的工程不用C運行庫就能進行鏈接。(在 DllMain / WinMain 以前,C運行時必須用來構造全局和靜態變量。)
            ??? 雖然你可以顯式地定義用于對象映射的各種函數,通常的方法是將 CComCoClass 添加到你自己類的基類列表中。CComCoClass 是一個模板類,它有兩個模板參數:你自己的類名和對應的 CLSID 指針。它添加適當的類型定義和靜態成員函數來提供對象映射必須的功能。下面的代碼示范了 CComCoClass 的使用:
                
            class CPager 
              : public CComObjectRootEx,
                public CComCoClass,
                public IPager 
             {
             public:
             BEGIN_COM_MAP(CPager)
               COM_INTERFACE_ENTRY(IPager)
             END_INTERFACE_MAP()
               STDMETHODIMP SendMessage(const OLECHAR * pwsz);
             };   
            一旦你從CComCoClass派生,你的類就已經被添加到ATL Object Map中。ATL所提供的用來簡化建立對象映射的宏很像接口映射宏。下面就是為多CLSID服務器建立的一個對象映射。
            BEGIN_OBJECT_MAP(ObjectMap)
               OBJECT_ENTRY(CLSID_Pager, CPager)
               OBJECT_ENTRY(CLSID_Laptop, CLaptop)
             END_OBJECT_MAP()
                  
            這個代碼建立了一個叫 ObjectMap 的 _ATL_OBJMAP_ENTRY 數組,初始化如下:

            ?static _ATL_OBJMAP_ENTRY ObjectMap[] = {
            ?{? &CLSID_Pager, &CPager::UpdateRegistry,?????
            ??? &CPager::_ClassFactoryCreatorClass::CreateInstance,?
            ??? &CPager::_CreatorClass::CreateInstance, NULL, 0,?
            ??? &CPager::GetObjectDescription?
            ?},
            ?{? &CLSID_Laptop, &CLaptop::UpdateRegistry,?????
            ??? &CLaptop::_ClassFactoryCreatorClass::CreateInstance,?
            ??? &CLaptop::_CreatorClass::CreateInstance, NULL, 0,?
            ??? &CLaptop::GetObjectDescription?
            ?},
            ?{ 0, 0, 0, 0 } };??????

            靜態成員函數從 CComCoClass 派生,被隱含式定義。以上定義的對象映射一般通過使用 CComModule 的 Init 方法被傳遞到ATL:

                 
             _Module.Init(ObjectMap, hInstance);
                  
            這個方法根據創建的服務器類型,在 DllMain 或 WinMain 中被調用。
            ??? 缺省情況下,CcomCoClass 為你的類提供了一個標準的類工廠,允許客戶端聚合你的對象。你可以通過添加下面的類定義代碼行來改變缺省的聚合行為:
                 
                  DECLARE_NOT_AGGREGATABLE(CPager)
                  DECLARE_ONLY_AGGREGATABLE(CPager)
                  DECLARE_POLY_AGGREGATABLE(CPager)
                  
            ??? 這些宏只是將 ATL Creator 定義成一個將被用于初始化對象映射的嵌套類型(CreatorClass)。前面兩個宏是自解釋的(它們禁止或需要聚合)。 第三個宏需要解釋一下。缺省情況下,CComCoClass 使用 ATL 類創建機制,根據是否需要使用聚合來創建兩個不同的類之一。如果不需要聚合,則創建新的 CComObject 實例。如果需要聚合,則創建新的CComAggObject實例。也就是說兩個不同的 vtables 必須在可執行文件中出現。對照之下,DECLARE_POLY_ AGGREGATABLE 總是創建一個 CComPolyObject 實例,并根據對象是否聚合來初始化這個外部控制指針。亦即只要定義一個C++類,只需一個 vtable。這個技術的不足之處是:非聚合對象的每個實例必須為非代理 IUnknown 指針多用4個字節。不論哪種情況,支持聚合都不需要實際的編碼,而只是在實例和代碼大小之間作出取舍。

            ATL和注冊表

            ??? CComModule 提供了兩個方法用于自注冊:一個是RegisterServer,另外一個是 UnregisterServer。這兩個方法使用傳遞到 Init 例程的對象映射來完成實際的工作。正像我前面所提到的那樣,每一個對象映射入口都包含 pfnUpdateRegistry 函數指針,這個指針必須由類實現者提供。ATL最初的版本所提供的例程為 CLSID 自動添加標準注冊入口,從而使缺省行為的實現很容易??上н@些例程不具備很好的可擴展性,而且如果服務器的需求超過了正常 InprocServer32 入口所包含的內容的話,就必須自己用手工來編寫注冊代碼。
            ??? 隨著組件種類(categories)和 AppIDs 概念的出現,幾乎就再沒有服務器能認可由ATL1.0提供的標準注冊入口。在ATL1.1及以后的版本中,首選的自注冊技術是使用注冊腳本,它非常靈活。這個技術需要 IRegistrar 接口的COM實現,它既可以靜態鏈接以降低依賴性,也可以用 CoCreateInstance 動態綁定來最小化代碼尺寸。
            ??? 注冊腳本只是個文本文件,它列出必須為給定的 CLSID 添加什么入口。注冊腳本文件默認的擴展名為RGS,并作為定制的 REGISTRY 類型資源被添加進可執行文件。注冊腳本的語法十分簡單,歸納起來為:

             [NoRemove|ForceRemove|val] Name [ = s|d ''''Value'''']?
             {
               ... 用于子鍵的腳本條目
             }
                 
            NoRemove 前綴表示在進行注銷時不刪除這個鍵。ForceRemove 前綴表示在寫這個鍵之前刪除當前的鍵和子鍵。Val 前綴表示這個入口是個命名的值,而不是一個鍵。s和d值前綴分別表示REG_SZ 或 REG_DWORD。ATL的解析機制既可以識別 HKEY_CLASSES_ROOT 等標準的注冊表鍵,也能識別HKCR之類的縮寫表示。
            下面是個腳本注冊的例子:REGEDIT4
             REGEDIT4
             [HKEY_CLASSES_ROOT\CLSID\{XXX}]
             @=My Class
             [HKEY_CLASSES_ROOT\CLSID\{XXX}\InprocServer32]
             @=C:\foo\bar.dll
             ThreadingModel=Free
            
            其對應的注冊腳本如下:
             HKCR {
               NoRemove CLSID {
                 ForceRemove {XXX} = s ''''My Class'''' {
                   InprocServer32 = s ''''%MODULE%'''' {
                     val ThreadingModel = s ''''Free''''
                   }
                 }
               }
             }  
            在使用資源腳本的時候,你的類 UpdateRegistry 方法可以輕松地通過DECLARE_ REGISTRY_RESOURCEID宏定義,它有一個資源ID(通常在resource.h中定義)作為參數:
            class CPager : public 
             CComObjectRoot,public   
             IPager
               CComCoClass {
               DECLARE_REGISTRY_RESOURCEID(IDR_PAGER)
             };
             
            這個宏僅僅定義了 UpdateRegistry 方法,它調用內建在 CComModule 中的方法UpdateRegistryFromResource。這個方法有調用資源腳本的解析機制。
            在上面顯示的注冊腳本中,所有出現 %MODULE% 的地方將被實際的 GetModuleFileName 調用結果所代替。如果你需要根據動態運行值添加額外的注冊條目,可以添加其它的在注冊之前能被置換的串,而不是用 %MODULE%。為此,首先要選用一個新的置換變量,用百分符號限定變量名。例如:
             DateInstalled = s ''''%CURRENTDATE%'''' 
            然后丁一個定制的UpdateRegistry方法代替使用DECLARE_REGISTRY_ RESOURCEID宏,在你的方法中,建立一個名字-值對置換表,提供給模塊的注冊引擎。
            下面是用包含當前日期的串來替換%CURRENTDATE%變量的一個例子:
            static HRESULT WINAPI 
             CPager::UpdateRegistry(BOOL b) {
               OLECHAR wsz [1024]; SYSTEMTIME st;  
               GetLocalTime(&st);
               wsprintfW(wsz, L"%d/%d/%d", st.wMonth, st.wDay, 
                         st.wYear);
               _ATL_REGMAP_ENTRY rm[] = {
                 { OLESTR("CURRENTDATE"), wsz}, { 0, 0 }, 
               };
               return _Module.UpdateRegistryFromResource(IDR_PAGER,b, rm);
             }
            這個代碼和注冊腳本最后的運行結果是注冊鍵 DateInstalled 將包含安裝時的日期。

            連接

            ??? COM 編程最單調乏味的一個方面是使用連接點來支持 outbound 接口。IConnectionPoint/IConnectionPointContainer 的設計好像是專門用來解決這個問題的,但是經驗證明,不論是在性能方面,還是在易用性方面,它還是存在不足之處。ATL為每一個這樣的接口提供了缺省的實現類,多少解決了一些易用性問題。
            ??? 要理解ATL如何實現連結點,最容易的方式是看例子:假設定義了如下的 outbound 接口:

            interface IPageSink : IUnknown {
               HRESULT OnPageReceived(void);
             }
             interface IStopSink : IUnknown {
               HRESULT OnShutdown(void);
             }
             
            為了支持這兩個 outbound 接口,下面的ATL代碼已經足夠:
             class CPager 
             : public CComObjectRoot, 
               public CComCoClass,
               public IPager,
               public IConnectionPointContainerImpl,
               public IConnectionPointImpl,
               public IConnectionPointImpl
             {
             BEGIN_COM_MAP(CPager)
               COM_INTERFACE_ENTRY(IPager)
               COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
             END_COM_MAP()
             
             BEGIN_CONNECTION_POINT_MAP(CPager)
               CONNECTION_POINT_ENTRY(IID_IPageSink)
               CONNECTION_POINT_ENTRY(IID_IStopSink)
             END_CONNECTION_POINT_MAP()
             
             };
             
            ??? 大多數有經驗的 COM 程序員首先注意到的是 CPager 類從一個接口(IConnectionPoint)派生的,這個接口并不作為 COM 本身的一部分提供。為了實現這種訣竅,ATL 類 IConnectionPointImpl 不從接口 IConnectionPoint 派生,而是象 IConnectionPoint 那樣以相同的順序定義它的虛函數。
            ??? 其次,為了防止每一個基類繼承主對象的 QueryInterface 實現,IConnectionPointImpl中第一個虛函數不是QueryInterface。而是一個類型兼容的方法,它叫做 LocCPQueryInterface,這個方法只針對 IID_IConnectionPoint 和 IID_IUnknown。它除了涉及允許完全基于多線程實現外,還有許多深奧的竅門在里面。
            ??? 對象的 FindConnectionPoint 方法實現使用由 ATL中 CONNECTION_POINT 宏定義的連接點映射。此映射是對象中的一個 DWORD表,表示與 IConnectionPoint 實現相對應的偏移量。FindConnectionPoint 遍歷此偏移量數組,詢問每一個所碰到的連接點,看看此連接是否擁有所請求的接口。
            ??? 上述例子建立了一個有效的對象實現,它支持作為 outbound 接口的 IStopSink 和 IPageSink 。但是,為了調用outbound 接口,你需要存取由 IConnectionPointImpl 類操縱的接口指針向量,并自己手動模擬多點傳送:
              
            typedef IConnectionPointImpl base;
             for (IUnknown** pp = base::m_vec.begin();
                  pp < base::m_vec.end(); 
                  pp++)
               if (*pp) 
                ((IPageSink*)(*pp))->OnPageRecieved();
                  
            編寫多點傳送例程十分繁瑣。所幸的是,ATL提供了 Visual Studio 組件—— ATL 代理產生器(ATL Proxy Generator),(如下圖五)它們會讀取接口的類型庫描述并產生 IConnectionPointImpl 派生類,為每一個 outbound 方法添加適當的 Fire 例程。連結點代理便被產生出來。

            圖五 ATL 代理產生器?
            這個類的定義應該像下面這樣:
            class CPager 
             : public CComObjectRoot, 
               public CComCoClass,
               public IPager,
               public IConnectionPointContainerImpl,
               public CProxyIPageSink,
               public CProxyIStopSink
             {
             BEGIN_COM_MAP(CPager)
               COM_INTERFACE_ENTRY(IPager)
               COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
             END_COM_MAP()
             
             BEGIN_CONNECTION_POINT_MAP(CPager)
               CONNECTION_POINT_ENTRY(IID_IPageSink)
               CONNECTION_POINT_ENTRY(IID_IStopSink)
             END_CONNECTION_POINT_MAP()
             };
            為了發送出境方法通知,你只要調用適當的Fire_XXX方法即可:
             STDMETHODIMP CPager::SendMessage(LPCOLESTR pwsz) {
             // send outbound notifications
               HRESULT hr = Fire_OnPageRecieved();
             // process normally
               return hr;
             }
            
            ??? 機器產生代理的一個限制是必須要 outbound 接口的類型庫定義。對于大量的 COM 接口而言,這是不可能的,因為類型庫在轉換方面與 IDL 特性背道而馳。對于更復雜的接口,你可以從機器所產生的代理開始并修改這些代碼來進行嘗試。

            后記

            ??? 本文概括地討論了 ATL 核心體系結構,主要針對 ATL 中使用,同時也是 ATL 用戶使用的基本編程風格。實際上本文中所討論的和涉及的內容特性都是基于 ATL1.1 版本的,某些內容在 ATL2.0 中稍有改動。本文沒有包含有關ActiveX 控件接口的缺省實現方面的內容,它們被加到了 ATL2.0 版本中,遵循與 ATL1.1 同樣的編程哲學。只要你常常推敲 ATL 源代碼,這些接口會很容易理解。
            ??? 在與同事和朋友討論 ATL 時,我發現大多數人的感觸是針對 ATL 的設計。有一小部分人覺得它非常棒。另外一小部分人覺得它復雜而不可思議。但大多數人(包括我自己)的感覺是雙重的,優劣兼而有之。幸運的是 ATL 編程是一個非常"量入為出"的過程。只要你去學總會有收獲,所以初學者需要努力而為之。

            From:http://www.vckbase.com/
            翻譯者:趙湘寧
            posted on 2007-01-29 09:53 我風 閱讀(3404) 評論(0)  編輯 收藏 引用
            <2025年5月>
            27282930123
            45678910
            11121314151617
            18192021222324
            25262728293031
            1234567

            常用鏈接

            留言簿(12)

            隨筆分類

            隨筆檔案

            文章檔案

            相冊

            收藏夾

            C++

            MyFavorite

            搜索

            •  

            積分與排名

            • 積分 - 326120
            • 排名 - 75

            最新評論

            閱讀排行榜

            評論排行榜

            精品久久亚洲中文无码| 欧美久久综合性欧美| 久久亚洲国产成人精品性色| 亚洲狠狠婷婷综合久久久久| 国产亚洲综合久久系列| 久久激情五月丁香伊人| 久久妇女高潮几次MBA| 四虎国产精品免费久久久| 亚洲精品无码久久不卡| 99久久er这里只有精品18| 久久精品国产亚洲Aⅴ香蕉| 午夜久久久久久禁播电影| 777久久精品一区二区三区无码| 精品久久久久成人码免费动漫| 99久久99这里只有免费的精品| 亚洲国产成人精品91久久久| 国产午夜精品理论片久久影视 | 狠狠色综合网站久久久久久久高清| 国产高潮国产高潮久久久| 亚洲成av人片不卡无码久久| 国产亚洲欧美精品久久久| 久久久亚洲欧洲日产国码是AV| A级毛片无码久久精品免费| 久久婷婷五月综合97色| 亚洲国产成人久久精品99| 国产免费久久精品99久久| 丰满少妇人妻久久久久久| 亚洲香蕉网久久综合影视| 中文精品99久久国产| 欧美国产成人久久精品| 91精品久久久久久无码| 亚洲乱亚洲乱淫久久| 国产欧美一区二区久久| …久久精品99久久香蕉国产| 香蕉久久av一区二区三区| 狠狠色丁香久久婷婷综合| 久久久精品久久久久影院| 综合久久精品色| 国产69精品久久久久APP下载 | 久久五月精品中文字幕| 久久久久国产一区二区|