追本朔源說COM

在現在以及未來的操作系統中COM(組件對象模板)技術佔有的比例越來越大了!
COM是什麼呢?你可以理解它爲對象性質的API(區別於函數性質的API),因爲一般函數性質的API調用是這樣的:Sendmessage(handle,msgunit,wparam.lparam);而對於COM中的方法(這裏改叫方法了)調用是這樣的:ShellLink.SetPath(pchar(filepath));在這裏可以講ShellLink看作一個對象,而SetPath是這個對象衆多方法中的一個,我們可以這樣生成一個對象:
var
ShellLink:IShellLink;//聲明一個接口類型的指針變量,其中IShellLink是已經定義好的接口類型(它是個系統提供的一個接口)
begin
CoCreateInstance(CLSID_ShellLink,nil,CLSID_INPROC_SERVER,IID_IshelllinkA,shelllink)
或者:
ShellLink:=CreateComObject(CLSID_ShellLink) AS IShellLink;
然後就可以調用其中的方法了。象這樣的調用方法用過DirectX(區別於一般GUI庫的用戶圖象處理庫,在遊戲開發中佔有極其重要的位置)朋友一定非常熟悉了,因爲DirectX本來就是微軟推出的第一個COM應用嘛!那麼它到底是怎麼工作的呢?想必很多初學COM的朋友一定很想知道吧?我開始學的時候就想知道其中的機理,但很多書都講得不是很清楚,後來買了本<COM/DCOM精通>纔算是看明白了,不過它是C++語言的,所以這裏我結合Delphi中的源代碼來講一下其中的工作機制!
不過對讀者有個要求:
你必須知道COM是什麼以及在使用是如何調用(也就是你使用過COM,但不是很懂)。
1.COM的概念及作用:
COM是一種服務器,它將自己的服務包裝在一個庫中,並且自己以一個對象形式出現,而其中的子函數叫做它的方法。它的全稱是Component Object Module組件對象模板,它是提供一種二進制層面(最底層)的共享手段,由於其是在二進制層面上提供共享,所以它就提供了一個很有用的特性-獨立於開發環境,使用各種語言的開發工具都可以使用它,這是它最大的特點!而且由於它是一種模板,那麼我們可以將軟件的各個部分分爲多個模板,在以後管理和升級的時候只要更改其中的模板而無需更改其他模塊中的任何代碼,這是它最大的賣點!
2.二進制層面的共享是什麼:
這裏會多次使用這個概念,那麼到底什麼是二進制層面呢?我們現在使用的各種開發工具都代用自身的庫文件,如windows.h或者windows.dcu,當程序在編譯時會將這些庫中的例程加入到程序相應的部分,由於他們是用各種語言寫的,所以如果呢要改變其中的行爲必須重新編譯你的源程序代碼。這叫做語言層面的共享(現在開發工具絕大多數的庫都是這種)!而COM首先是一種規範:它是用微軟規定的(但現在已經實現跨平臺了,畢竟好的理念誰都可以用嘛)一種二進制的規範,也就是COM必須提供一塊內存給調用者,這塊內存中放置各個COM方法的地址,這塊內存就是我們說的接口了。由於現在大多數的開發工具都使用各種方式(方式各有不同)提供了這樣一塊內存,那麼所有的開發語言就可以互相使用對方組件提供的方法了,因爲雖然語言不同,但他們提供的內存塊的規範是一樣的,進入各個方法後就可以運行了(各種語言提供的函數編譯器都編譯成二進制了),而且COM規範中對使用的參數和返回類型都做了定義,各種語言只要按照這個規定就可以了,所以說COM是一種規範更合適一些,雖然它也是一種高級技術!
3.這篇文章將講什麼:
由於COM多爲進程內服務器,所以我們這裏只是講以DLL形式存在的COM的使用和原理,對於進程外的服務器,你可以參考相應的DCOM教材。這裏僅僅就Delphi中如何實現相應的技術提供講解,對於其他語言的大家參考相應資料!
4.作者忠告:
COM是未來操作系統講廣泛採用的各種技術的基礎,所以大家必須要重視這塊的開發和理解,如果你對於COM一無所知的話,建議你看看C++中關於它的介紹,因爲我我覺得如果連COM的規範都不懂的話,就很難深入了,更別說理解Delphi中的COM了,因爲在Delphi中使用COM雖然簡單,但要理解它是如何包裝的沒有基礎是不可能的!
這篇文章講分幾個部分分別講解,如果大家有興趣可以關注這個論壇,如果有什麼不明白的地方也可以告訴我,留言或者給我mail:[email protected],我們互相討論一下^_^
這一節只是大家熱熱身,大家對於讀者要求做到了嗎?準備開始咯!
好了,我們下節見!

