Compiler 角度看待 COM DCOM

作者:吳明皓
2002 8 16日星期五

關於 COM DCOM讀者在很多程式語言的書籍之中,找到介紹它們的撰寫方法,網路上也有很多的官方資料可以提供參考。所以我在這個地方並不打算介紹如何去寫它們,而是嘗試用 Compiler 的角度來詮釋一下什麼是 COMDCOM。這篇文章在 1999 1 月曾發表在某家資訊公司的技術論壇之中,現在我重新整理後,再次發表於『點空間』網站,希望有興趣的人能藉此更了解 COM DCOM 的運作方式。

一、Function Main Code

在絕大部分的程式語言中,都可以將程式碼拆解成主程式部分(Main Code),也就是所謂的程式進入點的那段程式碼部分,以及函數部分(Function Procedure)。而拆解的目的在於模組化我們的程式,並藉此讓程式更為有條不紊,我們以 C 語言為例:

 

Main

{   a = = GotEmployeeName(code: Integer);

     。。。。。

}

 

function  GotEmployeeName(code: Integer)

{

     。。。。

}

 

上面的程式碼一個常見的標準寫法,然而經過編譯器編譯之後載入記憶體中執行,它將會呈現類似以下的樣子,主程式碼與函數分別位於不同的記憶體位址中。

 

 

 

我們將上面的範例,部分換成機器碼的示意圖,如下所示,我們將可以發現,其實編譯器對於指令「a == GotEmployeeName(code: Integer);」的做法是將參數 code 放入堆疊中,然後程式跳躍至 F12D:FFFF 的位址之上,執行完畢後再將結果指派給變數 a。當然這邊所指的位址是相對位址,而不是實體的真實位址。(詳細的技術細節請參閱編譯器及作業系統等相關資料)

 

上面的範例我們可以看到函數與主程式之間,是位於同一個程式 Source Code 之中,然而在絕大部分的情況之下,這樣的模組化程度並不夠。因為首先當系統開始龐大的時候,Source Code 也會日趨龐大,再則龐大的 Source Code 會使得維護上發生困難,同時也使得編譯的過程中,耗費大量的時間。

二、靜態連結機制(Include)。

為了解決上個小節所描述的問題,在 C 的程式語言中,提供一個所謂靜態連結的方法,全名稱之為「Static Library Link」如下面的範例所示。這樣的做法是將所謂的常用且公用函數,獨立出來,到一個所謂函數庫的『*.h』檔案中,然後在主程式裡,只要利用 #include 的指令,編譯器就會在編譯的過程,到函數庫的『*.h』檔案中,找到相對應的函數程式碼,並且將之編譯到執行檔中。

 

#include <io.h>

Main

{    printf(‘Hello World !!’);

     。。。

};

 

下圖所示主程式與函數庫之間分別位於不同的檔案之中。

 

下圖則表示雖然 Source Code 位於不同的檔案中,但是編譯完後,還是會將程式彙整到同一個執行檔中。

 

 

那麼編譯器是如何在編譯的過程之中,從函數庫裡面搜尋相對應的程式碼呢?我們從下圖中可以窺之一二。這是 IO.h 檔案的結構,編譯器就是依照檔案中的 Function Table,以查表的方式找到相對應的程式碼的,這個概念非常重要,因為在後面的章節裡,我們都可以看到相類似的情形。

 

 

在這個地方雖然省略掉許多編譯器詳細的編譯過程,但是我要強調的是,雖然在 Source Code 的部分,函數庫與主程式之間是分開的,也達成所謂 Source 分享的目的,但是所編譯出來的執行檔,還是包括了主程式與函數的部分,因此編譯所需要的時間,與前一小節所提到的是一模一樣的,所以如果程式很龐大的話,編譯起來依舊很費時。而且重點是,所謂的公用函式庫通常異動的機率很小,如果每次的編譯,都是要將之重新編譯過一次,實在不符合成本上的效益。

然而在 Delphi 中的靜態連結則又是如何做的呢?我們從下面的範例中觀察得到,其實就是利用『Uses』的指令來做的。而 Delphi 編譯器對於靜態連結的做法,除了使用者自訂的 Unit 單元與  C 語言相同外,其餘 Delphi 預設的 Unit 單元,如 WindowsMessages等等,都是採Object Code 方式作連結的。不過這是另外一個話題,在這個地方讀者只要知道『靜態連結』是怎麼樣一回事就好了。

 

