天天看點

Com程式設計入門——什麼是COM,如何使用COM

本文的目的是為剛剛接觸com的程式員提供程式設計指南,并幫助他們了解com的基本概念。内容包括com規範簡介,重要的com術語以及如何重用現有的com元件。本文不包括如何編寫自己的com對象和接口。

  com即元件對象模型,是component object model 取前三個字母的縮寫,這三個字母在當今windows的世界中随處可見。随時湧現出來的大把大把的新技術都以com為基礎。各種文檔中也充斥着諸如com 對象、接口、伺服器之類的術語。是以,對于一個程式員來說,不僅要掌握使用com的方法,而且還要徹底熟悉com的所有一切。

  本文由淺入深描述com的内在運作機制,教你如何使用第三方提供的com對象(以windows 外殼元件shell為例)。讀完本文後,你就能掌握如何使用windows作業系統中内建的元件和第三方提供的com對象。

  本文假設你精通c++語言。在例子代碼中使用了一點mfc和atl,如果你不熟悉mfc和atl也沒關系,本文會對這些代碼進行完全透徹的解釋。本文包括以下幾個部分:

com——到底是什麼?——com标準的要點介紹,它被設計用來解決什麼問題

基本元素的定義——com術語以及這些術語的含義

使用和處理com對象——如何建立、使用和銷毀com對象

基本接口——描述iunknown基本接口及其方法

掌握串的處理——在com代碼中如何處理串

應用com技術——例子代碼,舉例說明本文所讨論的所有概念

處理hresult——hresult類型描述,如何監測錯誤及成功代碼

<a target="_blank">com—</a>

  簡單地說,com是一種跨應用和語言共享二進制代碼的方法。與c++不同,它提倡源代碼重用。atl便是一個很好的例證。源碼級重用雖然好,但隻能用于c++。它還帶來了名字沖突的可能性,更不用說不斷拷貝重用代碼而導緻工程膨脹和臃腫。

  windows使用dlls在二進制級共享代碼。這也是windows程式運作的關鍵——重用kernel32.dll, user32.dll等。但dlls是針對c接口而寫的,它們隻能被c或了解c調用規範的語言使用。由程式設計語言來負責實作共享代碼,而不是由dlls本身。這樣的話dlls的使用受到限制。

mfc引入了另外一種mfc擴充dlls二進制共享機制。但它的使用仍受限制——隻能在mfc程式中使用。

  com通過定義二進制标準解決了這些問題,即com明确指出二進制子產品(dlls和exes)必須被編譯成與指定的結構比對。這個标準也确切規定了在 記憶體中如何組織com對象。com定義的二進制标準還必須獨立于任何程式設計語言(如c++中的命名修飾)。一旦滿足了這些條件,就可以輕松地從任何程式設計語言 中存取這些子產品。由編譯器負責所産生的二進制代碼與标準相容。這樣使後來的人就能更容易地使用這些二進制代碼。

  在記憶體中,com對象的這種标準形式在c++虛函數中偶爾用到,是以這就是為什麼許多com代碼使用c++的原因。但是記住,編寫子產品所用的語言是無關的,因為結果二進制代碼為所有語言可用。

  此外,com不是win32特有的。從理論上講,它可以被移植到unix或其它作業系統。但是我好像還從來沒有在windows以外的地方聽說過com。

我們從下往上看。接口隻不過是一組函數。這些函數被稱為方法。接口名字以大寫的i開頭,例如c++中的ishelllink,接口被設計成一個抽象基類,其中隻有純粹的虛拟函數。

  接口可以從其它接口繼承,這裡所說的繼承的原理就好像c++中的單繼承。接口是不允許多繼承的。

coclass(簡稱元件對象類——component object class)被包含在dll或exe中,并且包含着一個或者多個接口的代碼。元件對象類(coclasss)實作這些接口。com對象在記憶體中表現為元件 對象類(coclasss)的一個執行個體。注意com“類”和c++“類”是不相同的,盡管常常com類實作的就是一個c++類。 

com伺服器是包含了一個或多個coclass的二進制(dll或exe)。

注冊(registration)是建立系統資料庫入口的一個過程,告訴windows 作業系統com伺服器放在什麼位置。取消注冊(unregistration)則相反——從系統資料庫删除這些注冊入口。

guid(諧音為“fluid”,意思是全球唯一标示符——globally unique identifier)是個128位的數字。它是一種獨立于com程式設計語言的标示方法。每一個接口和coclass有一個guid。因為每一個guid都是全球唯一的,是以避免了名字沖突(隻要你用com api建立它們)。有時你還會碰到另一個術語uuid(意思也是全球唯一标示符——universally unique identifier)。uuids和guids在實際使用時的用途是一樣的。