---------------------------------------------------------------------------------
com在delphi中是實現:
com在delphi中實現起來是相當簡單的,只要你使用相應的開發嚮導,就可以很快實現一個com服務器了,我們先來做個簡單的com服務器,它的作用就是在桌面顯示一個時間和版權的信息!

此主題相關圖片如下:

我們採用delphi6.0(你也可以使用7.0,代碼一樣)
選擇file->open->other打開向導面板,選擇new items->activex->activex library
確定就可以生成一個空的庫文件(一個dll的框架),它只是提供了dllgetclassobject, dllcanunloadnow,dllregisterserver,dllunregisterserver這四個導出函數,其他的什麼也不做(但就是這四個函數提供了com運行的機會,後面會講)。然後我們要加一個com單元進來,因爲正是要它來提供給我們某個服務(如:這裏的桌面時間顯示),同樣操作選擇file->open->other打開向導面板,然後選擇new items->activex->com object這時會跳出一個對話框,你只要輸入你的類名,我們這裏使用的是tmyclass,其他的可以不動,生成一個com單元了。並且要求你用type library來設置你的接口,你可以設置你要的方法,我們這裏只提供兩個方法:getvalue,setvalue。好了,回到開發界面,我們現在已經有了一個可以自己寫代碼的類了(其中方法也空好了等我們寫了,開發工具想得真實周到啊!),現在我們可以寫上代碼了,怎麼寫呢?想想!我們提供的是一個在桌面上的時鐘,它要不斷的變化時間,而且要在最上層,不管你怎麼開窗口,它都可以讓我們看到,看來要求得不高!時間的變化我們可以通過一個timer組件來實現,最上層嘛,使用桌面的tcanvas對象不就可以咯。我想這裏多說兩句:我們的timer組件需要在這個類初始化時候建立,按一般的想法,我們可以重寫構造器函數create,但如果這樣的話就game over了,因爲在delphi中com的初始化是由initialize函數提供,就是你在create函數中寫上也沒有用,因爲delphi不會調用的!好了現在可以寫代碼了:
初始化部分:
procedure tmyclass.initialize;
begin
inherited initialize;
canvas:=tcanvas.create;
canvas.handle:=getdc(0);//得到桌面的dc;
time:=ttimer.create(nil);
time.interval:=1000;
time.enabled:=false;
time.ontimer:=ontime;//賦予time對象一個事件句柄;
end;
相對應就應該有銷燬的部分來銷燬timer產生的對象:
destructor tmyclass.destroy;
begin
canvas.free;
time.free;
inherited destroy;
end;
事件句柄:
procedure tmyclass.ontime(sender:tobject);
begin
getvalue;//內部調用其中的一個方法
end;
一個調用函數:
procedure tmyclass.setvalue(value:integer);
begin
time.enabled:=true;//激活時間組件
end;
方法的代碼:
function tmyclass.getvalue:integer;
var
t:tdatetime;
s:string;
begin
try
t:=now();
s:=formatdatetime('yyyy"年"mm"月"dd"日"ampmhh"時"',t);
canvas.font.color:=clred;
canvas.font.size:=18;
canvas.brush.style:=bsclear;
canvas.textout(500,520,s);
canvas.font.color:=clblue;
canvas.font.size:=18;
canvas.brush.style:=bsclear;
canvas.textout(500,540,'塗非平製作');
except
raise exception.create('canvas is error');
end;
result:=0;//我們這裏沒用它,返回一個0就可以了!^_^
end;
好了,基本代碼寫完了,可以編譯了,編譯後生成的dll還不可以調用,要先註冊到系統中(向註冊表寫入相應的信息),我們按下菜單run->register activex server彈出對話框表示我們正確註冊,現在我們就有一個com服務器了!!!!!!!!但我們怎樣調用它呢?打開剛剛開發的工程單元,找到相應的tlb.pas單元,記下一個值:clsid_myclass就可以了,我們要靠它來調用呢
一些這個單元中的信息(至於guid不要我說了吧)
------------------------------------
imyinterface=interface(iunknown)
['{a0c53f6f-f270-457b-8d42-89fb12737d6b}']
function getvalue:integer;stdcall;
procedure setvalue(value:integer);stdcall;
end;
const clsid_tmyclass:tguid='{c421dfce-1cfa-476e-b69b-d7f519ff87d7}';
------------------------------------
下面我們來寫調用部分:
建立一個單窗口程序,在上面加一個按鈕就可以了,現在就寫按鈕的事件句柄的代碼,其他的可以不寫(本來就沒有代碼,^_^)。
不過要先加入你剛纔編寫的com的相應信息進來纔可以,找到相應的tlb.pas單元,講接口部分複製過來(提供一個調用接口的規範,告訴程序怎樣調用),就是上面畫線的部分複製過來!
現在可以寫按鈕事件代碼了:
procedure tform1.button1click(sender: tobject);
begin
map:=createcomobject(clsid_tmyclass) as imyinterface;
map.setvalue(1);//至於這裏的參數,由於我們沒有用它,所以你可以輸入任何數,但不能大於整數的範圍喲!
end;
注意這裏的map是在類中聲明的一個接口變量,你可以認爲它是一個對象: map:imyinterface;
好了,試試吧!按一下按鈕是不是出現圖片上的情景了?
好了,這一節主要是複習一下製作com的步驟,雖然簡單,但複雜的com也是這樣做出來的,這裏還演示瞭如何在類中包裝對象,讓對象爲我們做事。以後幾節我會以這個例子來講解系統是怎樣讓它激活工作的,以及它是怎麼輸出它自己的方法的!
好了,下節見!