unit ;

interface

uses

  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,

  Db, DBClient, MConnect, MidasCon;

type

  TDMThinkEC = class(TDataModule)

    RemoteServer: TRemoteServer;

  private

  public

end;

 

 

三、動態連結機制(DLL)。

在上面的小節曾經提到,靜態鏈結對於編譯期間不符合時間成本效益的問題,於是有人想到如果我們把靜態的函式庫事先編譯起來,然後提供一個機制,讓執行檔在執行時期呼叫,不就可以解決上述的問題,沒錯!這正是動態連結機制的由來,全名稱之為「Dynamic Link Library」簡稱「DLL」。那麼動態連結機制是怎麼做出來的呢?在這裡我們先看看 Delphi 是怎麼達成的。

 

library MyTest;

uses  SysUtils, Windows, Classes;

 

function IsDBCSLead(ch: char): BOOL;

begin      。。。

end;

 

procedure ShowForm;

begin      。。。

end;

 

function GotEmployeeName(code: Integer): String;

begin      。。。

end;

 

Exports  ISDBCSLead Index 1;

Exports  ShowForm Index 2;

Exports  GotEmployeeName Index 3;

 

begin

end.

 

上面的程式碼是一個標準 Delphi DLL 原型,而重點則是在於 Exports 指令。原來我們在將 Source Code 編譯成 DLL 時,會依照 Exports 指令將函數名稱編列到 Function Table當中,請注意 Exports 指令後面的 Index 識別字,那就是指定該函數在 Function Table 中的索引位置。於是我們在主程式之中,可以使用下面的三種方式呼叫 DLL function

 

function IsDBCSLead(ch: char): Bool; External 'MyTest.dll' ;

Or

function IsDBCS(ch: char): Bool; External 'MyTest.dll' name ‘IsDBCSLead’;

Or

function IsDBCSLead(ch: char): Bool; External 'MyTest.dll' index 1;

 

嗯!至此『動態連結機制』果然幫我們解決許多問題,也可以將系統的模組化,更能具體的實踐。但是有一個問題卻出現了,那就是我們呼叫 DLL 過程當中,必須非常明確的指定 DLL 的位置,否則便會在執行時期出現錯誤。此外不同程式語言所開發出來的 DLL 在呼叫方式,也會略有不同,呼叫時必須經過轉換的過程,特別是在主程式與 DLL 之間是用不同的程式語言開發的時候(Delphi 的使用過程中,最明顯的就是 Windows API 的呼叫,有經驗的人應該知道光是 C Delphi 之間型別的轉換與呼叫的方式,就會讓人頭疼得老半天)。此外,每一個 DLL 版本與主程式之間,也存在著版本依存的關係。也就是如左圖中所示,當 DLL 因功能修正而改版時,主程式非常有可能必須跟著作修正,這樣的結果對於以團隊開發的模式,會造成很多的不便。於是 COM 的出現正是為此目的應運而生的。

另外由於 DLL 是獨立於主程式外面的,所以主程式在呼叫 DLL 中的函數時,必須利用載入的方式,把 DLL 載入到記憶體當中(換言之,就是主程式在未呼叫函數之前,是不會做載入動作的)。因此在第一個小節的示意圖中,主程式關於函數在記憶體的表現方式『a == F12DFFFF;』在 DLL 的使用範例裡,可以視為被編譯器放入一個虛假的位址,而當到了執行時期,主程式執行到這行指令,並且完成 DLL 載入動作後,才會填入正確位址到裡面。這樣的結果會使得程式的主控權,暫時的離開主程式的程式區段,而轉移到 DLL 的程式區段中。而這樣的現象,可能使一些資深的程式設計師,遭遇到以下的問題,那就是某些原先在主程式裡可以正確執行的函數,搬移到 DLL 就失敗了。其主要的原因,大都是程式主控權在往返於主程式與 DLL 之間所造的。不過這已經是屬於另外一個議題了,我會在另外的文章中討論,在這裡讀者只需了解 DLL 的運作方式就可以了。

 

四、元件型態物件模型 ( COM )。