類id或者clsid是命名coclass的guid。接口id或者iid是命名接口的guid。

在com中廣泛地使用guid有兩個理由:

guids隻是簡單的數字,任何程式設計語言都可以對之進行處理;

guids可以在任何機器上被任何人建立,一旦完成建立,它就是唯一的。是以,com開發人員可以建立自己特有的guids而不會與其它開發人員所建立的guids有沖突。這樣就消除了集中授權釋出guids的必要。

  hresult是com用來傳回錯誤和成功代碼的整型數字。除此之外,别無它意,雖然以h作字首,但沒有句柄之意。下文會對它有更多的讨論。

  最後,com庫是在你使用com時與你互動的作業系統的一部分,它常常指的就是com本身。但是為了避免混淆才分開描述的。

<a target="_blank"> </a>

  每一種語言都有其自己處理對象的方式。

 例如,c++是在棧中建立對象,或者用new動态配置設定。因為com必須獨立于語言,是以com庫為自己提供對象管理例程。下面是對com對象管理和c++對象管理所做的一個比較:

建立一個新對象

c++中,用new操作符,或者在棧中建立對象。

com中,調用com庫中的api。

删除對象

c++中,用delete操作符,或将棧對象踢出。

com中,所有的對象保持它們自己的引用計數。調用者必須通知對象什麼時候用完這個對象。當引用計數為零時,com對象将自己從記憶體中釋放。

  由此可見,對象處理的兩個階段:建立和銷毀,缺一不可。當建立com對象時要通知com庫使用哪一個接口。如果這個對象建立成功,com庫傳回所請求接口的指針。然後通過這個指針調用方法,就像使用正常c++對象指針一樣。

建立com對象

為了建立com對象并從這個對象獲得接口,必須調用com庫的api函數,cocreateinstance()。其原型如下:

以下是參數解釋:

   當你調用cocreateinstance()時,它負責在系統資料庫中查找com伺服器的位置,将伺服器加載到記憶體,并建立你所請求的coclass執行個體。 以下是一個調用的例子,建立一個clsid_shelllink對象的執行個體并請求指向這個對象ishelllink接口指針。

   首先聲明一個接受cocreateinstance()傳回值的hresult和ishelllink指針。調用cocreateinstance()來 建立新的com對象。如果hr接受到一個表示成功的代碼,則succeeded宏傳回true,否則傳回false。failed是一個與 succeeded對應的宏用來檢查失敗代碼。

删除com對象

   前面說過,你不用釋放com對象,隻要告訴它們你已經用完對象。iunknown是每一個com對象必須實作的接口,它有一個方 法,release()。調用這個方法通知com對象你不再需要對象。一旦調用了這個方法之後,就不能再次使用這個接口,因為這個com對象可能從此就從 記憶體中消失了。

  如果你的應用程式使用許多不同的com對象,是以在用完某個接口後調用release()就顯得非常重要。如果你不釋放接口,這個com對象(包含代 碼的dlls)将保留在記憶體中,這會增加不必要的開銷。如果你的應用程式要長時間運作,就應該在應用程式處于空閑期間調用 cofreeunusedlibraries() api。這個api将解除安裝任何沒有明顯引用的com伺服器,是以這也降低了應用程式使用的記憶體開銷。

繼續用上面的例子來說明如何使用release():

接下來将詳細讨論iunknown接口。

  

每一個com接口都派生于iunknown。

這個名字有點誤導人,其中沒有未知(unknown)接口的意思。它的原意是如果有一個指向某com對象的iunknown指針,就不用知道潛在的對象是什麼,因為每個com對象都實作iunknown。

iunknown 有三個方法:

addref() —— 通知com對象增加它的引用計數。如果你進行了一次接口指針的拷貝,就必須調用一次這個方法,并且原始的值和拷貝的值兩者都要用到。在本文的例子中沒有用到addref()方法;

release() —— 通知com對象減少它的引用計數。參見前面的release()示例代碼段;

queryinterface() —— 從com對象請求一個接口指針。當coclass實作一個以上的接口時,就要用到這個方法;

   前面已經看到了release()的使用,但如何使用queryinterface()呢?當你用cocreateinstance()建立對象的時候,你得到一個傳回的接口指針。如果這個com對象實作一個以上的接口(不包括iunknown),你就必須用queryinterface()方法來獲 得任何你需要的附加的接口指針。queryinterface()的原型如下:

  讓我們繼續外殼連結的例子。它實作了ishelllink 和ipersistfile接口。如果你已經有一個ishelllink指針,pisl,可以從com對象請求ipersistfile接口:

  然後使用succeeded宏檢查hr的值以确定queryinterface()的調用情況,如果成功的話你就可以象使用其它接口指針那樣使用新的接口指針,pipf。但必須記住調用pipf-&gt;release()通知com對象已經用完這個接口。