---------------------------------------------------------------------------------
簡單的利用開發嚮導,寫幾行簡單的代碼就完成一個顯示時間的com服務器,是不是很簡單,如果你真的這麼認爲那你就錯了。對!雖然你會用delphi開發com了,但如果你想理解它或者以後開發複雜的com和深入dcom機制不理解它的運行機制是不可能的!現在我們先就調用端來分析一下它是怎麼工作的!
我們調用是用這樣的形式:
map:=createcomobject(clsid_tmyclass) as imyinterface;
其實這裏完成的是兩個操作:
完整的應該是這樣的:
map1:=createcomobject(clsid_tmyclass);//map1是一個iunknown類型的接口
map1.queryinteface(iid_shelllinka,map);//通過map1查找iid_shelllinka接口
僅靠系統提供一個createcomobject我們就可以使用一個com服務器了,這個函數肯定做了什麼?
在delphi的源代碼中createcomobject是這樣定義的:
function createcomobject(const classid: tguid): iunknown;
begin
olecheck(cocreateinstance(classid, nil, clsctx_inproc_server or
clsctx_local_server, iunknown, result));
end;
呵呵,原來是函數的包裝啊!
這裏就要介紹一下cocreateinstance函數。
function cocreateinstance; external ole32 name 'cocreateinstance';
它是系統(ole32.dll)提供的一個api,那麼它是做什麼的呢,具體做了什麼工作?
這個函數真正要我們提供的參數就是一個classid,其他的可以使用上面的默認值。
它會從註冊表中找該com類的classid(在hkey_classes_root\clsid鍵中),找到相應的鍵後系統會讀取其中inprocserver32的數據,找到服務器的地址後用loadlibrary函數將服務器dll載入宿主進程,再利用dll中的dllgetclassobject函數(這是每一個dll形式com服務器必須提供的函數),獲取了iclassfactory接口指針後利用這個接口中createinstance方法創建我們需要的com對象的實例(這個時候對象纔出現了)!好了,現在知道他們之間的調用過程嗎。小節一下:
createcomobject-->cocreateinstance-->註冊表clsid-->loadlibrary
-->dllgetclassobject(我們在後面分析中將從這裏開始)
-->iclassfactory.createinstance
好了,前面的分析完成了,由於它大部分功能都是由系統完成的,沒有多少研究的必要,你只要會用,知道怎麼用和它是做什麼的就可以了。(也沒有條件分析下去,我可沒有這個api的源碼,你只有去問微軟了,不知道它會不會給你),那麼下面我們從dllgetclassobject函數開始分析,它是編程語言提供的(系統要求的,工具提供的),我們可以分析其中的代碼,先看看dll工程的代碼:
library project3;

uses
comserv,
unit3 in 'unit3.pas',
project3_tlb in 'project3_tlb.pas',
unit7 in 'unit7.pas' {form1};

exports
dllgetclassobject,
dllcanunloadnow,
dllregisterserver,
dllunregisterserver;

{$r *.tlb}

{$r *.res}