COM(Component Object Model)在很多書籍中的介紹大多偏向於它的嵌入能力,如左圖所示。COM Object 可以很容易的以嵌入的方式,變成其他系統的部分功能,例如我們在 MS Word 之中以插入物件的方式,可以將專門繪製統計圖表的 Microsoft Graphic 2000 COM Object 插入到 Word 之中,使之成為 Word  文件的一部分。如下圖所示。然而這樣的功能既酷且炫,而在 Delphi 之中我們同樣可以做到。例如我們在 Delphi IDE 環境中,元件盤的 ActiveX 頁可以找到一些系統預設加入的 COM 元件,當然讀者如果願意的話可以以手工的方式在 IDE 環境中,選擇 Compoment | Import Active Control 的功能加入其他的 COM 元件,WinFaxMSXML 等等(註:ActiveX COM 的差別在於 ActiveX 延伸自 COM 元件,他除了允許讓元件可以嵌入其他的 AP 中,更可以嵌入 IE 之中流通於Intranet / Internet之上)不過這不是我要介紹的重點,我要介紹的是從編譯器的角度要如何去看待COM這件事情。

首先 COM 存在的實體是什麼?我們翻翻 Delphi 的官方手冊,我們可以發現,它可以多種型態來表現,例如 exedllocx 等。只是它比傳統的 DLL 比較起來多出了很多資訊,如右圖所示。這些資訊大多以包裹許多方法(Method)、屬性(Property)所組成的介面(Interface)為主。而這些介面則負責起控制整個 COM 元件的橋樑,這樣的做法,與傳統 DLL Function Table 類似,但是在這裡不叫作 Function Table,而是叫做 Type Library因此我們在 Delphi IDE 環境中,嘗試開啟一個屬於 COM dllexeocx 等的檔案,那麼我們就可以看到一個如左圖的畫面。這是一個詳細列出介面(Interface),以及所屬的方法(Method) 、屬性(Property)的畫面,在這裡我們且不討論如何操作它,我們先討論 COM 的運作方式。

首先我們在 Delphi 中先以 New | Project 產生專案,然後再以 New | Other | ActiveX | Automation Object 的方式產生一個名叫 CoMyCOMObject COM 物件,然後在 Delphi 附帶產生的『*.TLB』檔中,我們可以看到如下的定義:

 

const

  Project1MajorVersion = 1;

  Project1MinorVersion = 0;

 

  LIBID_Project1: TGUID = '{0E13E0B4-389B-4A63-9AAE-CAFC3E61DD1D}';

 

  IID_ICoMyCOMObject: TGUID = '{899438F6-AAE8-4BA0-8D47-1EC3137F8676}';

  CLASS_CoMyCOMObject: TGUID = '{3B5CD863-CD90-4895-ACD7-7183553CAB02}';

type

 

  ICoMyCOMObject = interface;

  ICoMyCOMObjectDisp = dispinterface;

 

  CoMyCOMObject = ICoMyCOMObject;

 

  ICoMyCOMObject = interface(IDispatch)

    ['{899438F6-AAE8-4BA0-8D47-1EC3137F8676}']

    procedure SayHello; safecall;

  end;

 

  ICoMyCOMObjectDisp = dispinterface

    ['{899438F6-AAE8-4BA0-8D47-1EC3137F8676}']

    procedure SayHello; dispid 1;

  end;

 

  CoCoMyCOMObject = class

    class function Create: ICoMyCOMObject;

    class function CreateRemote(const MachineName: string): ICoMyCOMObject;

  end;

 

implementation

 

uses ComObj;

 

class function CoCoMyCOMObject.Create: ICoMyCOMObject;

begin

  Result := CreateComObject(CLASS_CoMyCOMObject) as ICoMyCOMObject;

end;

 

class function CoCoMyCOMObject.CreateRemote(const MachineName: string): ICoMyCOMObject;

begin

  Result := CreateRemoteComObject(MachineName, CLASS_CoMyCOMObject) as ICoMyCOMObject;

end;

 