這一部分将花點時間來讨論如何在com代碼中處理串。如果你熟悉unicode 和ansi,并知道如何對它們進行轉換的話,你就可以跳過這一部分,否則還是讀一下這一部分的内容。

   不管什麼時候,隻要com方法傳回一個串,這個串都是unicode串(這裡指的是寫入com規範的所有方法)。unicode是一種字元編碼集,類似ascii,但用兩個位元組表示一個字元。如果你想更好地控制或操作串的話,應該将它轉換成tchar類型串。

  tchar和以_t開頭的函數(如_tcscpy())被設計用來讓你用相同的源代碼處理unicode和ansi串。在大多數情況下編寫的代碼都是 用來處理ansi串和ansi windowsapis,是以在下文中,除非另外說明,我所說的字元/串都是指tchar類型。你應該熟練掌握tchar類型,尤其是當你閱讀其他人寫的 有關代碼時,要特别注意tchar類型。

  當你從某個com方法傳回得到一個unicode串時,可以用下列幾種方法之一将它轉換成char類型串:

調用 widechartomultibyte() api;

調用crt 函數wcstombs();

使用cstring 構造器或指派操作(僅用于mfc );

使用atl 串轉換宏;

你可以用widechartomultibyte()将一個unicode串轉換成一個ansi串。此函數的原型如下:

codepage:unicode字元轉換成的代碼頁。你可以傳遞 cp_acp來使用目前的ansi代碼頁。代碼頁是256個字元集。字元0——127與ansi編碼一樣。字元128——255與ansi字元不同,它可

以包含圖形字元或者讀音符号。每一種語言或地區都有其自己的代碼頁,是以使用正确的代碼頁對于正确地顯示重音字元很重要。

dwflags:dwflags 确定windows如何處理“複合” unicode字元,它是一種後面帶讀音符号的字元。

如è就是一個複合字元。如果這些字元在codepage參數指定的代碼頁中,不會出什麼事。

否則,windows必須對之進行轉換。 傳遞wc_compositecheck使得這個api檢查非映射複合字元。