begin
end.
僅僅是導出comserv庫中的四個函數,其中dllgetclassobject我們上面說過,也是最重要的,先分析它:
function dllgetclassobject(const clsid, iid: tguid; var obj): hresult;
var
factory: tcomobjectfactory;//注意:這裏都是由類工廠提供的。
begin
factory := comclassmanager.getfactoryfromclassid(clsid);//1
if factory <> nil then
if factory.getinterface(iid, obj) then//2
{由類工廠去查找相應的iid,成功就返回s_ok,並且將接口指針返回給obj}
result := s_ok
else
result := e_nointerface{沒有找到就返回e_nointerface}
else
begin//如果沒有相應類工廠
pointer(obj) := nil;//將接口指針置nil
result := class_e_classnotavailable;
end;
end;
現在講講這個函數,這個函數通過clsid, iid參數來找他們在dll中的接口的位置,然後通過參數obj來返回指針,而整個函數的返回值(hresult類型)只是用來告訴系統接口是否存在。這裏的obj是什麼呢?就是我們前面定義的map啊!是不是對於這種返回形式還不適應,不要緊,看多了就知道了,這種返回形式很有效的!不過要注意到它是var聲明形式的喲!!這個函數要往下分析的部分(或者要重點注意的部分)已經用數字標明瞭!那麼我們下一節就先分析1吧。
好了,先講到這裏,下節見。

------------------------------------------------------------------------------------
通過上面幾節的分析,不知道你對於這種學習方法是否滿意,如果你滿意那我會非常高興的。我希望更多的朋友分享我學習上的心得,做出更好的軟件,找到更好的工作,賺到更多的錢!好了閒話少敘,接着講。
上面我們講的第一個要點是這行代碼:
factory := comclassmanager.getfactoryfromclassid(clsid);//1
comclassmanager是一種對象,它很簡單,主要是用來管理類工廠的,它重要的方法就是這裏的getfactoryfromclassid,根據clsid來查找類工廠並返回之。有興趣的朋友可以看這個類的源代碼(很簡單的,估計是object pascal最簡單的幾個類之一了),這裏我們只要注意getfactoryfromclassid函數就可以了。
function tcomclassmanager.getfactoryfromclassid(const classid: tguid): tcomobjectfactory;
begin
flock.beginread;//鎖定並開始讀取其中數據
try
result := ffactorylist;{得到comclassmanager對象的ffactorylist(其中放置的就是類工廠,它使用addobjectfactory來添加相應的類工廠)}
while result <> nil do
begin
if isequalguid(result.classid, classid) then exit;{找到了就退出,並返回這個result值}
result := result.fnext;//類工廠存放在fnext變量中
end;
finally
flock.endread;//解鎖恢復
end;
end;
isequalguid是一個比較函數,它比較classid,classid相同返回true否則返回false;
從這裏可以知道這個函數主要是返回一個類工廠的指針。那麼到底什麼是類工廠,爲什麼要使用類工廠呢?
com中有一個重要的思想就是封裝。它將自己的實現封裝起來,僅僅只是留下一個接口讓用戶來調用!至於裏面是怎樣實現的外面一概不知,而類工廠的存在將這種封裝推向了及至,因爲它本身也是一個com對象,我們叫它“一級對象”,然後再由它來生成我們的com對象,那我們的com對象就可以叫做“二級對象”,通過兩級封裝,別人還知道你是怎麼實現的?在delphi中默認的com對象都是通過類工廠生成的二級對象(當然你也可以用一級來搞定,最後一節我們將實現這麼一個com,因爲這要求一定的基礎)。那麼類工廠到底是個什麼東西,它有什麼特別之處呢?
類工廠其實就是一個簡單的com類,不希奇,但的確有特別之處,因爲它提供了一個很有用的接口和方法:
iclassfactory = interface(iunknown)
['{00000001-0000-0000-c000-000000000046}']
function createinstance(const unkouter: iunknown; const iid: tiid;
out obj): hresult; stdcall;
function lockserver(flock: bool): hresult; stdcall;
end;
現在明白了吧,只要實現了iclassfactory接口的com類我們就可以叫它類工廠,沒有什麼其他的特點!至於createinstance函數後面將介紹,先將上面一節的第二個重點解釋一下:
factory.getinterface(iid, obj) 
如果找到一個類工廠的話就調用這個方法來找參數id標識的接口並依靠obj來返回指針給dllgetclassobject的obj參數!那麼它是怎麼查找的,隨我來!
function tobject.getinterface(const iid: tguid; out obj): boolean;
var
interfaceentry: pinterfaceentry;//接口指針的入口
begin
pointer(obj) := nil;//先將obj置爲nil
interfaceentry := getinterfaceentry(iid);{根據iid查找出接口的入口,這又是一個重要的函數喲,^_^}
if interfaceentry <> nil then//如果返回了接口入口
begin
if interfaceentry^.ioffset <> 0 then//確定其中的偏移量
begin
pointer(obj) := pointer(integer(self) + interfaceentry^.ioffset);{呵呵,找到這個入口了,它就是我們要返回的接口指針了}
if pointer(obj) <> nil then iinterface(obj)._addref;{如果調用成功,先調用接口的_addref方法將com對象使用次數加1,這是一個iunknown接口的標準方法}
end
else
iinterface(obj) := invokeimplgetter(self, interfaceentry^.implgetter);{這個我們可以先放下}
end;
result := pointer(obj) <> nil;//返回值是一個boolean值
end;
接下來我們先看看幾個數據結構的定義:
tguid = packed record
d1: longword;
d2: word;
d3: word;
d4: array[0..7] of byte;
end;
pinterfaceentry = ^tinterfaceentry;
tinterfaceentry = packed record
iid: tguid;//接口的定義iid
vtable: pointer;{接口指針指向的內存結構指針,一個虛擬方法表,這裏要區別delphi中的vmt喲}
ioffset: integer;//偏移量
implgetter: integer;
end;
pinterfacetable = ^tinterfacetable;
tinterfacetable = packed record
entrycount: integer;//接口個數
entries: array[0..9999] of tinterfaceentry;{接口入口數組,個數依據entrycount來定}
end;
這裏就不說幾個結構了,如果你連這個都不清楚那就不要看下面的內容,先充充電再來
接着說,上面那個函數最重要的就是getinterfaceentry函數的使用了,正是由它來完成查找接口的工作的!
它是一個類方法,至於什麼是類方法和類引用變量你可以參看論壇中我以前發表的一篇文章《delphi中的oop思想-1.pascal部分》裏面有詳細的介紹!
class function tobject.getinterfaceentry(const iid: tguid): pinterfaceentry;
var
classptr: tclass;//一個類引用變量
intftable: pinterfacetable;//接口表的指針
i: integer;
begin
classptr := self;//將自己的指針賦給classptr
while classptr <> nil do
begin
intftable := classptr.getinterfacetable;{調用getinterfacetable方法來返回給接口表指針變量,咳!又是一個包裝的函數,煩啊}
if intftable <> nil then
for i := 0 to intftable.entrycount-1 do{遍歷所有的接口來對比iid查找相應的表如果找到就跳出循環}
begin
result := @intftable.entries[i];
if (int64(result^.iid.d1) = int64(iid.d1)) and
(int64(result^.iid.d4) = int64(iid.d4)) then exit;//找到跳出
end;
classptr := classptr.classparent;
end;
result := nil;{如果剛纔的循環沒有跳出,說明沒有找到,返回爲nil}
end;
好了,這麼多函數,函數包函數,是不是有點頭暈啊,先歇歇,理清思路,準備下節接着講,好了下節見。