其中我們看到 LIBID_Project1 的屬性是 TGUID,這不就是剛好是 Windows Registry 機碼的類別型態嗎?於是我們再利用 RegEdit.exe 搜尋一下 {0E13E0B4-389B-4A63-9AAE-CAFC3E61DD1D}' 這個機碼,結果我們發現,在 HKEY_CLASSES_ROOT\TypeLib 路徑下找到它,於是我們檢查一下這個機碼,發現 HKEY_CLASSES_ROOT\TypeLib\{0E13E0B4-389B-4A63-9AAE-CAFC3E61DD1D}\1.0\0路徑下,機碼 win32 的內容,恰好就是我們剛剛產生的 COM  元件正確的實體位置。

 

 

嗯!這是一條很好的線索,於是我們回頭在看看『*.TLB』的 Source Code,我們發現這樣的程式碼: 

 

 

class function CoCoMyCOMObject.Create: ICoMyCOMObject;

begin

  Result := CreateComObject(CLASS_CoMyCOMObject) as ICoMyCOMObject;

end;

 

我們從 CreateComObject 函數所傳入的參數,以及傳回值來看,不就是利用程式中定義的 CLASS_CoMyCOMObject 機碼值取得我們剛剛建立, COM 物件中的介面嗎?(註:IUnknown 是恰為 Delphi 表示 COM 介面所用的類別)因此我們再追蹤一下 CreateComObject 函數到底在做些什麼事?

 

function CreateComObject(const ClassID: TGUID): IUnknown;

begin

  OleCheck(CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or

    CLSCTX_LOCAL_SERVER, IUnknown, Result));

end;

 

上面的程式碼中 CoCreateInstance 函數,是一個利用傳入機碼值,來傳回一個特定 CLSID 的函數,而在這個範例,則是傳回 LIBID_Project1 CLSID。然後程式回到 CoCoMyCOMObject.Create 之中,再經過型別轉換成 ICoMyCOMObject Interface 類別,此後,我們就可以開始呼叫剛剛所建立 COM 元件的方法了。哇!好曲折的一段,費了一番功夫才說明清楚。

另外 Delphi 還提供了一個呼叫 COM 元件的方法就是

function CreateOleObject(const ClassName: string): IDispatch;

我們順便看一下它是怎樣運作的。

 

function CreateOleObject(const ClassName: string): IDispatch;

var  ClassID: TCLSID;

begin

  ClassID := ProgIDToClassID(ClassName);

  OleCheck(CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or

    CLSCTX_LOCAL_SERVER, IDispatch, Result));

end;

 

從上面的程式碼中,看到只是多了一段 ClassID := ProgIDToClassID(ClassName); 這樣的指令,而 ProgIDToClassID 的原形則是如下面的樣子:

 

function ProgIDToClassID(const ProgID: string): TGUID;

begin

  OleCheck(CLSIDFromProgID(PWideChar(WideString(ProgID)), Result));

end;

 

其中 CLSIDFromProgID 則是轉換 Interface Class 名稱成為 CLSID 的函數,其餘的與 CoCoMyCOMObject.Create 函數一模一樣,這個方法倒是挺方便的。

 

此外我們在上面的小節中曾經介紹過 DLL 與主程式之間版本管控的問題,在 COM 中這個問題已經可以獲得一個比較好的解決。原來在 COM 的結構裡,由於是透過介面來達成呼叫的動作,而且 COM 又允許多個介面存在,因此,當我們因為客戶的需求,更改 COM 的內容時,對於舊版的主程式,因為我們只要保留 COM 舊的介面,而對於新版的主程式,我們則開始藉由新的介面來呼叫函數,如右圖所示,如此對於新舊主程式之間,都不會有 COM 不同版本困擾的問題了。這對於以團隊開發的環境而言,是非常重要的。因為我們自此之後,都不用擔心在團隊之中,某個成員異動了某個共同元件後,造成整套產品皆不堪使用的窘境會發生。

 至於 COM+ 則又是 COM 的另外一種衍生,它加入一個所謂類似生命週期的狀態相關資訊,讓 COM 元件能夠在 MTSMicrosoft Transaction Server)中加以管理,以達到所謂 Object Pool 的功能,不過這又另外的議題了,在這裡讀者先暫時只需知道,COM 與傳統 DLL 運作上的異同即可。

