• <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
            本文假設(shè)你熟悉C++和COM。

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

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






            圖一 ATL 對象向?qū)?br />

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

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

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

            實現(xiàn)IUnknown

            ??? 用純粹的C++實現(xiàn)IUnknown相對來說比較簡單。IUnknown實現(xiàn)之間的主要差別重點在于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++類定義實現(xiàn)了三個接口:
             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);
            };
            
            ?? 如果在堆中創(chuàng)建對象(也就是說用new操作符在內(nèi)部創(chuàng)建)并且只用單線程公寓(STA)模式運行,下面是合理的AddRef 和Release實現(xiàn):
            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)例程調(diào)用來代替:
            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實現(xiàn)都是正確的:
            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的最后四行代碼對所有的對象都一樣。其余的部分則根據(jù)這個對象類型層次上的類不同而有所不同。
            ??????? 如果IUnknown的實現(xiàn)能形成規(guī)律,那么ATL便可以提供一種機制將這些具有共性的程序語句從代碼中提取出來。實際上ATL做到了這一點,方法是通過提供靈活和可擴展的類層次,使得開發(fā)人員只要正確地說明所使用的類集,就可確定線程,服務(wù)器鎖定和對象生命期行為。
            ????? 如果看一看ATL實現(xiàn)的IUnknown類層次,你碰到的第一個參數(shù)化行為就是線程。ATL允許你構(gòu)造被優(yōu)化的對象和服務(wù)器,在相互轉(zhuǎn)換STA和MTA的用法時,不用修改源代碼。缺省情況下,ATL工程構(gòu)造的是安全的MTA(MTA-safe)對象,除了要具備構(gòu)造單STA對象所需的一切外,還需要附加代碼和狀態(tài)管理。通過定義適當(dāng)?shù)念A(yù)處理指令,你能改變?nèi)笔〉木€程行為,從而支持單STA或多個基于STA的工程。
            使用如下這個指令:
                  /D _ATL_SINGLE_THREADED
            來編譯工程可以改變服務(wù)器缺省的線程模型,讓它只支持一個基于STA的線程。它適合于進(jìn)程外的且不創(chuàng)建自擁有線程的基于STA的服務(wù)器情況,當(dāng)你用這個選項時,所有對ATL全局狀態(tài)的存取將都是不加鎖的,并發(fā)的。盡管此選項似乎很有效,但它實質(zhì)上限制了ATL服務(wù)器只能是一個單線程的。
            使用如下這個指令:
                  /D _ATL_APARTMENT_THREADED
            來編譯工程可以改變服務(wù)器缺省的線程模型支持多個基于STA的線程。它適合于建立注冊表項ThreadingModel=Apartment的進(jìn)程內(nèi)服務(wù)器。如果要創(chuàng)建基于STA的進(jìn)程外服務(wù)器且還要建立附加的基于STA的線程,那么這個指令也是必須的。使用這個選項導(dǎo)致ATL用能安全存取線程的臨界區(qū)來保護(hù)它的全局狀態(tài)。
            使用如下這個指令:
                  /D _ATL_FREE_THREADED
            可以創(chuàng)建與任何線程環(huán)境兼容的服務(wù)器。也就是說ATL的全局狀態(tài)將在臨界區(qū)中被鎖定,并且每個對象將擁有它自己的私有臨界區(qū)來保護(hù)它的實例狀態(tài)。如果沒有定義這些指令,則ATL頭文件假設(shè)為使用_ATL_FREE_THREADED。
            ??? 為了在ATL中正確使用線程,必須理解ATL的線程模型概念。ATL定義了三種線程模型類來實現(xiàn)線程安全行為所需的少數(shù)內(nèi)聯(lián)操作。每一種線程模型都輸出兩個靜態(tài)函數(shù),Increment 和Decrement。它們都有一個長整指針參數(shù),并且對指定的線程模型實現(xiàn)最快的,合法的增減操作。每一種線程模型都輸出兩個嵌套類型定義(typedefs),即AutoCriticalSection 和CriticalSection,它們要么打包Win32的CRITICAL_SECTION,要么就是出于兼容性考慮的空類,沒有實際實現(xiàn)。記住,所用臨界區(qū)實際使用的實際類型依賴于特定的線程模型。
            ??? ATL實現(xiàn)的三種線程模型分別是CComMultiThreadModel,CComSingleThreadModel和 CComMultiThreadModelNoCS。CComMultiThreadModel使用InterlockedIncrement/InterlockedDecrement和實的CRITICAL_SECTIONS。CComSingleThreadModel使用更有效的++和-操作符及虛的CRITICAL_SECTIONS。
            混合的CComMultiThreadModelNoCS除了使用虛的CRITICAL_SECTIONS外,還有InterlockedIncrement/InterlockedDecrement。第三種模型對于存在于MTAs中,但不需要任何數(shù)據(jù)成員的鎖定的對象很有用。
            ??? ATL提供了兩個類型定義,CComObjectThreadModel 和 CComGlobalsThreadModel,通過條件編譯來保證對象和全局變量各自的效率及行為安全。依據(jù)所定義的三種預(yù)編譯指令之一,每一類型名對應(yīng)著以上描述的三種線程模型類之一。下表說明了這種對應(yīng)關(guān)系,它依賴于ATL所使用的預(yù)處理指令。
            ATL類型定義_ATL_SINGLE_THREADED_ATL_APARTMENT_THREADED_ATL_FREE_THREADED
            CComGlobalsThreadModelCComSingleThreadModelCComMultiThreadModelCComMultiThreadModel
            CComObjectThreadModelCComSingleThreadModelCComSingleThreadModelCComMultiThreadModel

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

              參數(shù)化的線程
            
             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(); 
             // 實現(xiàn)任務(wù)
                 this->GenerateMessage();
                 this->WakeUpUser();
             // 允許其它線程
                 m_critsec.Unlock();
                 return S_OK;
               }
             }; 
            使用缺省選項(_ATL_FREE_THREADED)的編譯則將一個實臨界區(qū)添加到對象,并執(zhí)行Lock和Unlock方法將內(nèi)聯(lián)調(diào)用映射到EnterCriticalSection/LeaveCriticalSection API函數(shù)。同時,AddRef和Release方法將使用InterlockedIncrement/InterlockedDecrement來安全地改變這個對象的引用計數(shù)。
            ????? 如果前面的代碼是用_ATL_APARTMENT_THREADED 或者 _ATL_SINGLE_ THREADED選項編譯的,則m_critsee數(shù)據(jù)成員將為空,Lock和Unlock內(nèi)聯(lián)例程將變成虛操作,并且AddRef和Release方法將使用++和--操作符。如果這個對象是一個ActiveX控件且其線程模型為Apartment (ThreadingModel=Apartment)的進(jìn)程內(nèi)服務(wù)器,則這將是小而快的代碼。
            ????? 有時構(gòu)造以MTA模式(即它的AddRef和Release必須是線程安全的)運行,但不需要附加鎖定的對象很有用。對于這種類型的對象,混合型的CComMultiThreadModelNoCS很適合。
            通過將類的類型定義從:
                  typedef CComObjectThreadModel _ThreadModel;
            細(xì)化到
                  typedef CComMultiThreadModelNoCS _ThreadModel;
            那么針對每一個對象,你不必付出CRITICAL_SECTION的開銷(CComAutoCriticalSection 會映射到 CComFakeCriticalSection)就可以得到線程安全的AddRef和Release(將Increment 和 Decrement方法映射到InterlockedIncrement和InterlockedDecrement)。

            實現(xiàn)接口

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

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

            從下面的代碼可以看出,ATL中實現(xiàn)IUnknown的關(guān)鍵在于CComObjectRootBase 和 CComObjectRootEx。
            ?? CComObjectRoot?


            ?class CComObjectRootBase {
            ?public:
            ?// C++ 構(gòu)造函數(shù)
            ?? CComObjectRootBase() { m_dwRef = 0L; }
            ?
            ?// ATL 偽構(gòu)造函數(shù)和偽析構(gòu)函數(shù)
            ?? HRESULT FinalConstruct() { return S_OK; }?
            ?? void FinalRelease() {}
            ?
            ?// 內(nèi)部Unknown函數(shù)(由派生類提供的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函數(shù)
            ?? 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 創(chuàng)建者的鉤子例程
            ?? void SetVoid(void*) {}
            ?? void InternalFinalConstructAddRef() {}
            ?? void InternalFinalConstructRelease() {}
            ?
            ?// ATL 接口映射輔助函數(shù)
            ?? 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);
            ?
            ?// 實際的引用計數(shù)或者指針返回到真實的Unknown
            ?? union {
            ???? long m_dwRef;
            ???? IUnknown* m_pOuterUnknown;
            ?? };
            ?};
            ?
            ?template <class ThreadModel>
            ?class CComObjectRootEx : public CComObjectRootBase {
            ?public:
            ?? typedef ThreadModel _ThreadModel;
            ?? typedef _ThreadModel::AutoCriticalSection _CritSec;
            ?
            ?// 內(nèi)部 Unknown 函數(shù)(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的功能委派給外部實現(xiàn)。當(dāng)實現(xiàn)COM的聚合和tear-offs時要用到這些方法。其它三個方法--InternalQueryInterface,InternalAddRef和 InternalRelease的作用是實現(xiàn)本身的引用計數(shù)以及對象接口的查詢或?qū)Ш健?br />??? CComObjectRootEx是個模板類,允許你針對這個類指定使用哪種ATL線程模型。(如果你想要進(jìn)行條件編譯,則使用CComObjectRoot就可以了,它是一個針對CComObjectRootEx<CComObjectThreadModel>的類型定義。)CComObjectRootEx從CComObjectRootBase中派生其大多數(shù)功能,它是個相當(dāng)袖珍的類,只包含一個聯(lián)合類型的數(shù)據(jù)成員:
             union {
               long m_dwRef; 
               IUnknown *m_pOuterUnknown;
             }; 
            ? 根據(jù)使用這個類的實際方式,聯(lián)合中的成員將被用于保存給定類實例的生命周期。大多數(shù)情況下要用到m_dwRef,m_pOuterUnknown只有在支持聚合或tear-offs時用到。CComObjectRootBase提供了OuterQueryInterface,OuterAddRef和OuterRelease方法,通過m_pOuterUnknown成員轉(zhuǎn)發(fā)IUnknown請求。
            ? 反過來,CComObjectRootEx提供InternalAddRef 和InternalRelease方法根據(jù)模板參數(shù)傳遞的線程模型來實際增減m_dwRef變量得值。注意這些例程只是增減這個變量,而沒有真正刪除這個對象。這是因為此對象的分配策略將由派生類中提供,派生類將使用這些例程來調(diào)整引用計數(shù)。
            ? CComObjectRoot層次最引人注目的是它的QueryInterface實現(xiàn)函數(shù),它被作為CComObjectRootBase的方法(InternalQueryInterface)輸出:
            static HRESULT WINAPI 
             CComObjectRootBase::InternalQueryInterface(void *pThis, 
                                  const _ATL_INTMAP_ENTRY *pEntries, 
                                  REFIID riid, void **ppv);
                  
                 
            使用ATL實現(xiàn)IUnknown的每一個類必須制定一個接口映射來提供InternalQueryInterface。ATL的接口映射是IID/DWORD/函數(shù)指針數(shù)組,它指示當(dāng)QueryInterface請求一個給定的IID時要采取什么樣的行動。其類型都是_ATL_INTMAP_ENTRY。
            struct _ATL_INTMAP_ENTRY {
               const IID* piid;  // 接口ID (IID)
               DWORD dw;         // 多用途值
               HRESULT (*pFunc)(void*, REFIID, void**, DWORD); 
            };      
            

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

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

            不要過分抽象

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

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

            ??? 每一個 CComObject 類用派生來提供正確的 QueryInterface,AddRef 和 Release 實現(xiàn)。如果QueryInterface,AddRef 和 Release的語義正確,則所有 CComObject 類用你的類名作為模板參數(shù)并創(chuàng)建一個從你的類派生的新類。在這些類中,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);
             };      
                  
            ?? 它假設(shè)你的對象將在堆中分配,也就是說最終調(diào)用 Release 時,將觸發(fā)這個對象的 delete 操作。CComObjectNoLock 假設(shè)你的對象不是可聚合的,并且在服務(wù)器運行時,對象并不一直存在(因此有后綴 NoLock)。
            ??? 為了在堆中分配基于 ATL 類的 CPager 實例,只要對這個類名進(jìn)行 CComObject 模板包裝即可:
             IPager *p = new CComObjectNoLock();
                  
            ??? CComObjectNoLock 類從 CPager 派生,并且添加了由 CPager 提供的使用 InternalQueryInterface,InternalAddRef 和 InternalRelease 的 QueryInterface,AddRef 和 Release實現(xiàn)。因為此對象是基于堆的,所以對 delete 的調(diào)用將發(fā)生在 CComObjectNoLock 類的 Release 實現(xiàn)中,此時InternalRelease 返回零。
            ??? CComObject 類通常被用于基于堆的對象,這個對象只要存在,則服務(wù)器一直要運行。與許多 CComObject 家族中的其它類一樣,CComObject提供了一個全程變量,_Module,它有兩個方法,Lock 和 Unlock。這些方法與 MFC 的 AfxOleLockApp 和AfxOleUnLockApp 很相似。CComObject 的構(gòu)造函數(shù)調(diào)用_Module 的 Lock 方法,而 CComObject 的析構(gòu)函數(shù)則調(diào)用_Module的Unlock 方法。ATL提供了一個 CComModule 類,它以適當(dāng)?shù)姆绞綖檫M(jìn)程內(nèi)服務(wù)器實現(xiàn)了這些方法。當(dāng)建立進(jìn)程外服務(wù)器時,某個派生類(CExeModule)必須改寫默認(rèn)的 Lock / Unlock方法,以便以某種適當(dāng)?shù)姆绞较碌舴?wù)器。基于 AppWizard 的 ATL工程自動會有一個類用 PostThreadMessage 終止主線程的消息循環(huán)。

            輸出你的類

            ?? 實現(xiàn)了 CComObject ,你就有足夠的條件用 C++ new 操作符創(chuàng)建 COM 對象。不過這樣做沒有什么實用價值,因為畢竟外部客戶端使用 CoCreateInstance 或 CoGetClassObject 創(chuàng)建類實例。也就是說,你必須為每個外部類輸出類對象。幸運的是ATL分別在它的 CComClassFactory 和 CComClassFactory2 類中提供了缺省的 IClassFactory 和 IClassFactory2接口實現(xiàn)。
            ??? CComClassFactory 不是模板驅(qū)動類,但其中有一個函數(shù)指針作為數(shù)據(jù)成員,使用這個函數(shù)可以創(chuàng)建對象。ATL提供了一個類模板家族,它們都有一個單獨的靜態(tài)方法 CreateInstance,由 Creators 調(diào)用,Creators 提供正確的語義來從 CComClassFactory 創(chuàng)建基于 CComObjectRoot 的對象。下面的這段代碼展示了缺省的創(chuàng)建機制:CComCreator,它產(chǎn)生一個模板化的類實例,并用 ATL 中標(biāo)準(zhǔn)的 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) 優(yōu)化,所以在很大程度上依賴兩層構(gòu)造。declspec 取消掉了在抽象基類的構(gòu)造函數(shù)中必須對 vptr 進(jìn)行的初始化,因為抽象基類中的任何的 vptr 會在派生類中被重寫。之所以要進(jìn)行這種優(yōu)化,是因為初始化從未被使用過的 vptr 毫無意義。另外,因為不需要為抽象基類分配vtable,從而減少了代碼的大小。
            ??? 使用這種技術(shù)的類(包括大多數(shù) ATL 基類)需要當(dāng)心,不要調(diào)用構(gòu)造器中的虛函數(shù)。但是,為了在初始化時允許對虛函數(shù)的調(diào)用,ATL 的 Creators 調(diào)用 FinalConstruct 方法,在這個方法中進(jìn)行所有重要的初始化工作。在 FinalConstuct 中,從C++的角度看,你的類已經(jīng)完全構(gòu)造好了,也就是說你的所有對象的 vptr 完全被派生化。同時,基于 CComObject 的打包器也同時構(gòu)造好了,允許你存取在 COM 聚合或 tear-off 情況下無法知道的控制。
            ??? 如果在調(diào)試器中單步順序執(zhí)行 Creator 調(diào)用,你將注意到在缺省情況下對 InternalFinalConstructAddRef 和 InternalFinalConstructRelease 的調(diào)用什么也沒做,但是,如果你打算在你的 FinalConstruct 實現(xiàn)中創(chuàng)建 COM 聚合,你可能會臨時增加一次對象的引用計數(shù),以防止它過早銷毀(這發(fā)生在某個聚合對象調(diào)用 QueryInterface時)。你能通過添加下面的類定義行進(jìn)行自我保護(hù):
             DECLARE_PROTECT_FINAL_CONSTRUCT()
                  
            這一行代碼重新定義了類的 InternalFinalConstructAddRef 和 InternalFinalConstructRelease 來增減引用計數(shù),從而安全地傳遞可能調(diào)用 QueryInterface 的對象指針。
            ??? 每一個基于ATL的工程都包含著一個 CComModule 派生類的實例。除了實現(xiàn)前面提到過的服務(wù)器生命期行為外,CComModule 還維持著一個 CLSID 到 ClassObject 的映射(叫做對象映射 Object Map)向量來提供所有外部可創(chuàng)建類。這個對象映射被用于實現(xiàn)進(jìn)程內(nèi)服務(wù)器的 DllGetClassObject,并且它為進(jìn)程外服務(wù)器每次調(diào)用 CoRegisterClassObject 提供參數(shù)。雖然能直接顯式地使用 CComClassFactory 和 Creator 類,但通常都是在 ATL 對象映射基礎(chǔ)的上下文中使用。 ATL Object Map 是一個_ATL_OBJMAP_ ENTRY結(jié)構(gòu)數(shù)組:
               
             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成員的調(diào)用是在第一次需要創(chuàng)建新的類對象時。這個函數(shù)被作為 Creator 函數(shù)(pfnCreateInstance)的第一個參數(shù)傳遞,并且返回的結(jié)果接口指針被緩存在pCF成員中。通過按需要創(chuàng)建類對象,而不是靜態(tài)地實例化變量,就不再需要使用帶虛函數(shù)的全局對象,使得基于 ATL 的工程不用C運行庫就能進(jìn)行鏈接。(在 DllMain / WinMain 以前,C運行時必須用來構(gòu)造全局和靜態(tài)變量。)
            ??? 雖然你可以顯式地定義用于對象映射的各種函數(shù),通常的方法是將 CComCoClass 添加到你自己類的基類列表中。CComCoClass 是一個模板類,它有兩個模板參數(shù):你自己的類名和對應(yīng)的 CLSID 指針。它添加適當(dāng)?shù)念愋投x和靜態(tài)成員函數(shù)來提供對象映射必須的功能。下面的代碼示范了 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派生,你的類就已經(jīng)被添加到ATL Object Map中。ATL所提供的用來簡化建立對象映射的宏很像接口映射宏。下面就是為多CLSID服務(wù)器建立的一個對象映射。
            BEGIN_OBJECT_MAP(ObjectMap)
               OBJECT_ENTRY(CLSID_Pager, CPager)
               OBJECT_ENTRY(CLSID_Laptop, CLaptop)
             END_OBJECT_MAP()
                  
            這個代碼建立了一個叫 ObjectMap 的 _ATL_OBJMAP_ENTRY 數(shù)組,初始化如下:

            ?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 } };??????

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

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

            ATL和注冊表

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

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

            連接

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

            interface IPageSink : IUnknown {
               HRESULT OnPageReceived(void);
             }
             interface IStopSink : IUnknown {
               HRESULT OnShutdown(void);
             }
             
            為了支持這兩個 outbound 接口,下面的ATL代碼已經(jīng)足夠:
             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()
             
             };
             
            ??? 大多數(shù)有經(jīng)驗的 COM 程序員首先注意到的是 CPager 類從一個接口(IConnectionPoint)派生的,這個接口并不作為 COM 本身的一部分提供。為了實現(xiàn)這種訣竅,ATL 類 IConnectionPointImpl 不從接口 IConnectionPoint 派生,而是象 IConnectionPoint 那樣以相同的順序定義它的虛函數(shù)。
            ??? 其次,為了防止每一個基類繼承主對象的 QueryInterface 實現(xiàn),IConnectionPointImpl中第一個虛函數(shù)不是QueryInterface。而是一個類型兼容的方法,它叫做 LocCPQueryInterface,這個方法只針對 IID_IConnectionPoint 和 IID_IUnknown。它除了涉及允許完全基于多線程實現(xiàn)外,還有許多深奧的竅門在里面。
            ??? 對象的 FindConnectionPoint 方法實現(xiàn)使用由 ATL中 CONNECTION_POINT 宏定義的連接點映射。此映射是對象中的一個 DWORD表,表示與 IConnectionPoint 實現(xiàn)相對應(yīng)的偏移量。FindConnectionPoint 遍歷此偏移量數(shù)組,詢問每一個所碰到的連接點,看看此連接是否擁有所請求的接口。
            ??? 上述例子建立了一個有效的對象實現(xiàn),它支持作為 outbound 接口的 IStopSink 和 IPageSink 。但是,為了調(diào)用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 代理產(chǎn)生器(ATL Proxy Generator),(如下圖五)它們會讀取接口的類型庫描述并產(chǎn)生 IConnectionPointImpl 派生類,為每一個 outbound 方法添加適當(dāng)?shù)?Fire 例程。連結(jié)點代理便被產(chǎn)生出來。

            圖五 ATL 代理產(chǎn)生器?
            這個類的定義應(yīng)該像下面這樣:
            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()
             };
            為了發(fā)送出境方法通知,你只要調(diào)用適當(dāng)?shù)腇ire_XXX方法即可:
             STDMETHODIMP CPager::SendMessage(LPCOLESTR pwsz) {
             // send outbound notifications
               HRESULT hr = Fire_OnPageRecieved();
             // process normally
               return hr;
             }
            
            ??? 機器產(chǎn)生代理的一個限制是必須要 outbound 接口的類型庫定義。對于大量的 COM 接口而言,這是不可能的,因為類型庫在轉(zhuǎn)換方面與 IDL 特性背道而馳。對于更復(fù)雜的接口,你可以從機器所產(chǎn)生的代理開始并修改這些代碼來進(jìn)行嘗試。

            后記

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

            From:http://www.vckbase.com/
            翻譯者:趙湘寧
            posted on 2007-01-29 09:53 我風(fēng) 閱讀(3487) 評論(0)  編輯 收藏 引用

            只有注冊用戶登錄后才能發(fā)表評論。
            網(wǎng)站導(dǎo)航: 博客園   IT新聞   BlogJava   博問   Chat2DB   管理


            <2025年8月>
            272829303112
            3456789
            10111213141516
            17181920212223
            24252627282930
            31123456

            常用鏈接

            留言簿(12)

            隨筆分類

            隨筆檔案

            文章檔案

            相冊

            收藏夾

            C++

            MyFavorite

            搜索

            •  

            積分與排名

            • 積分 - 328099
            • 排名 - 75

            最新評論

            閱讀排行榜

            評論排行榜

            久久精品国产亚洲AV不卡| 久久久无码精品午夜| 精品久久久久久亚洲| 久久久久国色AV免费看图片| 久久久久久久波多野结衣高潮| 久久精品国产亚洲AV无码偷窥| 久久久久久国产精品免费免费| 伊人久久大香线蕉综合Av | 久久久久无码国产精品不卡| 亚洲熟妇无码另类久久久| 国产精品日韩欧美久久综合| 77777亚洲午夜久久多喷| 国产激情久久久久影院小草| 久久丫精品国产亚洲av| 久久婷婷五月综合成人D啪| av无码久久久久不卡免费网站| 中文字幕精品久久| 久久国产乱子伦精品免费午夜| 99久久人妻无码精品系列蜜桃| 超级97碰碰碰碰久久久久最新| 99久久精品国产一区二区三区 | 亚洲综合婷婷久久| 久久精品国产网红主播| 欧美日韩精品久久免费| 四虎亚洲国产成人久久精品| 99久久精品国产综合一区| 国产精品美女久久久久| 色综合久久综合中文综合网| 欧美一区二区久久精品| 亚洲欧美精品一区久久中文字幕| 一本色道久久99一综合| 国产色综合久久无码有码| 久久亚洲av无码精品浪潮| 久久er国产精品免费观看2| 久久久久久久久无码精品亚洲日韩| 亚洲国产成人精品91久久久 | 国产精品久久久久a影院| 久久夜色精品国产www| 精品久久久久久国产牛牛app| 99久久精品国产一区二区蜜芽| 久久精品一区二区影院|