---------------------------------------------------------------------------------
好了,思路理得怎麼樣啊?現在接着開講了,上一節講到了getinterfaceentry函數中內包裝了一個函數getinterfacetable,這個函數和getinterfaceentry都是tobject提供的函數,很重要(特別爲支持com設計的)下面看看getinterfacetable是怎麼得到虛擬表指針的:
class function tobject.getinterfacetable: pinterfacetable;
begin
result := ppointer(integer(self) + vmtintftable)^;{如果你瞭解delphi的對象模型的話,這個代碼很容易理解了,它就是指向vmt表中vmtintftable的地址中的值,簡單吧,這樣就找到我們找了這麼辛苦的接口指針,下面有個圖來說明vmt的結構!}
end;

此主題相關圖片如下:

這個是怎麼回事啊?原來object pascal編譯器一碰到了如下的聲明形式
tmyclass=class(tcomobject,imyinterface)
它就將imyinterface定義的方法(對應於你的com對象中的方法,通過名字來確定)地址記住,然後再生成一個符合com接口規範的內存塊並將imyinterface中方法的地址寫入其中,以後就可以通過這塊內存來調用方法了,那麼怎麼確定這塊內存呢?就是靠vmt表中的vmtintftable,它指向的地址就是這塊內存塊的頭地址!至於vmt中的vmtintftable我要簡單說說。對於一般的虛擬函數地址,它會加到vmt的正數偏移的表格中,而負數偏移的是object pascal中非常重要的信息存在地,如動態方法表,接口指針表等等,vmtintftable的負偏移是-72,那麼上面函數的integer(self) + vmtintftable就是將vmt的值+vmtintftable,其中integer(self)是vmt的地址,而常數vmtintftable=-72,integer(self) + vmtintftable不就是vmt中vmtintftable的值嗎?而我們要用到這個指針指向的表的值並將其轉換爲指針,所以還要做個變換: ppointer(integer(self) + vmtintftable)^ 。ok了,現在知道是怎麼回事吧?
現在你對於com調用和查找應該明白了吧,那麼現在你是否非常想知道我們的com對象是怎麼產生的?是通過類工廠產生的啊,廢話,到底怎麼產生的呢?我們下一節接着說!