經過上面這麼曲折的追蹤後,相信各位讀者已經了解 COM 的運作方式,其實與傳統的 DLL 原理是差不多的。只是 COM 把一些增加的資訊,複製一份到 Windows Registry 當中,然後再利用 Registry 的特性讓每一個使用 COM 元件的 AP,都能順利地存取得到它的服務。嗯!這樣的機制的確符合 Component Object Model 的精神,也解決掉傳統 DLL 於服務共享上的問題。

五、分散式物件模型 (DCOM) 。

上一個小節中提到 COM 對於團隊的開發,與應用系統的切割,提供了一個非常良好的機制,但是這個機制僅能在單一的 Windows 作業系統上運行,這對於日趨興盛的網路環境,以及多層應用程式服務概念的興起時代裡,這樣的架構是不夠的,於是 Microsoft 便提出分散式 COM 的機制,也就是所謂的 DCOM(Distribute COM)機制,讓應用程式跨越機器與機器之間的鴻溝,藉由網路取得另一台機器上的 COM 服務。如右圖所示在每一台機器上藉由 DCOM 服務的機制,每台機器上的 AP 都可以取得其他機器上,所提供的服務,真正發揮分散式架構的好處。

在前面的小節中,我們已經清楚 COM 是怎樣的運作,現在我們再看看 DCOM 又是怎樣的運作方式。同樣的我們藉由 Delphi 產生的『*.TLB』檔案來觀察:

 

const

  // TypeLibrary Major and minor versions

  Project1MajorVersion = 1;

  Project1MinorVersion = 0;

 

  LIBID_Project1: TGUID = '{B24EAA7C-A265-44FA-9E5F-8F3F306BBBB3}';

 

  IID_ICoMyDCOMObject: TGUID = '{90FE2295-BF3C-4C29-B755-B91F5AAA0092}';

  CLASS_CoMyDCOMObject: TGUID = '{0C6F00F5-1F8D-4E9C-8FE2-32D342AFD0BA}';

type

 

  ICoMyDCOMObject = interface;

  ICoMyDCOMObjectDisp = dispinterface;

 

  CoMyDCOMObject = ICoMyDCOMObject;

 

  ICoMyDCOMObject = interface(IAppServer)

    ['{90FE2295-BF3C-4C29-B755-B91F5AAA0092}']

    procedure SayHello; safecall;

  end;

 

  ICoMyDCOMObjectDisp = dispinterface

    ['{90FE2295-BF3C-4C29-B755-B91F5AAA0092}']

    procedure SayHello; dispid 1;

    function AS_ApplyUpdates(const ProviderName: WideString; Delta: OleVariant; MaxErrors: Integer;

                             out ErrorCount: Integer; var OwnerData: OleVariant): OleVariant; dispid 20000000;

    function AS_GetRecords(const ProviderName: WideString; Count: Integer; out RecsOut: Integer;

                           Options: Integer; const CommandText: WideString; var Params: OleVariant;

                           var OwnerData: OleVariant): OleVariant; dispid 20000001;

    function AS_DataRequest(const ProviderName: WideString; Data: OleVariant): OleVariant; dispid 20000002;

    function AS_GetProviderNames: OleVariant; dispid 20000003;

    function AS_GetParams(const ProviderName: WideString; var OwnerData: OleVariant): OleVariant; dispid 20000004;

    function AS_RowRequest(const ProviderName: WideString; Row: OleVariant; RequestType: Integer;

                           var OwnerData: OleVariant): OleVariant; dispid 20000005;

    procedure AS_Execute(const ProviderName: WideString; const CommandText: WideString;

                         var Params: OleVariant; var OwnerData: OleVariant); dispid 20000006;

  end;

 

  CoCoMyDCOMObject = class

    class function Create: ICoMyDCOMObject;

    class function CreateRemote(const MachineName: string): ICoMyDCOMObject;

  end;

 

implementation

 

uses ComObj;

 

class function CoCoMyDCOMObject.Create: ICoMyDCOMObject;

begin

  Result := CreateComObject(CLASS_CoMyDCOMObject) as ICoMyDCOMObject;

end;

 

class function CoCoMyDCOMObject.CreateRemote(const MachineName: string): ICoMyDCOMObject;

begin

  Result := CreateRemoteComObject(MachineName, CLASS_CoMyDCOMObject) as ICoMyDCOMObject;

end;

 