傳遞wc_sepchars使得windows将字元分為兩段,即字元加讀音,如e`。

傳遞wc_discardns使得windows丢棄讀音符号。

傳遞wc_defaultchar使得windows用lpdefaultchar參數中說明的預設字元替代複合字元。

預設行為是wc_sepchars。

lpwidecharstr 要轉換的unicode串。

cchwidechar lpwidecharstr在unicode 字元中的長度。通常傳遞-1,表示這個串是以0x00結尾。

lpmultibytestr 接受轉換的串的字元緩沖 cbmultibyte lpmultibytestr的位元組大小。

lpdefaultchar 可選——當dwflags包含wc_compositecheck | wc_defaultchar并且某個unicode字元不能被映射到同等的ansi串時所傳遞的一個單字元ansi串,包含被插入的“預設”字元。可以

傳遞null,讓api使用系統預設字元(一種寫法是一個問号)。

lpuseddefaultchar 可選——指向bool類型的一個指針,設定它來表示是否預設字元曾被插入ansi串。可以傳遞null來忽略這個參數。

  我自己都有點暈菜了……!,萬事開頭難啊……,不搞清楚這些東西就很難搞清楚com的串處理。何況文檔中列出的比實際應用的要複雜得 多。下面就給出了如何使用這個api的例子:

調用這個函數後,szansistring将包含unicode串的ansi版本。 調用這個函數後,szansistring将包含unicode串的ansi版本。

這個crt函數wcstombs()是個簡化版,但它終結了widechartomultibyte()的調用,是以最終結果是一樣的。其原型如下:

  wcstombs()在它對widechartomultibyte()的調用中使用wc_compositecheck | wc_sepchars标志。用wcstombs()轉換前面例子中的unicode串,結果一樣:

mfc中的cstring包含有構造函數和接受unicode串的指派操作,是以你可以用cstring來實作轉換。例如:

  atl有一組很友善的宏用于串的轉換。w2a()用于将unicode串轉換為ansi串(記憶方法是“wide to ansi”——寬字元到ansi)。實際上使用ole2a()更精确,“ole”表示的意思是com串或者ole串。下面是使用這些宏的例子:

  ole2a()宏“傳回”轉換的串的指針,但轉換的串被存儲在某個臨時棧變量中,是以要用lstrcpy()來獲得自己的拷貝。其它的幾個宏是w2t()(unicode 到 tchar)以及w2ct()(unicode到常量tchar串)。

  有個宏是ole2ca()(unicode到常量char串),可以被用到上面的例子中,ole2ca()實際上是個更正宏,因為lstrcpy()的第二個參數是一個常量char*,關于這個問題本文将在以後作詳細讨論。

  另一方面,如果你不想做以上複雜的串處理,盡管讓它還保持為unicode串,如果編寫的是控制台應用程式,輸出/顯示unicode串時應該用全程變量std::wcout,如:

  但是要記住,std::wcout隻認unicode,是以你要是“正常”串的話,還得用std::cout輸出/顯示。對于unicode串文字量,要使用字首l标示,如:

如果保持串為unicode,程式設計時有兩個限制:

必須使用wcsxxx() unicode串處理函數,如wcslen();

在windows 9x環境中不能在windows api中傳遞unicode串。要想編寫能在9x和nt上都能運作的應用,必須使用tchar類型,詳情請參 考msdn;

下面用兩個例子示範本文所講的com概念。

代碼中還包含了本文的例子工程。

使用單接口com對象

   第一個例子展示的是單接口com對象。這可能是你碰到得最簡單的例子。它使用外殼中的活動桌面元件對象類(clsid_activedesktop)來獲得目前桌面牆紙的檔案名。請确認系統中安裝了活動桌面(active desktop)。 以下是程式設計步驟:

初始化com庫。 (initialize);

建立一個與活動桌面互動的com對象,并取得iactivedesktop接口;

調用com對象的getwallpaper()方法;

如果getwallpaper()成功,則輸出/顯示牆紙檔案名;

釋放接口(release());

收回com庫(uninitialize);

   在這個例子中,輸出/顯示unicode 串 wszwallpaper用的是std::wcout。

使用多接口的com對象

   第二個例子展示了如何使用一個提供單接口的com對象queryinterface()函數。其中的代碼用外殼的shell link元件對象類建立我們在第一個例子中獲得的牆紙檔案的快捷方式 。以下是程式設計步驟:

初始化 com 庫;

建立一個用于建立快捷方式的com 對象并取得ishelllink 接口;

調用ishelllink 接口的setpath()方法;

調用對象的queryinterface()函數并取得ipersistfile接口;

調用ipersistfile 接口的save()方法;

釋放接口;

收回com庫;

   這一部分準備用succeeded 和

failed宏進行一些簡單的出錯處理。主要是深入研究從com方法傳回的hresult,以便達到完全了解和熟練應用。

  hresult是個32位符号整數,其非負值表示成功,負值表示失敗。hresult有三個域:程度位(表示成功或失敗),功能碼和狀态碼。功能碼表 示hresult來自什麼元件或程式。微軟給不同的元件多賦予功能碼,如:com、任務排程程式等都有功能碼。功能碼是個16位的值,僅此而已,沒有其它 内在含義;它在數字和意義之間是随意關聯的;類似getlasterror()傳回的值。

  如果你在winerror.h頭檔案中查找錯誤代碼,會看到許多按照[功能]_[程度]_[描述]命名規範列出的hresult值,由元件傳回的通用的hresult(類似e_outofmemory)在名字中沒有功能碼。如 :

regdb_e_readregdb:

功能碼 = regdb, 指“系統資料庫資料庫(registry database)”;

程度 = e 意思是錯誤(error);

描述 = readregdb 是對錯誤的描述(意思是不能讀系統資料庫資料庫)。 s_ok: 沒有功能碼——通用(generic)

hresult;

程度=s;表示成功(success);

ok 是狀态描述表示一切都好(everything''s ok)。

   好在有一種比察看winerror.h檔案更容易的方法來确定hresult的意思。使用vc提供的錯誤查找工具(error lookup)可以輕松查到為hresult内建功能碼。例如,假設你在cocreateinstance()之前忘了調用 coinitialize()。cocreateinstance()傳回的值是0x800401f0。你隻要将這個值輸入到錯誤查找工具按“look up”按鈕,便可以看到錯誤資訊描述“尚未調用coinitialize”如下圖所示:

Com程式設計入門——什麼是COM,如何使用COM

  另外一種查找hresult描述的方法是在調試器中。假設有一個hresult變量是hres。在watch視窗的左邊框中輸入“hres,hr”,表示想要看的值,“hr”便會通知vc顯示hresult所描述的值。如下圖所示:

Com程式設計入門——什麼是COM,如何使用COM

通過以上的讨論,想必你對com程式設計有了初步的認識,本文第二部分将探讨com的内部機制。教你如何用c++編寫自己的接口。

繼續閱讀