---------------------------------------------------------------------------------
我們回到最前面的那個顯示時間的例子,那麼我們的com是怎麼產生的呢?如果你試圖查找的話,一定會失望的,因爲代碼很少,給人的感覺是無從下手。這裏列出它的源代碼,大家看找得到嗎?
unit unit3;
interface
uses comserv,comobj,activex,windows,graphics,sysutils,extctrls;
const clsid_tmyclass:tguid='{c421dfce-1cfa-476e-b69b-d7f519ff87d7}';
type
imyinterface=interface(iunknown)
['{a0c53f6f-f270-457b-8d42-89fb12737d6b}']
function getvalue:integer;stdcall;
procedure setvalue(value:integer);stdcall;
end;

tmyclass=class(tcomobject,imyinterface)
private
canvas:tcanvas;
time:ttimer;
protected
function getvalue:integer;virtual;stdcall;
procedure setvalue(value:integer);virtual;stdcall;
public
procedure initialize;override;
destructor destroy;override;
procedure ontime(sender: tobject);
end;
implementation
procedure tmyclass.initialize;
begin
.......
end;

destructor tmyclass.destroy;
begin
.......
end;

function tmyclass.getvalue:integer;
begin
.......
end;

procedure tmyclass.setvalue(value:integer);
begin
.......
end;