上面的程式碼,是一個簡單的 DCOM 原型,與前面的小節一樣,僅僅提供一個 SayHello 的函數而已,不過程式碼比較起來,果然多了很多資訊在裡面,其中 ICoMyDCOMObject 類別從 IDispatch 類別更改成,由 IAppServer 類別來繼承,而 ICoMyDCOMObjectDisp 類別除原先 SayHello 函數外,則多出許多的函數出來。而 IAppServer 類別,是從 IDispatch 類別繼承下來,除了具有 IDispatch 的功能外,我們從 IAppServer 新增的功能來看,原來 IAppServer 只是多了對於資料庫操作的函數,而 TClientDataSet AppServer 屬性,也正是屬於這樣的一個類別型態,所以 ICoMyDCOMObjectDisp 類別多出來的函數,便是對於資料庫操作的一個鏈結而已。然而這個部分不是我要討論的重點,所以我們在看看 CoCoMyDCOMObject.CreateRemote 在做些什麼事? CreateRemoteComObject 是一個有趣的函數名稱,譯為『建立遠端的 COM 物件』,它的原型是這樣子的:

 

function CreateRemoteComObject(const MachineName: WideString;

  const ClassID: TGUID): IUnknown;

const

  LocalFlags = CLSCTX_LOCAL_SERVER or CLSCTX_REMOTE_SERVER or CLSCTX_INPROC_SERVER;

  RemoteFlags = CLSCTX_REMOTE_SERVER;

var

  MQI: TMultiQI;

  ServerInfo: TCoServerInfo;

  IID_IUnknown: TGuid;

  Flags, Size: DWORD;

  LocalMachine: array [0..MAX_COMPUTERNAME_LENGTH] of char;

begin

  if @CoCreateInstanceEx = nil then

    raise Exception.CreateRes(@SDCOMNotInstalled);

  FillChar(ServerInfo, sizeof(ServerInfo), 0);

  ServerInfo.pwszName := PWideChar(MachineName);

  IID_IUnknown := IUnknown;

  MQI.IID := @IID_IUnknown;

  MQI.itf := nil;

  MQI.hr := 0;

  if Length(MachineName) > 0 then

  begin

    Size := Sizeof(LocalMachine);  // Win95 is hypersensitive to size

    if GetComputerName(LocalMachine, Size) and

       (AnsiCompareText(LocalMachine, MachineName) = 0) then

      Flags := LocalFlags else

      Flags := RemoteFlags;

  end else

    Flags := LocalFlags;

  OleCheck(CoCreateInstanceEx(ClassID, nil, Flags, @ServerInfo, 1, @MQI));

  OleCheck(MQI.HR);

  Result := MQI.itf;

end;

 

在上面的程式碼中原來 Delphi 是用 CoCreateInstanceEx 函數,並利用第四個參數傳遞遠端機器資訊,來取得該機器上的介面,然後我們在從

if @CoCreateInstanceEx = nil then

    raise Exception.CreateRes(@SDCOMNotInstalled);

這兩行指令得知 CoCreateInstanceEx 函數是專屬 DCOM 的指令。而透過上述的手續之後,我們就可以像呼叫 COM 一樣的方式呼叫 DCOM 了。不過光是這樣子就可以呼叫 DCOM 的遠端服務嗎?感覺上還是怪怪的。所以我們來看看在撰寫 DCOM 所使用的 TRemoteDataModule 在做些什麼事?

 

class procedure TCoMyDCOMObject.UpdateRegistry(Register: Boolean; const ClassID, ProgID: string);

begin

  if Register then

  begin

    inherited UpdateRegistry(Register, ClassID, ProgID);

    EnableSocketTransport(ClassID);

    EnableWebTransport(ClassID);

  end else

  begin

    DisableSocketTransport(ClassID);

    DisableWebTransport(ClassID);

    inherited UpdateRegistry(Register, ClassID, ProgID);

  end;

end;

 

上面的程式碼,是 TRemoteDataModule 檢查 DCOM Server 程式是否已經註冊的程式碼,我們可以看到兩行 EnableSocketTransport(ClassID);EnableWebTransport(ClassID); 指令,這樣 DCOM 伺服器除了可以透過 RPC(註 1.)的方式進行連接之外,還提供 SocketWeb 的方式,嗯!!國內的書籍中,倒是沒有提過這件事。不過沒關係,這也不是本文的重點,我們還是再追蹤一下 TRemoteDataModule 的原型:

 

