Delphi 中 COM 實現研究手記(一)

前言

 

    前些日子用 Delphi 寫了一個 Windows 外殼擴展程序,大家知道 Windows 外殼擴展實際上就是 COM 的一種應用 -- Shell COM,雖然整個程序寫得還算比較順利,但寫完後還是感覺對 Delphi 中 COM 的實現有點霧裏看花的感覺,因此我認爲有必要花一點時間對 COM 在 Delphi 中的實現做一些研究。另外我也買了李維的新書 --《深入核心 -- VCL架構剖析》,裏面有兩章涉及了與 COM 相關內容,看完後我知道了COM 在 Delphi 中的實現是基於接口(Interface),而 Delphi 中的接口概念又起源於對 COM 的支持,總之他們之間互相影響,發展成接口在 Delphi 中已經是 First-Class 的地位,並且完全擺脫 COM 而獨立存在。
    本系列文章側重於描述 COM 在 Delphi 中的實現手法,主要配合 VCL 源碼片斷進行分析,不會涉及過多的基本概念,因此要求讀者有一定的 COM 和 接口概念,可以參考我在文章末尾列出的文獻。本篇主要講 COM 對象在 Delphi 中的創建過程。


正文

 

   爲了讓讀者能跟着我的分析輕鬆讀完本篇文章,我引用文獻[2]中的範例做解釋,但爲了更清楚地闡述問題,我改寫了部分代碼。所有分析請在 Delphi 7 上測試。
   在 Delphi 中首先通過選擇菜單 File-->New-->Other...新建一個 ActiveX Library 並保存名稱爲 SimpleComServer,再新建一個 COM Object,在COM Object Wizard 中將對象命名爲 SimpleCOMObject,Options 中的兩個複選框都可以不必選中其他的保持默認, 現在 COM服務器端的框架已經建立起來了。剩下的就是需要我們把聲明的接口 ISimpleCOMObject 的代碼實現。

完成服務器端的代碼後,我們需要寫一個客戶端小程序來執行服務器端內的接口代碼,我僅列出由我改寫的關鍵代碼部分

 

現在開始進入主題,跟隨我一起走進 Delphi 的 COM Framework 世界吧。我主要從客戶端程序創建 COM 對象來剖析 VCL 源碼。
   客戶端代碼中我用兩種獲得創建 SimpleCOMObject 對象並獲得 ISimpleCOMObject 接口,一旦獲得接口,你就可以自由地使用接口指定的方法了。
    讓我們先看看 Button1Click 裏如何創建 COM 對象的。代碼調用了 CoGetClassObject 獲得創建 SimpleCOMObject 對象的類工廠 -- IClassFactory 接口,緊接着又通過調用該接口的 CreateInstance 方法創建了真正的 SimpleCOMObject 對象實例,返回 ISimpleComObject 接口指針。 那麼上面整個過程在 VCL 中是如何實現的呢?讓我們先從 CoGetClassObject 這個API 說起。
    CoGetClassObject 是 Windows 的一個標準 COM API,該函數存在於 OLE32.DLL中,它是 Windows COM DLL 之一。函數先根據系統註冊表中的信息,找到類標識符 CLSID 對應的組件程序(即服務器端程序,我們這裏討論的是一個 DLL 文件)的全路徑,然後調用 LoadLibrary(實際上是 CoLoadLibrary)函數初始化服務器(Dll 被加載到客戶程序進程中)並調用組件程序的 DllGetClassObject 輸出函數。DllGetClassObject 函數負責創建相應的類廠對象,並返回類廠對象的 IClassFactory 接口。至此 CoGetClassObject 函數的任務完成,然後客戶程序繼續調用類廠對象的 CreateInstance 成員函數,由它負責 COM 對象的創建工作。
    注意:Windows COM 規範中指定你必須在服務器中完成並輸出 DllGetClassObject,如果這個沒有被發現,Windows 將不能傳遞對象到客戶端,DllGetClassObject 將是進入我們的 dll(COM 服務器)的入口點。
    從上面的一番簡要陳述不難看出獲得 IClassFactory 接口是通過調用服務器端的 DllGetClassObject 函數獲得的,傳奇實際也就是從這個輸出函數開始的。讓我們看看它是如何實現的(如果源碼中我附加了註釋,請一定仔細看看,下面不再提示):

ComClassManager 是什麼?它是我們需要介紹的 Delphi COM Framework 中的第一個類。

     每個服務器端內存在一個 TComClassManager 實例,即ComClassManagerVar 全局對象變量,它負責管理 COM 服務器中的所有類工廠(class factory)對象(本例中只有一個類工廠)。而類工廠又是什麼時候創建的?其實我前面已經列出了,COM Object Wizard 生成的 SimpleCOMObject 的骨架代碼的 Initialization 部分已經自動爲我們創建一個 TComObjectFactory 對象:

    Delphi關鍵字Initialization提示我們 dll 在被載入客戶端程序進程空間時,負責創建 impleCOMObject 對象的類工廠 TComObjectFactory 就已經被創建了。我們知道,一個服務器端裏可以包含多個 COM 對象,並且每一個獨立的 COM 對象都必須相應有創建該類的類工廠,假如你設計的服務器端裏有十個 COM 對象,那麼肯定會有十個負責創建不同類的類工廠,這十個類工廠在程序初始化時都會被一一創建出來。這個概念一定在你的頭腦中建立起來,否則後面就不好理解了。再提示一下,VCL 中定義了數種 ClassFactory 類,分別負責某一種類型的 COM 對象創建,TComObjectFactory 是其中最簡單的一種[1]。那麼 ComClassManager 和 TComObjectFactory 又是如何聯繫到一起呢?看看 TComObjectFactory 的 Constructor:

再看看 ComClassManager 相關實現代碼:

   ComClassManagerVar 維護着服務器中的所有的類工廠的一個鏈表,每個單一類工廠的實例都是自動初始化,在我們的服務器 Initialization 節你可以看到,並自動將自己添加到 ComClassManager 的鏈表(FactoryList)中。現在想想,這樣的設計是不是非常棒。
    請跟隨我繼續往下走。當客戶端要求 DllGetClassObject 返回指定創建的類工廠,在函數內部調用了 TComClassManager 的 GetFactoryFromClassID 方法。該方法遍歷 FactoryList 鏈表,根據 ClassID 找到對應的類工廠,並返回類工廠對象實例。

   對上面的代碼分析我再多說一下,鏈表 FFactoryList 變量實際就是 TComObjectFactory 類型,TComObjectFactory 創建時就獲得了豐富的關於它要創建的相關 COM 對象信息,例如在我們這個範例裏,ClassFactory 知道了它要創建的 COM 對象類型是 TSimpleComObject, ClassID 是 Class_SimpleComObject..等等,這些都爲類工廠在創建相關類以及一些輔助方法(函數)都提供了極爲重要的信息

    DllGetClassObject 獲得正確的類工廠對象之後調用它的 GetInterface 方法,這個方法實際上是繼承自 TObject.GetInterface,Delphi 爲每一個帶有 GUID 的接口設計了一個記錄結構 -- TInterfaceEntry 記錄,實現 IClassFactory 接口的 TComObjectFactory 對象 VMT 中的 vmtIntfTable 指向一個 TInterfaceTable 記錄, 該記錄包含有它實現的接口數量(IUnknown、IClassFactory)、相應接口的 TInterfaceEntry 記錄等信息,通過查詢 IClassFactory 接口相應 TInterfaceEntry 記錄中的 IOffset 域獲得該接口在  TComObjectFactory 對象實例中的正確位置,並返回指向該位置的 IClassFactory 接口指針[1][3]。

   至此,CoGetClassObject 內部調用服務器端的 DllGetClassObject 已經正確獲得了負責創建 SimpleCOMObject 對象的 IClassFactory 接口。在獲得這個接口後,就可以調用它的方法 CreateInstance 創建 SimpleCOMObject 對象並返回 ISimpleCOMObject 接口,現在你可以對 ISimpleCOMObject 接口任意進行操作了

 

   讓我們再看看 ButtonClick2 中是如何創建 SimpleCOMObject 對象的。
    ButtonClick2 是調用 CreateComObject 函數創建 SimpleCOMObject 對象的。 CreateComObject 函數只是對 COM API -- CoCreateInstance 的一個簡單包裝。爲什麼要包裝它,你可以看一下 CoCreateInstance 的參數就知道爲什麼了,參數多且複雜,這是 Windows API 的通病,而 VCL 實現卻很體貼我們,它傳遞 CLSID 作爲唯一的參數,其實平時應用中我們創建的大部分 COM 對象都是 CLSID 已知,並且對象是駐留在本地或進程內服務器的指定對象。

    CoCreateInstance 也存在於 OLE32.DLL中,其內部也是先調用 CoGetClassObject 函數,返回負責創建 SimpleCOMObject 的IClassFactory 接口,然後也還是調用該接口的 CreateInstance 創建 SimpleCOMObject 並返回該對象的 IUnknown 接口,到這一步,與Button1Click 中創建 SimpleCOMObject 的實現方法區別在於 Button1Click 通過 ClassFactory 的 CreateInstance 直接返回 ISimpleCOMObject 接口而不是它的 IUnknown 接口,其他的並沒有什麼區別,相對 Button1Click 的方法更直觀。在獲得了 SimpleCOMObject 的 IUnknown 接口之後,我們並不能立即用此接口去調用 ISimpleCOMObject 的方法,爲了和對象通信,必須先將它轉換成 ISimpleComObject 接口。那麼有讀者會問爲什麼 CreateComObject 不設計成能直接返回需要的接口呢,我想還是爲了簡化這個函數的使用吧。獲得 ISimpleComObject 接口可以通過調用 IUnknown 接口的 QueryInterface 方法查詢 SimpleCOMObject 對象是否支持該接口, Delphi 爲我們提供了更簡單的方法 -- “AS”關鍵字。先讓我們看看 As 在幕後到底爲我們做了什麼(Debug 狀態下的反彙編源碼):

可以看到, AS 被轉換成調用 @IntfCast,即 system 單元的 _IntfCast 函數。呵呵,其實就是調用 IUnknown 接口的 QueryInterface 方法。

由此可見,第二種方法也可以按照下面的方法調用:

   至此兩種創建 SimpleCOMObject 對象的方法全部分析完畢。那麼在平時的應用中我們到底使用哪種方法創建 COM 對象比較好呢?其實在 Delphi 的官方幫助中已經給了我們答案:當你只創建單一 COM 對象時,你可以調用 CreateComObject;當你需要成批創建同一類 COM 對象時,那麼還是直接選擇類工廠吧,還是它來得快。
    在我分析後,你是否認爲複雜的 COM 結構被 VCL 包裝得很完美?至少我認爲是這樣的,使我不得不佩服 Borland Delphi R&D 小組的高超技術水準。如果你還沒盡興,那麼等我的下篇吧...

 

參考文獻

1. 李維.《深入核心 -- VCL架構剖析》第六、七章

2.
Fernando Vicaria."Delphi COM In-Process Servers Under the Microscope, Part 1". Hardcore Delphi Magazine, Mar 2000

3. savetime."Delphi 的接口機制淺探", Feb 2004

4. savetime."《COM 原理與應用》學習筆記", Feb 2004

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章