procedure tmyclass.ontime(sender:tobject);
begin
getvalue;
end;
initialization
tcomobjectfactory.create(comserver,tmyclass,clsid_tmyclass,'mytestclass',
'this is my first comobject',cimultiinstance,tmapartment);
end.
好像沒有看到我們的tmyclass使用create創建對象啊,這裏我們到是看到一個熟悉的函數tcomobjectfactory.create,對了,這是一個類工廠的創建函數,它的參數就是我們開始使用開發嚮導中輸入的參數而已!我們的com對象就是由它創建的咯。注意tcomobjectfactory.create函數是方法initialization(初始化)中的,這表明這個dll被調用時這個類工廠實例就會被創建!這個類工廠是delphi提供的一種基本的類工廠,它除了定義和實現了iclassfactory接口外還添加了很多對象管理的功能!那麼我們現在來看看這個類工廠是怎麼創建我們的com對象的?
constructor tcomobjectfactory.create(comserver: tcomserverobject;
comclass: tcomclass; const classid: tguid; const classname,
description: string; instancing: tclassinstancing;
threadingmodel: tthreadingmodel);
begin
ismultithread := ismultithread or (threadingmodel <> tmsingle);
if threadingmodel in [tmfree, tmboth] then
coinitflags := coinit_multithreaded else
if (threadingmodel = tmapartment) and (coinitflags <> coinit_multithreaded) then
......
comclassmanager.addobjectfactory(self);{前面講過,這裏是添加自己到工廠管理者中}
fcomserver := comserver;
fcomclass := comclass;//我們的類定義就放在這裏,這是個類引用喲
fclassid := classid;
......
end;
這個函數將我賦給它我們的com類tmyclass存放在fcomclass內部變量中,由這個類工廠來產生我們的對象,那麼這裏還是沒有見到我們的對象的產生啊!?!?不知道你是否記得前面我們講類工廠時講到的一個重要的接口iclassfactory中有個createinstance方法啊,系統會調用這個方法讓類工廠產生我們的com對象,那麼我們對象肯定是在這裏產生的沒喲錯咯!看看它的方法代碼:
function tcomobjectfactory.createinstance(const unkouter: iunknown;
const iid: tguid; out obj): hresult;
begin
result := createinstancelic(unkouter, nil, iid, ', obj);
end; 
呵呵,又包裝了一個函數createinstancelic,接着看看它的代碼:
function tcomobjectfactory.createinstancelic(const unkouter: iunknown;
const unkreserved: iunknown; const iid: tiid; const bstrkey: widestring;
out vobject): hresult; stdcall;
var
comobject: tcomobject;{注意這裏喲!這裏聲明瞭一個tcomobject類型的變量,它就是存放我們com對象的指針}
begin
if @vobject = nil then
begin
result := e_pointer;
exit;
end;
pointer(vobject) := nil;
if fsupportslicensing and
((bstrkey <> ') and (not validateuserlicense(bstrkey))) or
((bstrkey = ') and (not hasmachinelicense)) then
begin
result := class_e_notlicensed;
exit;
end;
if (unkouter <> nil) and not (isequaliid(iid, iunknown)) then
begin
result := class_e_noaggregation;
exit;
end;
try
comobject := createcomobject(unkouter);{創建我們的com對象,參數先我們可以不管,它是高級com開發中需要的一個參數}
except
if fshowerrors and (exceptobject is exception) then
with exception(exceptobject) do
begin
if (message <> ') and (ansilastchar(message) > '.') then
message := message + '.';
messagebox(0, pchar(message), pchar(sdaxerror), mb_ok or mb_iconstop or
mb_setforeground);
end;
result := e_unexpected;
exit;
end;
result := comobject.objqueryinterface(iid, vobject);{通過comobject.objqueryinterface方法(objqueryinterface與我們前面講的getinterface是一樣的,只是一個包裝而已)查找我們要的iid並通過vobject返回接口指針}
if comobject.refcount = 0 then comobject.free;{如果使用計數爲0就刪除com對象}
end;
那麼我們要看看createcomobject方法了:
function tcomobjectfactory.createcomobject(const controller: iunknown): tcomobject;
begin
result := tcomclass(fcomclass).createfromfactory(self, controller);
end;
這裏我們看到我們剛纔輸入的fcomclass了,它是我們com類的一個引用,就是我們最前面輸入給類工廠的參數,將它進行類型轉換後調用createfromfactory,再來看看createfromfactory這個函數,真是麻煩喲!
constructor tcomobject.createfromfactory(factory: tcomobjectfactory;
const controller: iunknown);
begin
frefcount := 1;
ffactory := factory;
fcontroller := pointer(controller);
if not fnoncountedobject then ffactory.comserver.countobject(true);
initialize;//嘿嘿,就是它了,我們的初始化就在這裏完成的喲!
dec(frefcount);
end;
這裏看到我們最開始例子中初始化的函數initialize,就是由它來負責我們我的com初始化,現在知道爲什麼com的初始化不能寫在create中嗎?那也許你會問我們希望得到的tmyclass.create還是沒有出現啊!因爲這個工作在這一步已經由delphi幫我們生成了,只是這些代碼在它的source部分忽略了,我們也可以不必管它,因爲既然追到我們需要初始化部分的代碼,說明我們的com對象肯定已經成功創建了,否則com類中的成員數據怎麼可能完成初始化呢?!
這裏得說說分析代碼的經驗了:對於一個代碼,它可能是多個函數的組合或者包裝,你需要分清楚重點,然後再深入到包裝的函數裏面,對於其他的一些對象管理維護的變量和函數我們可以不管!
我們已經從系統調用和com內部實現兩個方向理解了整個com是怎麼來的怎麼工作的!下一節我們將創建一個com對象,它有一個特別的地方,我們前面的com對象是二級對象,現在我們利用類工廠直接創建一個一級com對象。
好了,我們下一節接着說!

---------------------------------------------------------------------------------
我們現在瞭解了com的生存和調用機制,大家是不是想試試身手啊,那麼我提一個要求看
大家是否可以自己獨立完成,如果不行的話再回來看我這個例子!
要求:
大家都知道delphi中的com都是通過相應的類工廠產生的一個二級封裝對象,那麼我們有
沒有辦法做一個一級的com對象呢,就是直接將自己的com對象暴露出來?
好了,現在來說說我的考慮:
前面講過createcomobject來創建com對象後臺其實是通過查找註冊表載入相應的dll來完
成的,而dll中的dllgetclassobject又是通過查找相應的類工廠來找我們的com對象類,
那麼如果我們重新更改所有的代碼將是很困難的!!那換個思維方式考慮,我們可以自
己建立一個com對象,讓它同時是自己的工廠不就ok了,問題就迎刃而解!由於這個對象
同時是自己的類工廠,那麼就是一個一級的對象了。好了,有了一個好的想法,我們現
在就來實現它吧!
一個類工廠最大的特點就是具有iclassfactory接口,那我們就可以定義成一個這樣的類

type
  tmyclasses=class of tcomobject;
  imyclass = interface(iunknown)//我們自己的接口
    ['{20073cda-c810-4e6e-9b00-49b9349b3c99}']
    function  hehe: hresult; stdcall;
  end;
  iclassfactory = interface(iunknown)//系統提供的類工廠接口
    ['{00000001-0000-0000-c000-000000000046}']
    function createinstance(const unkouter: iunknown; const iid: tiid;
      out obj): hresult; stdcall;
    function lockserver(flock: bool): hresult; stdcall;
  end;
  tmyclass = class(tcomobjectfactory,iclassfactory,imyclass){看!我們這裏導出
了兩個接口,一個是類工廠需要的iclassfactory接口,一個是爲我們服務的imyclass接
口!}
  protected
    function hehe: hresult; stdcall;
    function iclassfactory.createinstance=mycreate;{注意這裏,由於我們是直接
繼承於tcomobjectfactory類,它裏面有默認的iclassfactory.createinstance實現,我
們要重新寫一個實現方式,所以要將iclassfactory接口的createinstance方法指向我們
給它定義的一個相同參數的方法,偷樑換柱,呵呵}
  public
    function mycreate(const unkouter: iunknown; const iid: tiid;
      out obj): hresult;virtual;stdcall;{這就是我們給iclassfactory接口create
instance方法的一個替換方法,注意應該聲明爲virtual方法,這是編譯器的要求喲,它
提供一種動態分配方法地址機制,如果是靜態的就不會將方法放置在接口vtable表裏面
了,不信你試試,呵呵}
  end;
好了,一個框架搞好了,現在先寫我們的接口(imyclass)的唯一一個方法:
function tmyclass.hehe: hresult;
begin
  messagebox(0,pchar('hehe'),pchar('success'),mb_ok);
  result:=s_ok;
end;
僅僅是顯示一個對話框並返回s_ok就可以了,我們僅僅是試驗我們的想法和鍛鍊我們的
能力嘛。
現在的問題是我們該如何寫那個我們改了的接口中的建立com對象的方法呢?
我們可以先看看系統提供的實現代碼,它做了什麼?前面的文章已經說得很清楚,這裏
重申一下:它所做的就是建立我們的com類的對象並根據我們的對象查找相應的接口返回
就可以了。我們這裏由於對象是本身,那就更好辦了!因爲它不要再去建立對象了,直
接用自己就可以了,下面是它的代碼:
function tmyclass.mycreate(const unkouter: iunknown; const iid: tiid;
      out obj): hresult;
var mycom:tmyclass;//這裏聲明一個對象,就是我們對象的類型了
begin
if @obj = nil then
  begin
    result := e_pointer;
    exit;
  end;
  pointer(obj) := nil;
  if (unkouter <> nil) and not (isequaliid(iid, iunknown)) then
  begin
    result := class_e_noaggregation;
    exit;
  end;
  try
    mycom:=self;{這裏是關鍵,將這個對象的指針變量就設置爲自己。}
  except
    result := e_unexpected;
    exit;
  end;
  result :=mycom.queryinterface(iid,obj);
  if mycom.queryinterface(iid,obj)<>s_ok then mycom.free;{根據我們的對象(自
己嘛)來調用queryinterface函數查找iid並且由obj返回之}
end;
代碼簡單吧!?如果你真的理解前面講的內容,那麼這些代碼你一看就懂,否則的話我
是怎麼改的你肯定不知道的!
再在單元中生成一個guid來表示類:const class_myclass: tguid = '{021038df-233c
-43f1-83b5-bf8cacc293a0}'
在初始化的地方寫上:tmyclass.create({注意:這裏使用的不是別的類工廠,而是我們
定義的對象本身tmyclass喲,由它自己創建}
comserver,
tmyclasses(tmyclass),{由於自己改寫的函數中並沒有用到這個參數,這裏我們隨便寫
一個,只要記住類型就可以了,爲了便於直接使用原來的函數,不然我們還得再寫一個
,麻煩!這裏我們複習一下原來講的類引用的用法,所以故意寫成這樣的!}
class_myclass,
'mytestclass',
'this is my first comobject'
cimultiinstance,
 tmapartment);
好了,一切都做好了!
註冊,準備使用它,驗證我們的想法是否正確!
然後再創建一個窗口來測試一下吧!
建立一個窗口並在上面放置一個按鈕,按下按鈕後調用com對象的hehe方法,看看會不會
有對話框彈出。下面是按鈕的代碼:
在類型聲明中增加
const class_myclass: tguid = '{021038df-233c-43f1-83b5-bf8cacc293a0}';
  imyclass = interface(iunknown)
    ['{20073cda-c810-4e6e-9b00-49b9349b3c99}']
    function  hehe: hresult; stdcall;
  end;
在窗口類中增加一個數據:p:imyclass;
按鈕代碼:
procedure tform1.button1click(sender: tobject);
var s:hresult;
begin
p:=createcomobject(class_myclass)as imyclass;
s:=p.hehe;//調用我們對象的方法;
end;
如果一切正常(我們的想法正確,我們的做法正確)的話,按下按鈕後應該彈出一個對
話框的!我的程序截圖如下:
此主題相關圖片如下:
不知道你做得正確嗎?如果不正確,那麼肯定有什麼地方沒有理解或者疏忽了,再看看
我的代碼吧!
好了,這個專題講完了,本來還想講一個com應用的例子,但現在這樣的例子太多了,我
就懶得寫那麼多代碼了,大家自己看吧!
如果你有什麼問題可以找我,我們共同討論:[email protected]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章