TRemoteDataModule = class(TDataModule, IAppServer)

  private

    FProviders: TList;

    FLock: TRTLCriticalSection;

    function GetProviderCount: integer;

  protected

    function GetProvider(const ProviderName: string): TCustomProvider; virtual;

    class procedure UpdateRegistry(Register: Boolean; const ClassID, ProgID: string); override;

    { IAppServer }

    function AS_GetProviderNames: OleVariant; safecall;

    function AS_ApplyUpdates(const ProviderName: WideString; Delta: OleVariant;

      MaxErrors: Integer; out ErrorCount: Integer;

      var OwnerData: OleVariant): OleVariant; safecall;

    function AS_GetRecords(const ProviderName: WideString; Count: Integer;

      out RecsOut: Integer; Options: Integer; const CommandText: WideString;

      var Params, OwnerData: OleVariant): OleVariant; safecall;

    function AS_DataRequest(const ProviderName: WideString;

      Data: OleVariant): OleVariant; safecall;

    function AS_GetParams(const ProviderName: WideString; var OwnerData: OleVariant): OleVariant; safecall;

    function AS_RowRequest(const ProviderName: WideString; Row: OleVariant;

      RequestType: Integer; var OwnerData: OleVariant): OleVariant; safecall;

    procedure AS_Execute(const ProviderName: WideString;

      const CommandText: WideString; var Params, OwnerData: OleVariant); safecall;

  public

    constructor Create(AOwner: TComponent); override;

    destructor Destroy; override;

    procedure RegisterProvider(Value: TCustomProvider); virtual;

    procedure UnRegisterProvider(Value: TCustomProvider); virtual;

    procedure Lock; virtual;

    procedure Unlock; virtual;

    property Providers[const ProviderName: string]: TCustomProvider read GetProvider;

    property ProviderCount: integer read GetProviderCount;

  end;

 

從上面的原型宣告中可以看出,TRemoteDataModule 除繼承自 TDataModule 之外還加入 IAppServer 的介面,使得 Client 端可以藉由 Socket 呼叫到 DCOM Server 所提供的服務,另外 TRemoteDataModule FProviders 屬性也提供了對於資料庫感知的連結,所以使用 Delphi 所開發的 DCOM Server,可以具有資料庫感知的能力,便是來自於此了。 

結論:

雖然這篇文章對於 COM DCOM 的介紹,並未提及它們的結構的基本規格,或者是相關的設計技術與應用技巧,但相信透過本文的引導,讀者應該對於它們運作方式,有了相當程度的認識,這對於我們日後在 Windows 平台應用系統的分析與設計上,有著非常大的幫助(至少對於筆者而言是這樣子的)。當然 COM DCOM 尚有其他的議題可以討論,但是浩繁不及備載。讀者就請自行涉獵,以補充相關的知識吧!

 

1. RPC (Remote Procedure Call) 是建構於 TCP/IP 通訊協定的遠端程序呼叫機制。詳細內容請見李維先生的『Delphi 4.x 實戰篇 1p.68 中說明。以下是關於RPC的部分說明是摘自交大袁賢銘教師的分散式系統程式設計課程內容:

  • OSF/DCE RPC "相容"。
    • 相容並非遵循,Microsoft 只做出了 DCE 規格裡的 RPC 元件部份。而且 Microsoft RPC 和 OSF/DCE RPC 在應用程式界面 (Application Interface, API) lication Interface, API) 的命名上也有些不同。
    • 硬體及作業系統獨立性,在 Windows 上執行的 RPC client 或 server 均可和其他與 DCE RPC 相容的 RPC 系統協同運作,但與 ONC RPC (Sun RPC) 不相容。為同步程序呼叫模式 (Synchronous call model)。IDL 編譯器可以產生並行 (concurrent) RPC server 所需的存根程式。
  • 支援 Microsoft RPC。
    • MS-DOS:只有 Client 部份,Windows 3.X:只有 Client 部份,Windows 95:Client/Server 均有支援,Windows NT:Client/Server 均有支援。
  • 開發工具。
    • 在 Microsoft Win32 SDK 裡的 RPC Toolkit。