熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> ASP編程 >> 正文

用VC進行COM編程所必須掌握的理論知識

2022-06-13   來源: ASP編程 

  這篇文章是給初學者看的盡量寫得比較通俗易懂並且盡量避免編程細節完全是根據我自己的學習體會寫的其中若有技術上的錯誤之處請大家多多指正

  一為什麼要用COM

  軟件工程發展到今天從一開始的結構化編程到面向對象編程再到現在的COM編程目標只有一個就是希望軟件能象積方塊一樣是累起來的是組裝起來的而不是一點點編出來的結構化編程是函數塊的形式通過把一個軟件劃分成許多模塊每個模塊完成各自不同的功能盡量做到高內聚低藕合這已經是一個很好的開始我們可以把不同的模塊分給不同的人去做然後合到一塊這已經有了組裝的概念了軟件工程的核心就是要模塊化最理想的情況就是%內聚%藕合整個軟件的發展也都是朝著這個方向走的結構化編程方式只是一個開始下一步就出現了面向對象編程它相對於面向功能的結構化方式是一個巨大的進步我們知道整個自然界都是由各種各樣不同的事物組成的事物之間存在著復雜的千絲萬縷的關系而正是靠著事物之間的聯系交互作用我們的世界才是有生命力的才是活動的我們可以認為在自然界中事物做為一個概念它是穩定的不變的而事物之間的聯系是多變的運動的事物應該是這個世界的本質所在面向對象的著眼點就是事物就是這種穩定的概念每個事物都有其固有的屬性都有其固有的行為這些都是事物本身所固有的東西而面向對象的方法就是描述出這種穩定的東西而面向功能的模塊化方法它的著眼點是事物之間的聯系它眼中看不到事物的概念它只注重功能我們平常在劃分模塊的時侯有沒有想過這個函數與哪些對象有關呢?很少有人這麼想一個函數它實現一種功能這個功能必定與某些事物想聯系我們沒有去掌握事物本身而只考慮事物之間是怎麼相互作用而完成一個功能的說白了這叫本末倒置也叫急功近利因為不是我們智慧不夠只是因為我們沒有多想一步面向功能的結構化方法因為它注意的只是事物之間的聯系而聯系是多變的事物本身可能不會發生大的變化而聯系則是很有可能發生改變的聯系一變那就是另一個世界了那就是另一種功能了如果我們用面向對象的方法我們就可以以不變應萬變只要事先把事物用類描述好我們要改變的只是把這些類聯系起來的方法只是重新使用我們的類庫而面向過程的方法因為它構造的是一個不穩定的世界所以一點小小的變化也可能導致整個系統都要改變然而面向對象方法仍然有問題問題在於重用的方法搭積木式的軟件構造方法的基礎是有許許多多各種各樣的可重用的部件模塊我們首先想到的是類庫因為我們用面向對象的方法產生的直接結果就是許多的類但類庫的重用是基於源碼的方式這是它的重大缺陷首先它限制了編程語言你的類庫總是用一種語言寫的吧那你就不能拿到別的語言裡用了其次你每次都必須重新編譯只有編譯了才能與你自己的代碼結合在一起生成可執行文件在開發時這倒沒什麼關鍵在於開發完成後你的EXE都已經生成好了如果這時侯你的類庫提供廠商告訴你他們又做好了一個新的類庫功能更強大速度更快而你為之心動又想把這新版的類庫用到你自己的程序中那你就必須重新編譯重新調試!這離我們理想的積木式軟件構造方法還有一定差距在我們的設想裡希望把一個模塊拿出來再換一個新的模塊是非常方便的事可是現在不但要重新編譯還要冒著很大的風險因為你可能要重新改變你自己的代碼另一種重用方式很自然地就想到了是DLL的方式Windows裡到處是DLL它是Windows 的基礎但DLL也有它自己的缺點總結一下它至少有四點不足()函數重名問題DLL裡是一個一個的函數我們通過函數名來調用函數那如果兩個DLL裡有重名的函數怎麼辦?()各編譯器對C++函數的名稱修飾不兼容問題對於C++函數編譯器要根據函數的參數信息為它生成修飾名DLL庫裡存的就是這個修飾名但是不同的編譯器產生修飾的方法不一樣所以你在VC 裡編寫的DLL在BC裡就可以用不了不過也可以用extern C;來強調使用標准的C函數特性關閉修飾功能但這樣也喪失了C++的重載多態性功能()路徑問題放在自己的目錄下面別人的程序就找不到放在系統目錄下就可能有重名的問題而真正的組件應該可以放在任何地方甚至可以不在本機用戶根本不需考慮這個問題()DLL與EXE的依賴問題我們一般都是用隱式連接的方式就是編程的時侯指明用什麼DLL這種方式很簡單它在編譯時就把EXE與DLL綁在一起了如果DLL發行了一個新版本我們很有必要重新鏈接一次因為DLL裡面函數的地址可能已經發生了改變DLL的缺點就是COM的優點首先我們要先把握住一點COM和DLL一樣都是基於二進制的代碼重用所以它不存在類庫重用時的問題另一個關鍵點是COM本身也是DLL既使是ActiveX控件ocx它實際上也是DLL所以說DLL在還是有重用上有很大的優勢只不過我們通過制訂復雜的COM協議通COM本身的機制改變了重用的方法以一種新的方法來利用DLL來克服DLL本身所固有的缺陷從而實現更高一級的重用方法COM沒有重名問題因為根本不是通過函數名來調用函數而是通過虛函數表自然也不會有函數名修飾的問題路徑問題也不復存在因為是通過查注冊表來找組件的放在什麼地方都可以即使在別的機器上也可以也不用考慮和EXE的依賴關系了它們二者之間是松散的結合在一起可以輕松的換上組件的一個新版本而應用程序混然不覺

  二用VC進行COM編程必須要掌握哪些COM理論知識

  我見過很多人學COM看完一本書後覺得對COM的原理比較了解了COM也不過如此可是就是不知道該怎麼編程序我自己也有這種情況我也是經歷了這樣的階段走過來的要學COM的基本原理我推薦的書是《COM技術內幕》但僅看這樣的書是遠遠不夠的我們最終的目的是要學會怎麼用COM去編程序而不是拼命的研究COM本身的機制所以我個人覺得對COM的基本原理不需要花大量的時間去追根問底沒有必要是吃力不討好的事其實我們只需要掌握幾個關鍵概念就夠了這裡我列出了一些我自己認為是用VC編程所必需掌握的幾個關鍵概念(這裡所說的均是用C++語言條件下的COM編程方式)

  () COM組件實際上是一個C++類而接口都是純虛類組件從接口派生而來我們可以簡單的用純粹的C++的語法形式來描述COM是個什麼東西

  class IObject
  {
  public:
    virtual Function() = ;
    virtual Function() = ;
    
  };
  class MyObject : public IObject
  {
  public:
    virtual Function(){}
    virtual Function(){}

  };

  看清楚了嗎?IObject就是我們常說的接口MyObject就是所謂的COM組件切記切記接口都是純虛類它所包含的函數都是純虛函數而且它沒有成員變量而COM組件就是從這些純虛類繼承下來的派生類它實現了這些虛函數僅此而已從上面也可以看出COM組件是以 C++為基礎的特別重要的是虛函數和多態性的概念COM中所有函數都是虛函數都必須通過虛函數表VTable來調用這一點是無比重要的必需時刻牢記在心

  () COM組件有三個最基本的接口類分別是IUnknownIClassFactoryIDispatch

  COM規范規定任何組件任何接口都必須從IUnknown繼承IUnknown包含三個函數分別是 QueryInterfaceAddRefRelease這三個函數是無比重要的而且它們的排列順序也是不可改變的QueryInterface用於查詢組件實現的其它接口說白了也就是看看這個組件的父類中還有哪些接口類AddRef用於增加引用計數Release用於減少引用計數引用計數也是COM中的一個非常重要的概念大體上簡單的說來可以這麼理解COM組件是個DLL當客戶程序要用它時就要把它裝到內存裡另一方面一個組件也不是只給你一個人用的可能會有很多個程序同時都要用到它但實際上DLL只裝載了一次即內存中只有一個COM組件那COM組件由誰來釋放?由客戶程序嗎?不可能因為如果你釋放了組件那別人怎麼用所以只能由COM組件自己來負責所以出現了引用計數的概念COM維持一個計數記錄當前有多少人在用它每多一次調用計數就加一少一個客戶用它就減一當最後一個客戶釋放它的時侯COM知道已經沒有人用它了它的使用已經結束了那它就把它自己給釋放了引用計數是COM編程裡非常容易出錯的一個地方但所幸VC的各種各樣的類庫裡已經基本上把AddRef的調用給隱含了在我的印象裡我編程的時侯還從來沒有調用過AddRef我們只需在適當的時侯調用Release至少有兩個時侯要記住調用Release第一個是調用了 QueryInterface以後第二個是調用了任何得到一個接口的指針的函數以後記住多查MSDN 以確定某個函數內部是否調用了AddRef如果是的話那調用Release的責任就要歸你了 IUnknown的這三個函數的實現非常規范但也非常煩瑣容易出錯所幸的事我們可能永遠也不需要自己來實現它們

  IClassFactory的作用是創建COM組件我們已經知道COM組件實際上就是一個類那我們平常是怎麼實例化一個類對象的?是用new命令!很簡單吧COM組件也一樣如此但是誰來new它呢?不可能是客戶程序因為客戶程序不可能知道組件的類名字如果客戶知道組件的類名字那組件的可重用性就要打個大大的折扣了事實上客戶程序只不過知道一個代表著組件的位的數字串而已這個等會再介紹所以客戶無法自己創建組件而且考慮一下如果組件是在遠程的機器上你還能new出一個對象嗎?所以創建組件的責任交給了一個單獨的對象這個對象就是類廠每個組件都必須有一個與之相關的類廠這個類廠知道怎麼樣創建組件當客戶請求一個組件對象的實例時實際上這個請求交給了類廠由類廠創建組件實例然後把實例指針交給客戶程序這個過程在跨進程及遠程創建組件時特別有用因為這時就不是一個簡單的new操作就可以的了它必須要經過調度而這些復雜的操作都交給類廠對象去做了IClassFactory最重要的一個函數就是CreateInstance顧名思議就是創建組件實例一般情況下我們不會直接調用它API函數都為我們封裝好它了只有某些特殊情況下才會由我們自己來調用它這也是VC編寫COM組件的好處使我們有了更多的控制機會而VB給我們這樣的機會則是太少太少了

  IDispatch叫做調度接口它的作用何在呢?這個世上除了C++還有很多別的語言比如VB VJVBScriptJavaScript等等可以這麼說如果這世上沒有這麼多亂七八糟的語言那就不會有IDispatch:) 我們知道COM組件是C++類是靠虛函數表來調用函數的對於VC來說毫無問題這本來就是針對C++而設計的以前VB不行現在VB也可以用指針了也可以通過VTable來調用函數了VJ也可以但還是有些語言不行那就是腳本語言典型的如 VBScriptJavaScript不行的原因在於它們並不支持指針連指針都不能用還怎麼用多態性啊還怎麼調這些虛函數啊沒辦法也不能置這些腳本語言於不顧吧現在網頁上用的都是這些腳本語言而分布式應用也是COM組件的一個主要市場它不得不被這些腳本語言所調用既然虛函數表的方式行不通我們只能另尋他法了時勢造英雄IDispatch應運而生:) 調度接口把每一個函數每一個屬性都編上號客戶程序要調用這些函數屬性的時侯就把這些編號傳給IDispatch接口就行了IDispatch再根據這些編號調用相應的函數僅此而已當然實際的過程遠比這復雜僅給一個編號就能讓別人知道怎麼調用一個函數那不是天方夜潭嗎你總得讓別人知道你要調用的函數要帶什麼參數參數類型什麼以及返回什麼東西吧而要以一種統一的方式來處理這些問題是件很頭疼的事IDispatch接口的主要函數是Invoke客戶程序都調用它然後Invoke再調用相應的函數如果看一看MS的類庫裡實現 Invoke的代碼就會驚歎它實現的復雜了因為你必須考慮各種參數類型的情況所幸我們不需要自己來做這件事而且可能永遠也沒這樣的機會:)

  () dispinterface接口Dual接口以及Custom接口

  這一小節放在這裡似乎不太合適因為這是在ATL編程時用到的術語我在這裡主要是想談一下自動化接口的好處及缺點用這三個術語來解釋可能會更好一些而且以後遲早會遇上它們我將以一種通俗的方式來解釋它們可能並非那麼精確就好象用偽代碼來描述算法一樣:)

  所謂的自動化接口就是用IDispatch實現的接口我們已經講解過IDispatch的作用了它的好處就是腳本語言象VBScript JavaScript也能用COM組件了從而基本上做到了與語言無關它的缺點主要有兩個第一個就是速度慢效率低這是顯而易見的通過虛函數表一下子就可以調用函數了而通過Invoke則等於中間轉了道手續尤其是需要把函數參數轉換成一種規范的格式才去調用函數耽誤了很多時間所以一般若非是迫不得已我們都想用VTable的方式調用函數以獲得高效率第二個缺點就是只能使用規定好的所謂的自動化數據類型如果不用IDispatch我們可以想用什麼數據類型就用什麼類型VC會自動給我們生成相應的調度代碼而用自動化接口就不行了因為Invoke的實現代碼是VC事先寫好的而它不能事先預料到我們要用到的所有類型它只能根據一些常用的數據類型來寫它的處理代碼而且它也要考慮不同語言之間的數據類型轉換問題所以VC自動化接口生成的調度代碼只適用於它所規定好的那些數據類型當然這些數據類型已經足夠豐富了但不能滿足自定義數據結構的要求你也可以自己寫調度代碼來處理你的自定義數據結構但這並不是一件容易的事考慮到IDispatch的種種缺點(它還有一個缺點就是使用麻煩:) )現在一般都推薦寫雙接口組件稱為dual接口實際上就是從IDispatch繼承的接口我們知道任何接口都必須從 IUnknown繼承IDispatch接口也不例外那從IDispatch繼承的接口實際上就等於有兩個基類一個是IUnknown一個是IDispatch所以它可以以兩種方式來調用組件可以通過 IUnknown用虛函數表的方式調用接口方法也可以通過IDispatch::Invoke自動化調度來調用這就有了很大的靈活性這個組件既可以用於C++的環境也可以用於腳本語言中同時滿足了各方面的需要

  相對比的dispinterface是一種純粹的自動化接口可以簡單的就把它看作是IDispatch接口 (雖然它實際上不是的)這種接口就只能通過自動化的方式來調用COM組件的事件一般都用的是這種形式的接口

  Custom接口就是從IUnknown接口派生的類顯然它就只能用虛函數表的方式來調用接口了

  () COM組件有三種進程內本地遠程對於後兩者情況必須調度接口指針及函數參數

  COM是一個DLL它有三種運行模式它可以是進程內的即和調用者在同一個進程內也可以和調用者在同一個機器上但在不同的進程內還可以根本就和調用者在兩台機器上這裡有一個根本點需要牢記就是COM組件它只是一個DLL它自己是運行不起來的必須有一個進程象父親般照顧它才行即COM組件必須在一個進程內那誰充當看護人的責任呢?先說說調度的問題調度是個復雜的問題以我的知識還講不清楚這個問題我只是一般性的談談幾個最基本的概念我們知道對於WIN程序每個進程都擁有GB的虛擬地址空間每個進程都有其各自的編址同一個數據塊在不同的進程裡的編址很可能就是不一樣的所以存在著進程間的地址轉換問題這就是調度問題對於本地和遠程進程來說DLL 和客戶程序在不同的編址空間所以要傳遞接口指針到客戶程序必須要經過調度Windows 已經提供了現成的調度函數就不需要我們自己來做這個復雜的事情了對遠程組件來說函數的參數傳遞是另外一種調度DCOM是以RPC為基礎的要在網絡間傳遞數據必須遵守標准的網上數據傳輸協議數據傳遞前要先打包傳遞到目的地後要解包這個過程就是調度這個過程很復雜不過Windows已經把一切都給我們做好了一般情況下我們不需要自己來編寫調度DLL

  我們剛說過一個COM組件必須在一個進程內對於本地模式的組件一般是以EXE的形式出現所以它本身就已經是一個進程對於遠程DLL我們必須找一個進程這個進程必須包含了調度代碼以實現基本的調度這個進程就是dllhostexe這是COM默認的DLL代理實際上在分布式應用中我們應該用MTS來作為DLL代理因為MTS有著很強大的功能是專門的用於管理分布式DLL組件的工具

  調度離我們很近又似乎很遠我們編程時很少關注到它這也是COM的一個優點之一既平台無關性無論你是遠程的本地的還是進程內的編程是一樣的一切細節都由COM自己處理好了所以我們也不用深究這個問題只要有個概念就可以了當然如果你對調度有自己特殊的要求就需要深入了解調度的整個過程了這裡推薦一本《COM+技術內幕》這絕對是一本講調度的好書

  () COM組件的核心是IDL

  我們希望軟件是一塊塊拼裝出來的但不可能是沒有規定的胡亂拼接總是要遵守一定的標准各個模塊之間如何才能親密無間的合作必須要事先共同制訂好它們之間交互的規范這個規范就是接口我們知道接口實際上都是純虛類它裡面定義好了很多的純虛函數等著某個組件去實現它這個接口就是兩個完全不相關的模塊能夠組合在一起的關鍵試想一下如果我們是一個應用軟件廠商我們的軟件中需要用到某個模塊我們沒有時間自己開發所以我們想到市場上找一找看有沒有這樣的模塊我們怎麼去找呢?也許我們需要的這個模塊在業界已經有了標准已經有人制訂好了標准的接口有很多組件工具廠商已經在自己的組件中實現了這個接口那我們尋找的目標就是這些已經實現了接口的組件我們不關心組件從哪來它有什麼其它的功能我們只關心它是否很好的實現了我們制訂好的接口這種接口可能是業界的標准也可能只是你和幾個廠商之間內部制訂的協議但總之它是一個標准是你的軟件和別人的模塊能夠組合在一起的基礎是COM組件通信的標准

  COM具有語言無關性它可以用任何語言編寫也可以在任何語言平台上被調用但至今為止我們一直是以C++的環境中談COM那它的語言無關性是怎麼體現出來的呢?或者換句話說我們怎樣才能以語言無關的方式來定義接口呢?前面我們是直接用純虛類的方式定義的但顯然是不行的除了C++誰還認它呢?正是出於這種考慮微軟決定采用IDL來定義接口說白了IDL實際上就是一種大家都認識的語言用它來定義接口不論放到哪個語言平台上都認識它我們可以想象一下理想的標准的組件模式我們總是從IDL開始先用IDL制訂好各個接口然後把實現接口的任務分配不同的人有的人可能善長用VC有的人可能善長用VB這沒關系作為項目負責人我不關心這些我只關心你把最終的DLL 拿給我這是一種多麼好的開發模式可以用任何語言來開發也可以用任何語言來欣賞你的開發成果

  () COM組件的運行機制即COM是怎麼跑起來的

  這部分我們將構造一個創建COM組件的最小框架結構然後看一看其內部處理流程是怎樣的

  IUnknown *pUnk=NULL;
    IObject *pObject=NULL;
    CoInitialize(NULL);
    CoCreateInstance(CLSID_Object CLSCTX_INPROC_SERVER NULL IID_IUnknown (void**)&pUnk);
    pUnk>QueryInterface(IID_IOjbect (void**)&pObject);
    pUnk>Release();
    pObject>Func();
    pObject>Release();
    CoUninitialize();

  這就是一個典型的創建COM組件的框架不過我的興趣在CoCreateInstance身上讓我們來看看它內部做了一些什麼事情以下是它內部實現的一個偽代碼:

  CoCreateInstance()
    {
    
    IClassFactory *pClassFactory=NULL;
    CoGetClassObject(CLSID_Object CLSCTX_INPROC_SERVER NULL IID_IClassFactory (void **)&pClassFactory);
    pClassFactory>CreateInstance(NULL IID_IUnknown (void**)&pUnk);
    pClassFactory>Release();
    
   }

  這段話的意思就是先得到類廠對象再通過類廠創建組件從而得到IUnknown指針繼續深入一步看看CoGetClassObject的內部偽碼

  CoGetClassObject()
   {
    //通過查注冊表CLSID_Object得知組件DLL的位置文件名
    //裝入DLL庫
    //使用函數GetProcAddress()得到DLL庫中函數DllGetClassObject的函數指針
    //調用DllGetClassObject
   }
    DllGetClassObject是干什麼的它是用來獲得類廠對象的只有先得到類廠才能去創建組件
    下面是DllGetClassObject的偽碼
    DllGetClassObject()
    {
    
    CFactory* pFactory= new CFactory; //類廠對象
    pFactory>QueryInterface(IID_IClassFactory (void**)&pClassFactory);
    //查詢IClassFactory指針
    pFactory>Release();
    
    }
    CoGetClassObject的流程已經到此為止現在返回CoCreateInstance看看CreateInstance的偽碼
    CFactory::CreateInstance()
    {
    
    CObject *pObject = new CObject; //組件對象
    pObject>QueryInterface(IID_IUnknown (void**)&pUnk);
    pObject>Release();
    
    }

  () 一個典型的自注冊的COM DLL所必有的四個函數

  DllGetClassObject:用於獲得類廠指針

  DllRegisterServer:注冊一些必要的信息到注冊表中

  DllUnregisterServer:卸載注冊信息

  DllCanUnloadNow:系統空閒時會調用這個函數以確定是否可以卸載DLL

  DLL還有一個函數是DllMain這個函數在COM中並不要求一定要實現它但是在VC生成的組件中自動都包含了它它的作用主要是得到一個全局的實例對象

  () 注冊表在COM中的重要作用

  首先要知道GUID的概念COM中所有的類接口類型庫都用GUID來唯一標識GUID是一個位的字串根據特制算法生成的GUID可以保證是全世界唯一的 COM組件的創建查詢接口都是通過注冊表進行的有了注冊表應用程序就不需要知道組件的DLL文件名位置只需要根據CLSID查就可以了當版本升級的時侯只要改一下注冊表信息就可以神不知鬼不覺的轉到新版本的DLL

  本文是本人一時興起的塗鴨之作講得並不是很全面還有很多有用的體會沒寫出來以後如果有時間有興趣再寫出來希望這篇文章能給大家帶來一點用處那我一晚上的辛苦就沒有白費了:)


From:http://tw.wingwit.com/Article/program/ASP/201311/21887.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.