masm32开发com组件
生活随笔
收集整理的這篇文章主要介紹了
masm32开发com组件
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
標題: masm32開發com組件介紹[一][二][三]
作者: combojiang
時間: 2007-12-10 14:09
鏈接: http://bbs.pediy.com/showthread.php?t=56328
詳細信息:
聲明:本貼參考網站:http://ourworld.compuserve.com/
[一]基礎知識篇
組件對象模型(Com)在windows操作系統中應用越來越廣泛。com因為大量的技術細節顯得很復雜,但是正是這種復雜才使com組件的調用顯得十分簡單。 com和使用程序采用server/client架構。下面我們將在后續的兩篇中介紹com組件的編寫與調用。
com編程時當前程序開發的熱點,各種編程語言都為組件編寫提供了很好的支持,但是匯編語言例外,匯編語言開發組件沒有優勢。但是透過匯編開發的了解,可以使我們了解com組件的工作原理。好了,閑話少說,開始介紹:)
所有的inc頭文件都要滿足如下特點:
1) masm32松散的類型定義約定將繼續使用。就是說參數可以被定義為他們的基本類型,代表性的如:DWORD
2) 里面不能創建任何的代碼,僅僅包含定義信息,頭文件里面需要包含代碼,則必須定義為宏。
3) 結構體應該參照他們的C原形來定義。
4) GUID 結構定義在windows.inc文件中,GUID的值應該通過textequ宏來定義,這樣不會直接產生任何代碼。
5) 接口定義分為兩步:
1.一個通用的宏產生一個通用的接口結構。
2.使用接口名字來修飾結構自身的方法名字。
這種方式可以有效地避免namespace沖突,并且方便接口定義結構繼承。
6)COM接口函數調用使用coinvoke宏。
GUIDS
EXAMPLE:
sIID_IUnknown TEXTEQU ;; run thru args to see if edx is lurking in there
IFIDNI ,
.ERR
ENDIF
ENDM
istatement CATSTR ,,
IFNB ;; add the list of parameter arguments if any
istatement CATSTR istatement, ,
ENDIF
mov edx, pInterface
mov edx, [edx]
istatement
ENDM
;------------------------------------------------- --------------------
例如:QueryInterface方法調用如下:
coinvoke ppv ,IUnknown, QueryInterface, ADDR IID_SomeOtherInterface,
ADDR ppnew
HRESULTS
任何一個com接口函數的返回值類型都是一個hResult, 4個字節長。返回值在eax寄存器中。可以用這個值來判斷函數調用是否成功。
.IF !SIGN?
; function passed
.ELSE
; function failed
.ENDIF
接下來,我們定義了宏來簡化它:
.IF SUCCEEDED ; TRUE if SIGN bit not set
.IF FAILED ; TRUE is SIGN bit set
結論:
以上這些是你用匯編開發com需要用到的,這些適用于activex的開發。
[二]用匯編語言編寫com組件
組件對象模型com是以win32 dll或exe形式發布的執行代碼組成的。Com是由一些對象和對象的接口組成,在com里,接口提供對象操作的機制。 而接口是由一個或者多個相關的方法、屬性、事件組成的。在這里我們開發一個簡單的但是功能齊全的一個進程內com組件(即以dll形式存在)。
這里假設你已經了解了com對象模型的基礎知識,了解什么是虛表,什么是虛函數表指針。如果你不熟悉這些,建議看看《com本質論》這本書。
我們先來分析下進程內com服務的組成。由于它是一個dll形式發布的,其中包括5個重要的函數。其中后面的四個是要作為dll導出函數來導出的。
DllMain: 這是動態鏈接庫德第一個入口函數,它在庫被加載的時候被調用,通過這個函數,可以對客戶端程序進行檢查。
DllRegisterServer:通過這個函數能夠實現組建的自我注冊,注冊信息作為資源保存在動態鏈接庫中,這個函數能夠讀取資源,把信息寫進注冊表,使用
regsvr32.exe 注冊組建時,實際上是調用了組件輸出的這個函數。
DllUnregisterServer: 當一個組件不再使用時,這個組件應該能夠提供自我卸載,regsvr32.exe能調用這個函數,實現這一步。
DllCanUnloadNow: com服務中的全局變量用于保存它的狀態,客戶端可以周期性的調用這個函數,檢查組件服務器是否在使用,然后把它卸載。
DllGetClassObject: 這是完成組件輸出的函數,這個輸出需要3個參數,創建組件的GUID,要創建組件接口的GUID以及創建后指向對象的指針。如果組件對象或者接口不被支持,執行將失敗。
到現在為止,我們應該注意到一件事情,就是如果不是因為間接訪問,com將什么也不是。實際上,DllGetClassObject函數返回的對象不是我們要尋找的對象,它是類廠對象,一個類廠對象了解如何實例化其他任何的類。第一層的間接訪問允許組件創建的細節被指定,如果它僅僅是簡單而又直接的返回一個我們要尋找的對象指針,那么說明對象已經存在,那樣,我們將不能設置和控制關于構造對象的任何參數。
DllGetClassObject返回一個IClassFactory接口,這個接口是從IUnknown派生的,另外他還有自己的兩個重要的成員函數。
HRESULT CreateInstance(
IUnknown * pUnkOuter, //Pointer to outer object when part of an
// aggregate REFIID riid,
REFIID riid, //Reference to the interface identifier
oid** ppvObject); //Address of output variable that receives
// the interface pointer requested in riid
HRESULT LockServer(BOOL fLock);
//Increments or decrements the lock count
LockServer用來控制類廠對象的引用計數,系統檢查改計數以確定是否要卸載組件,即:控制類廠的生存期。
CreateInstance是最重要的,類廠組件的唯一功能是創建其它組件。一個類廠組件可以對應多種普通COM組件,但每個類廠組件的實例只能創建一種COM組件。
它接收一個接口GUID,返回該接口的指針。它并不接受組件的CLSID,所以一個類廠實例只能夠創建一種COM組件,即傳給 CoGetClassObject的CLSID對應的組件。
客戶、COM庫、組件dll、類廠、組件之間的交互過程:
1. 客戶首先調用COM庫的CoCreateInstance函數來創建COM組件。
2. CoCreateInstance首先調用COM庫的CoGetClassObject獲取類廠。
3. 該函數具體是通過調用了組件DLL輸出的DllGetClassObject來創建類廠。
4. DllGetClassObject通過new函數產生一個Cfactory的對象,并通過QueryInterface獲取其接口指針(一般是IclassFactory指針)。
5. 返回到COM庫的CoCreateInstance調用剛才獲得的接口指針(IclassFactory,類廠)的CreateInstance函數。
6. 該函數new指定的組件類,通過QueryInterface獲得指定的接口
7. CoCreateInstanse釋放掉IclassFactory指針(通過Release),然后向客戶程序返回獲得的指針。
8. 可以在客戶中使用獲得的接口了。
在第6步中,根據不同的CLSID創建不同的組件,可以實現一個類廠供該DLL中多個組件共用。但只是類共用,不是實例共用。一旦在創建類廠時通過CoGetClassObject指定了CLSID,則只能創建該COM組件的實例。
在這里我們將深入c++對象模型,來看下一些內部的實現細節。通常編譯器來處理這些。com的設計者充分利用了這些,因此,我們需要了解它。
當我們用匯編寫一個常規的程序時,我依靠編譯器為我們創建代碼段和數據段,內存中的一塊區域是我們執行的代碼,另一塊區域保存了我們需要的數據。
C++運行時動態內存分配,給每一個類實例,每一個小的代碼段它自己的數據段。換句話講,一個類的實例就是這個數據段,每一個類實例的數據描述都是保存在一個動態的數據區域。
或許你聽說過c++傳遞對象成員函數參數時,有一個隱藏的參數,即this指針。當一個人為對象寫一個低層的代碼時(在c++中編譯器會作這個工作,你不需要考慮),
你首先遇到的問題是"我在給哪個對象寫代碼?"
This指針是一個簡單的指針,它指向這個動態數據內存區域的這個類對象實例。當一個類對象函數被調用時,this指針就會被悄悄地傳遞過去。當這個對象的私有數據被訪問時,類的代碼區域就會使用this指針,來找到它的對象實例的數據。
對于一個com接口指針跟this指針很類似。使用中,com是一個接口規范,讓你看不到它的代碼實現。
; declare the ClassFactory object structure
ClassFactoryObject STRUCT
lpVtbl DWORD 0 ; function table pointer
nRefCount DWORD 0 ; object and reference count
ClassFactoryObject ENDS
; declare the MyCom object structure
MyComObject STRUCT
lpVtbl DWORD 0 ; function table pointer
nRefCount DWORD 0 ; reference count
nValue DWORD 0 ; interface private data
MyComObject ENDS
第一個lpVtbl是一個虛表指針,它指向一個虛函數表,我用它來控制每個接口的私有數據。就像這里的nRefCount和nValue。
這些結構所在的動態內存是通過CoTaskMemAlloc和CoTaskMemFree這兩個API函數來分配和釋放的。這兩個函數是由ole32.dll導出的。Ole32.dll還導出了很多的函數,例如比對GUIDs值和把轉換GUIDs為字符串,或者把字符串轉換為GUIDs。
為了舉例說明com接口的工作原理,我們創建一個簡單接口IMyCom(注:所有的com接口都有一個"I"前綴。同其他接口一樣,他派生于IUnknown接口,也就是說他的前三個函數是QueryInterface, AddRef, 和Release。下面我們添加幾個接口函數。下面看到的是c風格的函數原形:
HRESULT SetValue(long *pVal);
HRESULT GetValue(long newVal);
HRESULT RaiseValue(long newVal);
其中,SetValue 和 GetValue用于讀,設置我們接口的數據成員。RaiseValue用于增加這個數據的值。
這個結構在內存中的形式如下:
客戶端僅僅擁有一個分布式結構的指針(ppv)這個名字來源于它的c++形式的定義("pointer to pointer to (void)."),當創建類實例的時候,這個對象數據塊是動態分配和初始化的,虛函數表vtable和server functions是靜態的,他們在編譯時定義好。
有一點需要注意的是,虛函數表擁有的是函數指針,而并非是函數本身。因此,我們可以修改虛函數表中指向的例程,就可以簡單的"override"一個派生函數。
在例子中,IClassFactory和IMyCom都是派生于IUnknown接口,都繼承了QueryInterface,但是他們支持不同的接口,它們需要指向不同的例程,返回不同的結果。
因此,它們有各自的QueryInterface例程(QueryInterfaceCF 和 QueryInterfaceMC)被不同的虛函數表指向。
同樣的,AddRef和Release也要被不同的支持他們的接口來定制。
類型庫:
每一個com接口都是從系統注冊表中得到信息,這些接口的定義都是由一個被稱為接口定義語言(IDL)來描述的,在windows平臺下,使用MIDL進行編譯。我們可以利用vc開發環境,通過向導來創建一個原始的接口定義文件。
-------------------------------------------------- --------------------------------------------------- ----------------On WinTel platforms,
我建一個ATL工程,命名為MyComApp,然后選擇"insert a new ATL object",然后選擇"Simple Object",命名為:MyCom。這樣就創建了一個空的IMyCom接口,然后通過右鍵菜單,我們添加屬性SetValue和GetValue,并增加一個RaiseValue方法。然后我們保存退出工程,拷貝MyComApp.idl文件到我的匯編工程目錄。
下面就是這個idl文件的內容:
// MyCom.idl : IDL source for MyCom.dll
//
// This file will be processed by the MIDL tool to
// produce the type library (MyCom.tlb) and marshalling code
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(F8CE5E41-1135-11d4-A324-0040F6D487D9),
helpstring("IMyCom Interface"),
pointer_default(unique)
]
interface IMyCom : IUnknown
{
[propget, helpstring("property Value")]
HRESULT Value([out, retval] long *pVal);
[propput, helpstring("property Value")]
HRESULT Value([in] long newVal);
[helpstring("method Raise")]
HRESULT Raise(long Value);
};
[
uuid(F8CE5E42-1135-11d4-A324-0040F6D487D9),
version(1.0),
helpstring("MyComApp 1.0 Type Library")
]
library MyComLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(F8CE5E43-1135-11d4-A324-0040F6D487D9),
helpstring("MyCom Class")
]
coclass MyCom
{
[default] interface IMyCom;
};
};
這個文件可以被用來作為原型進一步進行接口定義。注意這里面有三個GUIDs,一個是為接口,一個是為coclass,一個是為類型庫。對于新的應用,它們的值一定不同。
透過這個定義的文件結構,我們很容易了解他的內容。
[propget, helpstring("property Value")] HRESULT Value([out, retval] long *pVal); [propput, helpstring("property Value")] HRESULT Value([in] long newVal); [helpstring("method Raise")] HRESULT Raise(long Value);
下面是這些接口在masm32中的定義:
GetValue PROTO :DWORD, :DWORD
SetValue PROTO :DWORD, :DWORD
RaiseValue PROTO :DWORD, :DWORD
他們有很大的不同,但是原因很簡單。類型庫中的接口是作為通用的,可以直接被客戶端象VB來使用。
為了創建類型庫,可以使用MIDL命令行來編譯idl文件:
MIDL MyCom.idl
編譯產生的幾個文件,除了MyCom.tlb外,其他的都可以忽略,接下來我們需要把類型庫添加到dll資源文件中。例如:
1 typelib MyCom.tlb
讓他作為資源文件中的第一個元素是很重要的,后續我們將會使用LoadTypeLib API函數來使用這個庫,同時這個函數也是希望在第一位置發現這個庫。 注冊組件:
DllRegisterServer 和 DllUnregisterServer 為我們注冊組件和注銷組件用.內容如下:
HKEY_CLASSES_ROOT\CMyCom
(Default) "CMyCom simple client"
HKEY_CLASSES_ROOT\CMyCom\CLSID
(Default) "{A21A8C43-1266-11D4-A324-0040F6D487D9}"
HKEY_CLASSES_ROOT\CLSID\{A21A8C43-1266-11D4-A324-0 040F6D487D9}
(Default) "CMyCom simple client"
HKEY_CLASSES_ROOT\CLSID\{A21A8C43-1266-11D4-A324-0 040F6D487D9}\CMyCom
(Default) "CMyCom"
HKEY_CLASSES_ROOT\CLSID\{A21A8C43-1266-11D4-A324-0 040F6D487D9}\InprocServer32
(Default) "C:\MASM32\MYCOM\MYCOM.DLL"
ThreadingModel "Single"
HKEY_CLASSES_ROOT\TypeLib\{A21A8C42-1266-11D4-A324 -0040F6D487D9}
(Default) (value not set)
HKEY_CLASSES_ROOT\TypeLib\{A21A8C42-1266-11D4-A324 -0040F6D487D9}\1.0
(Default) "MyCom 1.0 Type Library"
HKEY_CLASSES_ROOT\TypeLib\{A21A8C42-1266-11D4-A324 -0040F6D487D9}\1.0\0
(Default) (value not set)
HKEY_CLASSES_ROOT\TypeLib\{A21A8C42-1266-11D4-A324 -0040F6D487D9}\1.0\0\win32
(Default) " C:\masm32\COM\MyCom \MYCOM.DLL"
HKEY_CLASSES_ROOT\TypeLib\{A21A8C42-1266-11D4-A324 -0040F6D487D9}\1.0\FLAGS
(Default) "O"
HKEY_CLASSES_ROOT\TypeLib\{A21A8C42-1266-11D4-A324 -0040F6D487D9}\1.0\HELPDIR
(Default) "C:\masm32\COM\MyCom"
有一個鍵值是變化的,它是服務dll自身的路徑和文件名,在我的系統上,我把它放置在 "C:\MASM32\COM\MYCOM\MYCOM.DLL",當我注冊組件的時候,這個可以被檢測到,DllRegisterServer通過調用GetModuleFileName可以發現dll自身的存儲位置。
這里有大量的信息是關于這個com服務的,但是我們僅僅需要傳遞{A21A8C43-1266-11D4-A324-0040F6D487D9}這個ID和一個有效的接口ID給CoCreateInstance函數來實例化我們的com服務。這個函數將會跟蹤注冊表設置,利用CLSID來發現創建組件需要的東西,一旦它創建了組件,它將加載類型庫,以獲取更多需要的信息。
對我們來說非常幸運,最后的5個注冊入口項通過RegisterTypeLib函數可以完成。在DllRegisterServer中,我們通過一些列的注冊表函數來設置前面5項鍵值。然后調用RegisterTypeLib。 DllUnregisterServer函數刪除DllRegisterServer中的注冊表項,然后調用UnRegisterTypeLib。注意不要完全刪除HKEY_CLASSES_ROOT\CLSID\
實現 Unknown
AddRef_MC proc this_:DWORD
mov eax, this_
inc (MyComObject ptr [eax]).nRefCount
mov eax, (MyComObject ptr [eax]).nRefCount
ret ; note we return the object count
AddRef_MC endp
Release_MC proc this_:DWORD
mov eax, this_
dec (MyComObject ptr [eax]).nRefCount
mov eax, (MyComObject ptr [eax]).nRefCount
.IF (eax == 0)
; the reference count has dropped to zero
; no one holds reference to the object
; so let's delete it
invoke CoTaskMemFree, this_
dec MyCFObject.nRefCount
xor eax, eax ; clear eax (count = 0)
.ENDIF
ret ; note we return the object count
Release_MC endp
MyCom自己的成員實現:
GetValue proc this_:DWORD, pval:DWORD
mov eax, this_
mov eax, (MyComObject ptr [eax]).nValue
mov edx, pval
mov [edx], eax
xor eax, eax ; return S_OK
ret
GetValue endp
SetValue proc this_:DWORD, val:DWORD
mov eax, this_
mov edx, val
mov (MyComObject ptr [eax]).nValue, edx
xor eax, eax ; return S_OK
ret
SetValue endp
RaiseValue PROC this_:DWORD, val:DWORD
mov eax, this_
mov edx, val
add (MyComObject ptr [eax]).nValue, edx
xor eax, eax ; return S_OK
ret
RaiseValue ENDP
MyCom.dll, 這個com服務工程需要以下5個文件來編譯:
MyCom.asm 匯編源程序
MyCom.idl IDL文件,用于編譯產生MyCom.tlb
MyCom.tlb 類型庫,需要一個rc資源文件
rsrc.rc 資源文件,從中可以獲得類型庫信息
MyCom.DEF 標準的dll輸出文件
編譯后,代碼不會做任何事情,直到我們注冊它,我們可以使用命令行:
regsvr32 MyCom.dll注冊。
[三]用匯編語言訪問com對象
大量的細節使得Com看上去很復雜,但是使用起來卻很簡單。最難的部分就是理解里面的數據結構,盡管COM是語言無關的,但是他借用了很多c++的術語來描述自己。
為了能使用某個對象的com接口函數,你必須首先要從類廠中創建這個對象,并且讓他來返回接口指針。這個過程被CoCreateInstance這個API函數完成。當你使用完接口時,要調用Release方法。一個COM對象可以看作是一個服務,調用com的應用程序就是他的客戶端。
在調用com接口函數之前,你需要了解接口是什么,一個com接口就是一個函數指針表,我們還是從IUnknown接口開始,如果你創建了一個組件導出了IUnknown接口,那么你就有了一個全功能的com對象。IUnknown有三個基本的幾口方法,既然所有的接口都是從它派生出來,那么我們一定要記住,一個接口實際上就是一個函數指針成員組成的結構體。
例如:
IUnknown STRUCT DWORD
; IUnknown methods
IUnknown_QueryInterface QueryInterface_Pointer ?
IUnknown_AddRef AddRef_Pointer ?
IUnknown_Release Release_Pointer ?
IUnknown ENDS
它只有12個字節長,它具有3個DWORD指針來指向實際的實現函數。對于虛函數表,你一定聽說過,這些指針定義如下,因此我們可以讓masm在編譯我們的調用時進行一些類型檢查。
QueryInterface_Pointer typedef ptr QueryInterface_Proto
AddRef_Pointer typedef ptr AddRef_Proto
Release_Pointer typedef ptr Release_Proto
最后我們定義我們的函數如下:
QueryInterface_Proto typedef PROTO :DWORD, :DWORD, :DWORD
AddRef_Pointer typedef PROTO :DWORD
Release_Pointer typedef PROTO :DWORD
為了保持masm32松散的類型檢查一致,函數參數都定義為dword
定義接口是一個相當大的編輯就是,masm不支持前向引用。因此,我們不得不顛倒一下定義的順序。先定義函數頭,再定義函數指針,最后定義接口。實際上在使用接口時,你需要一個指向它的指針。
CoCreateInstance函數能用來直接返回一個接口指針。它實際上指向了擁有接口的對象。這個結構看上去如圖所示:
這個結構里有大量的間接訪問,使用宏可以簡化它。
當客戶端調用COM庫創建com組件時,它傳進了一個地址用于存放對象指針。這個就是我們所說的ppv. 從c++的角度來講,叫做指向指針的指針,void類型代表無類型。它保存了另一個指針pv的地址。pv指向了虛函數表。
例如:我們使用CoCreateInstance函數成功的返回了一個接口指針ppv,我們想看下它是否支持其他的接口,我們可以調用QueryInterface方法。用c++的方法描述QueryInterface如下:
(HRESULT) SomeObject::QueryInterface (this:pObject, IID:pGUID, ppv2:pInterface)
用匯編寫法如下:
; get pointer to the object
mov eax, ppv
; and use it to find the interface structure
mov edx, [eax]
; push the function parameters onto the stack
push OFFSET ppv2
push OFFSET IID_ISomeOtherInterface
push dword ppv
; and then call that method
call dword ptr [edx + 0]
使用invoke調用簡化如下:
; get pointer to the object
mov eax, ppv
; and use it to find the interface structure
mov edx, [eax]
; and then call that method
invoke (IUnknown PTR [edx]).IUnknown_QueryInterface, ppv,
ADDR IID_SomeOtherInterface, ADDR ppv_new
注意IUnknown PTR [edx]這個類型轉換,是讓編譯器知道使用哪個結構來得到QueryInterface函數在虛表中的正確偏移。其中有一個模糊的地方,注意我修改了函數名字為"IUnknown_QueryInterface",這個名字修飾時必要的。當你有一個大的com工程,有許多相似的接口,你就會遇到麻煩。不同的接口對應不同的方法表示,是非常有效的。
coinvoke 宏,這個宏定義在oaidl.inc文件中。使用它,可以進一步簡化com調用。
;------------------------------------------------- --------------------
; coinvoke MACRO
;
;
; pInterface pointer to a specific interface instance
; Interface the Interface's struct typedef
; Function which function or method of the interface to perform
; args all required arguments
; (type, kind and count determined by the function)
;
coinvoke MACRO pInterface:REQ, Interface:REQ, Function:REQ, args:VARARG
LOCAL istatement, arg
FOR arg, ;; run thru args to see if edx is lurking in there
IFIDNI ,
.ERR
ENDIF
ENDM
istatement CATSTR ,,
IFNB ;; add the list of parameter arguments if any
istatement CATSTR istatement, ,
ENDIF
mov edx, pInterface
mov edx, [edx]
istatement
ENDM
;------------------------------------------------- --------------------
因此,前面的QueryInterface方法調用就可以簡化成:
coinvoke ppv ,IUnknown, QueryInterface, ADDR IID_SomeOtherInterface,
ADDR ppnew
注意這里名字修飾是隱藏在宏中處理的。
代碼后面附上。。。
作者: combojiang
時間: 2007-12-10 14:09
鏈接: http://bbs.pediy.com/showthread.php?t=56328
詳細信息:
聲明:本貼參考網站:http://ourworld.compuserve.com/
[一]基礎知識篇
組件對象模型(Com)在windows操作系統中應用越來越廣泛。com因為大量的技術細節顯得很復雜,但是正是這種復雜才使com組件的調用顯得十分簡單。 com和使用程序采用server/client架構。下面我們將在后續的兩篇中介紹com組件的編寫與調用。
com編程時當前程序開發的熱點,各種編程語言都為組件編寫提供了很好的支持,但是匯編語言例外,匯編語言開發組件沒有優勢。但是透過匯編開發的了解,可以使我們了解com組件的工作原理。好了,閑話少說,開始介紹:)
所有的inc頭文件都要滿足如下特點:
1) masm32松散的類型定義約定將繼續使用。就是說參數可以被定義為他們的基本類型,代表性的如:DWORD
2) 里面不能創建任何的代碼,僅僅包含定義信息,頭文件里面需要包含代碼,則必須定義為宏。
3) 結構體應該參照他們的C原形來定義。
4) GUID 結構定義在windows.inc文件中,GUID的值應該通過textequ宏來定義,這樣不會直接產生任何代碼。
5) 接口定義分為兩步:
1.一個通用的宏產生一個通用的接口結構。
2.使用接口名字來修飾結構自身的方法名字。
這種方式可以有效地避免namespace沖突,并且方便接口定義結構繼承。
6)COM接口函數調用使用coinvoke宏。
GUIDS
EXAMPLE:
sIID_IUnknown TEXTEQU ;; run thru args to see if edx is lurking in there
IFIDNI ,
.ERR
ENDIF
ENDM
istatement CATSTR ,,
IFNB ;; add the list of parameter arguments if any
istatement CATSTR istatement, ,
ENDIF
mov edx, pInterface
mov edx, [edx]
istatement
ENDM
;------------------------------------------------- --------------------
例如:QueryInterface方法調用如下:
coinvoke ppv ,IUnknown, QueryInterface, ADDR IID_SomeOtherInterface,
ADDR ppnew
HRESULTS
任何一個com接口函數的返回值類型都是一個hResult, 4個字節長。返回值在eax寄存器中。可以用這個值來判斷函數調用是否成功。
.IF !SIGN?
; function passed
.ELSE
; function failed
.ENDIF
接下來,我們定義了宏來簡化它:
.IF SUCCEEDED ; TRUE if SIGN bit not set
.IF FAILED ; TRUE is SIGN bit set
結論:
以上這些是你用匯編開發com需要用到的,這些適用于activex的開發。
[二]用匯編語言編寫com組件
組件對象模型com是以win32 dll或exe形式發布的執行代碼組成的。Com是由一些對象和對象的接口組成,在com里,接口提供對象操作的機制。 而接口是由一個或者多個相關的方法、屬性、事件組成的。在這里我們開發一個簡單的但是功能齊全的一個進程內com組件(即以dll形式存在)。
這里假設你已經了解了com對象模型的基礎知識,了解什么是虛表,什么是虛函數表指針。如果你不熟悉這些,建議看看《com本質論》這本書。
我們先來分析下進程內com服務的組成。由于它是一個dll形式發布的,其中包括5個重要的函數。其中后面的四個是要作為dll導出函數來導出的。
DllMain: 這是動態鏈接庫德第一個入口函數,它在庫被加載的時候被調用,通過這個函數,可以對客戶端程序進行檢查。
DllRegisterServer:通過這個函數能夠實現組建的自我注冊,注冊信息作為資源保存在動態鏈接庫中,這個函數能夠讀取資源,把信息寫進注冊表,使用
regsvr32.exe 注冊組建時,實際上是調用了組件輸出的這個函數。
DllUnregisterServer: 當一個組件不再使用時,這個組件應該能夠提供自我卸載,regsvr32.exe能調用這個函數,實現這一步。
DllCanUnloadNow: com服務中的全局變量用于保存它的狀態,客戶端可以周期性的調用這個函數,檢查組件服務器是否在使用,然后把它卸載。
DllGetClassObject: 這是完成組件輸出的函數,這個輸出需要3個參數,創建組件的GUID,要創建組件接口的GUID以及創建后指向對象的指針。如果組件對象或者接口不被支持,執行將失敗。
到現在為止,我們應該注意到一件事情,就是如果不是因為間接訪問,com將什么也不是。實際上,DllGetClassObject函數返回的對象不是我們要尋找的對象,它是類廠對象,一個類廠對象了解如何實例化其他任何的類。第一層的間接訪問允許組件創建的細節被指定,如果它僅僅是簡單而又直接的返回一個我們要尋找的對象指針,那么說明對象已經存在,那樣,我們將不能設置和控制關于構造對象的任何參數。
DllGetClassObject返回一個IClassFactory接口,這個接口是從IUnknown派生的,另外他還有自己的兩個重要的成員函數。
HRESULT CreateInstance(
IUnknown * pUnkOuter, //Pointer to outer object when part of an
// aggregate REFIID riid,
REFIID riid, //Reference to the interface identifier
oid** ppvObject); //Address of output variable that receives
// the interface pointer requested in riid
HRESULT LockServer(BOOL fLock);
//Increments or decrements the lock count
LockServer用來控制類廠對象的引用計數,系統檢查改計數以確定是否要卸載組件,即:控制類廠的生存期。
CreateInstance是最重要的,類廠組件的唯一功能是創建其它組件。一個類廠組件可以對應多種普通COM組件,但每個類廠組件的實例只能創建一種COM組件。
它接收一個接口GUID,返回該接口的指針。它并不接受組件的CLSID,所以一個類廠實例只能夠創建一種COM組件,即傳給 CoGetClassObject的CLSID對應的組件。
客戶、COM庫、組件dll、類廠、組件之間的交互過程:
1. 客戶首先調用COM庫的CoCreateInstance函數來創建COM組件。
2. CoCreateInstance首先調用COM庫的CoGetClassObject獲取類廠。
3. 該函數具體是通過調用了組件DLL輸出的DllGetClassObject來創建類廠。
4. DllGetClassObject通過new函數產生一個Cfactory的對象,并通過QueryInterface獲取其接口指針(一般是IclassFactory指針)。
5. 返回到COM庫的CoCreateInstance調用剛才獲得的接口指針(IclassFactory,類廠)的CreateInstance函數。
6. 該函數new指定的組件類,通過QueryInterface獲得指定的接口
7. CoCreateInstanse釋放掉IclassFactory指針(通過Release),然后向客戶程序返回獲得的指針。
8. 可以在客戶中使用獲得的接口了。
在第6步中,根據不同的CLSID創建不同的組件,可以實現一個類廠供該DLL中多個組件共用。但只是類共用,不是實例共用。一旦在創建類廠時通過CoGetClassObject指定了CLSID,則只能創建該COM組件的實例。
在這里我們將深入c++對象模型,來看下一些內部的實現細節。通常編譯器來處理這些。com的設計者充分利用了這些,因此,我們需要了解它。
當我們用匯編寫一個常規的程序時,我依靠編譯器為我們創建代碼段和數據段,內存中的一塊區域是我們執行的代碼,另一塊區域保存了我們需要的數據。
C++運行時動態內存分配,給每一個類實例,每一個小的代碼段它自己的數據段。換句話講,一個類的實例就是這個數據段,每一個類實例的數據描述都是保存在一個動態的數據區域。
或許你聽說過c++傳遞對象成員函數參數時,有一個隱藏的參數,即this指針。當一個人為對象寫一個低層的代碼時(在c++中編譯器會作這個工作,你不需要考慮),
你首先遇到的問題是"我在給哪個對象寫代碼?"
This指針是一個簡單的指針,它指向這個動態數據內存區域的這個類對象實例。當一個類對象函數被調用時,this指針就會被悄悄地傳遞過去。當這個對象的私有數據被訪問時,類的代碼區域就會使用this指針,來找到它的對象實例的數據。
對于一個com接口指針跟this指針很類似。使用中,com是一個接口規范,讓你看不到它的代碼實現。
; declare the ClassFactory object structure
ClassFactoryObject STRUCT
lpVtbl DWORD 0 ; function table pointer
nRefCount DWORD 0 ; object and reference count
ClassFactoryObject ENDS
; declare the MyCom object structure
MyComObject STRUCT
lpVtbl DWORD 0 ; function table pointer
nRefCount DWORD 0 ; reference count
nValue DWORD 0 ; interface private data
MyComObject ENDS
第一個lpVtbl是一個虛表指針,它指向一個虛函數表,我用它來控制每個接口的私有數據。就像這里的nRefCount和nValue。
這些結構所在的動態內存是通過CoTaskMemAlloc和CoTaskMemFree這兩個API函數來分配和釋放的。這兩個函數是由ole32.dll導出的。Ole32.dll還導出了很多的函數,例如比對GUIDs值和把轉換GUIDs為字符串,或者把字符串轉換為GUIDs。
為了舉例說明com接口的工作原理,我們創建一個簡單接口IMyCom(注:所有的com接口都有一個"I"前綴。同其他接口一樣,他派生于IUnknown接口,也就是說他的前三個函數是QueryInterface, AddRef, 和Release。下面我們添加幾個接口函數。下面看到的是c風格的函數原形:
HRESULT SetValue(long *pVal);
HRESULT GetValue(long newVal);
HRESULT RaiseValue(long newVal);
其中,SetValue 和 GetValue用于讀,設置我們接口的數據成員。RaiseValue用于增加這個數據的值。
這個結構在內存中的形式如下:
客戶端僅僅擁有一個分布式結構的指針(ppv)這個名字來源于它的c++形式的定義("pointer to pointer to (void)."),當創建類實例的時候,這個對象數據塊是動態分配和初始化的,虛函數表vtable和server functions是靜態的,他們在編譯時定義好。
有一點需要注意的是,虛函數表擁有的是函數指針,而并非是函數本身。因此,我們可以修改虛函數表中指向的例程,就可以簡單的"override"一個派生函數。
在例子中,IClassFactory和IMyCom都是派生于IUnknown接口,都繼承了QueryInterface,但是他們支持不同的接口,它們需要指向不同的例程,返回不同的結果。
因此,它們有各自的QueryInterface例程(QueryInterfaceCF 和 QueryInterfaceMC)被不同的虛函數表指向。
同樣的,AddRef和Release也要被不同的支持他們的接口來定制。
類型庫:
每一個com接口都是從系統注冊表中得到信息,這些接口的定義都是由一個被稱為接口定義語言(IDL)來描述的,在windows平臺下,使用MIDL進行編譯。我們可以利用vc開發環境,通過向導來創建一個原始的接口定義文件。
-------------------------------------------------- --------------------------------------------------- ----------------On WinTel platforms,
我建一個ATL工程,命名為MyComApp,然后選擇"insert a new ATL object",然后選擇"Simple Object",命名為:MyCom。這樣就創建了一個空的IMyCom接口,然后通過右鍵菜單,我們添加屬性SetValue和GetValue,并增加一個RaiseValue方法。然后我們保存退出工程,拷貝MyComApp.idl文件到我的匯編工程目錄。
下面就是這個idl文件的內容:
// MyCom.idl : IDL source for MyCom.dll
//
// This file will be processed by the MIDL tool to
// produce the type library (MyCom.tlb) and marshalling code
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(F8CE5E41-1135-11d4-A324-0040F6D487D9),
helpstring("IMyCom Interface"),
pointer_default(unique)
]
interface IMyCom : IUnknown
{
[propget, helpstring("property Value")]
HRESULT Value([out, retval] long *pVal);
[propput, helpstring("property Value")]
HRESULT Value([in] long newVal);
[helpstring("method Raise")]
HRESULT Raise(long Value);
};
[
uuid(F8CE5E42-1135-11d4-A324-0040F6D487D9),
version(1.0),
helpstring("MyComApp 1.0 Type Library")
]
library MyComLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(F8CE5E43-1135-11d4-A324-0040F6D487D9),
helpstring("MyCom Class")
]
coclass MyCom
{
[default] interface IMyCom;
};
};
這個文件可以被用來作為原型進一步進行接口定義。注意這里面有三個GUIDs,一個是為接口,一個是為coclass,一個是為類型庫。對于新的應用,它們的值一定不同。
透過這個定義的文件結構,我們很容易了解他的內容。
[propget, helpstring("property Value")] HRESULT Value([out, retval] long *pVal); [propput, helpstring("property Value")] HRESULT Value([in] long newVal); [helpstring("method Raise")] HRESULT Raise(long Value);
下面是這些接口在masm32中的定義:
GetValue PROTO :DWORD, :DWORD
SetValue PROTO :DWORD, :DWORD
RaiseValue PROTO :DWORD, :DWORD
他們有很大的不同,但是原因很簡單。類型庫中的接口是作為通用的,可以直接被客戶端象VB來使用。
為了創建類型庫,可以使用MIDL命令行來編譯idl文件:
MIDL MyCom.idl
編譯產生的幾個文件,除了MyCom.tlb外,其他的都可以忽略,接下來我們需要把類型庫添加到dll資源文件中。例如:
1 typelib MyCom.tlb
讓他作為資源文件中的第一個元素是很重要的,后續我們將會使用LoadTypeLib API函數來使用這個庫,同時這個函數也是希望在第一位置發現這個庫。 注冊組件:
DllRegisterServer 和 DllUnregisterServer 為我們注冊組件和注銷組件用.內容如下:
HKEY_CLASSES_ROOT\CMyCom
(Default) "CMyCom simple client"
HKEY_CLASSES_ROOT\CMyCom\CLSID
(Default) "{A21A8C43-1266-11D4-A324-0040F6D487D9}"
HKEY_CLASSES_ROOT\CLSID\{A21A8C43-1266-11D4-A324-0 040F6D487D9}
(Default) "CMyCom simple client"
HKEY_CLASSES_ROOT\CLSID\{A21A8C43-1266-11D4-A324-0 040F6D487D9}\CMyCom
(Default) "CMyCom"
HKEY_CLASSES_ROOT\CLSID\{A21A8C43-1266-11D4-A324-0 040F6D487D9}\InprocServer32
(Default) "C:\MASM32\MYCOM\MYCOM.DLL"
ThreadingModel "Single"
HKEY_CLASSES_ROOT\TypeLib\{A21A8C42-1266-11D4-A324 -0040F6D487D9}
(Default) (value not set)
HKEY_CLASSES_ROOT\TypeLib\{A21A8C42-1266-11D4-A324 -0040F6D487D9}\1.0
(Default) "MyCom 1.0 Type Library"
HKEY_CLASSES_ROOT\TypeLib\{A21A8C42-1266-11D4-A324 -0040F6D487D9}\1.0\0
(Default) (value not set)
HKEY_CLASSES_ROOT\TypeLib\{A21A8C42-1266-11D4-A324 -0040F6D487D9}\1.0\0\win32
(Default) " C:\masm32\COM\MyCom \MYCOM.DLL"
HKEY_CLASSES_ROOT\TypeLib\{A21A8C42-1266-11D4-A324 -0040F6D487D9}\1.0\FLAGS
(Default) "O"
HKEY_CLASSES_ROOT\TypeLib\{A21A8C42-1266-11D4-A324 -0040F6D487D9}\1.0\HELPDIR
(Default) "C:\masm32\COM\MyCom"
有一個鍵值是變化的,它是服務dll自身的路徑和文件名,在我的系統上,我把它放置在 "C:\MASM32\COM\MYCOM\MYCOM.DLL",當我注冊組件的時候,這個可以被檢測到,DllRegisterServer通過調用GetModuleFileName可以發現dll自身的存儲位置。
這里有大量的信息是關于這個com服務的,但是我們僅僅需要傳遞{A21A8C43-1266-11D4-A324-0040F6D487D9}這個ID和一個有效的接口ID給CoCreateInstance函數來實例化我們的com服務。這個函數將會跟蹤注冊表設置,利用CLSID來發現創建組件需要的東西,一旦它創建了組件,它將加載類型庫,以獲取更多需要的信息。
對我們來說非常幸運,最后的5個注冊入口項通過RegisterTypeLib函數可以完成。在DllRegisterServer中,我們通過一些列的注冊表函數來設置前面5項鍵值。然后調用RegisterTypeLib。 DllUnregisterServer函數刪除DllRegisterServer中的注冊表項,然后調用UnRegisterTypeLib。注意不要完全刪除HKEY_CLASSES_ROOT\CLSID\
實現 Unknown
AddRef_MC proc this_:DWORD
mov eax, this_
inc (MyComObject ptr [eax]).nRefCount
mov eax, (MyComObject ptr [eax]).nRefCount
ret ; note we return the object count
AddRef_MC endp
Release_MC proc this_:DWORD
mov eax, this_
dec (MyComObject ptr [eax]).nRefCount
mov eax, (MyComObject ptr [eax]).nRefCount
.IF (eax == 0)
; the reference count has dropped to zero
; no one holds reference to the object
; so let's delete it
invoke CoTaskMemFree, this_
dec MyCFObject.nRefCount
xor eax, eax ; clear eax (count = 0)
.ENDIF
ret ; note we return the object count
Release_MC endp
MyCom自己的成員實現:
GetValue proc this_:DWORD, pval:DWORD
mov eax, this_
mov eax, (MyComObject ptr [eax]).nValue
mov edx, pval
mov [edx], eax
xor eax, eax ; return S_OK
ret
GetValue endp
SetValue proc this_:DWORD, val:DWORD
mov eax, this_
mov edx, val
mov (MyComObject ptr [eax]).nValue, edx
xor eax, eax ; return S_OK
ret
SetValue endp
RaiseValue PROC this_:DWORD, val:DWORD
mov eax, this_
mov edx, val
add (MyComObject ptr [eax]).nValue, edx
xor eax, eax ; return S_OK
ret
RaiseValue ENDP
MyCom.dll, 這個com服務工程需要以下5個文件來編譯:
MyCom.asm 匯編源程序
MyCom.idl IDL文件,用于編譯產生MyCom.tlb
MyCom.tlb 類型庫,需要一個rc資源文件
rsrc.rc 資源文件,從中可以獲得類型庫信息
MyCom.DEF 標準的dll輸出文件
編譯后,代碼不會做任何事情,直到我們注冊它,我們可以使用命令行:
regsvr32 MyCom.dll注冊。
[三]用匯編語言訪問com對象
大量的細節使得Com看上去很復雜,但是使用起來卻很簡單。最難的部分就是理解里面的數據結構,盡管COM是語言無關的,但是他借用了很多c++的術語來描述自己。
為了能使用某個對象的com接口函數,你必須首先要從類廠中創建這個對象,并且讓他來返回接口指針。這個過程被CoCreateInstance這個API函數完成。當你使用完接口時,要調用Release方法。一個COM對象可以看作是一個服務,調用com的應用程序就是他的客戶端。
在調用com接口函數之前,你需要了解接口是什么,一個com接口就是一個函數指針表,我們還是從IUnknown接口開始,如果你創建了一個組件導出了IUnknown接口,那么你就有了一個全功能的com對象。IUnknown有三個基本的幾口方法,既然所有的接口都是從它派生出來,那么我們一定要記住,一個接口實際上就是一個函數指針成員組成的結構體。
例如:
IUnknown STRUCT DWORD
; IUnknown methods
IUnknown_QueryInterface QueryInterface_Pointer ?
IUnknown_AddRef AddRef_Pointer ?
IUnknown_Release Release_Pointer ?
IUnknown ENDS
它只有12個字節長,它具有3個DWORD指針來指向實際的實現函數。對于虛函數表,你一定聽說過,這些指針定義如下,因此我們可以讓masm在編譯我們的調用時進行一些類型檢查。
QueryInterface_Pointer typedef ptr QueryInterface_Proto
AddRef_Pointer typedef ptr AddRef_Proto
Release_Pointer typedef ptr Release_Proto
最后我們定義我們的函數如下:
QueryInterface_Proto typedef PROTO :DWORD, :DWORD, :DWORD
AddRef_Pointer typedef PROTO :DWORD
Release_Pointer typedef PROTO :DWORD
為了保持masm32松散的類型檢查一致,函數參數都定義為dword
定義接口是一個相當大的編輯就是,masm不支持前向引用。因此,我們不得不顛倒一下定義的順序。先定義函數頭,再定義函數指針,最后定義接口。實際上在使用接口時,你需要一個指向它的指針。
CoCreateInstance函數能用來直接返回一個接口指針。它實際上指向了擁有接口的對象。這個結構看上去如圖所示:
這個結構里有大量的間接訪問,使用宏可以簡化它。
當客戶端調用COM庫創建com組件時,它傳進了一個地址用于存放對象指針。這個就是我們所說的ppv. 從c++的角度來講,叫做指向指針的指針,void類型代表無類型。它保存了另一個指針pv的地址。pv指向了虛函數表。
例如:我們使用CoCreateInstance函數成功的返回了一個接口指針ppv,我們想看下它是否支持其他的接口,我們可以調用QueryInterface方法。用c++的方法描述QueryInterface如下:
(HRESULT) SomeObject::QueryInterface (this:pObject, IID:pGUID, ppv2:pInterface)
用匯編寫法如下:
; get pointer to the object
mov eax, ppv
; and use it to find the interface structure
mov edx, [eax]
; push the function parameters onto the stack
push OFFSET ppv2
push OFFSET IID_ISomeOtherInterface
push dword ppv
; and then call that method
call dword ptr [edx + 0]
使用invoke調用簡化如下:
; get pointer to the object
mov eax, ppv
; and use it to find the interface structure
mov edx, [eax]
; and then call that method
invoke (IUnknown PTR [edx]).IUnknown_QueryInterface, ppv,
ADDR IID_SomeOtherInterface, ADDR ppv_new
注意IUnknown PTR [edx]這個類型轉換,是讓編譯器知道使用哪個結構來得到QueryInterface函數在虛表中的正確偏移。其中有一個模糊的地方,注意我修改了函數名字為"IUnknown_QueryInterface",這個名字修飾時必要的。當你有一個大的com工程,有許多相似的接口,你就會遇到麻煩。不同的接口對應不同的方法表示,是非常有效的。
coinvoke 宏,這個宏定義在oaidl.inc文件中。使用它,可以進一步簡化com調用。
;------------------------------------------------- --------------------
; coinvoke MACRO
;
;
; pInterface pointer to a specific interface instance
; Interface the Interface's struct typedef
; Function which function or method of the interface to perform
; args all required arguments
; (type, kind and count determined by the function)
;
coinvoke MACRO pInterface:REQ, Interface:REQ, Function:REQ, args:VARARG
LOCAL istatement, arg
FOR arg, ;; run thru args to see if edx is lurking in there
IFIDNI ,
.ERR
ENDIF
ENDM
istatement CATSTR ,,
IFNB ;; add the list of parameter arguments if any
istatement CATSTR istatement, ,
ENDIF
mov edx, pInterface
mov edx, [edx]
istatement
ENDM
;------------------------------------------------- --------------------
因此,前面的QueryInterface方法調用就可以簡化成:
coinvoke ppv ,IUnknown, QueryInterface, ADDR IID_SomeOtherInterface,
ADDR ppnew
注意這里名字修飾是隱藏在宏中處理的。
代碼后面附上。。。
總結
以上是生活随笔為你收集整理的masm32开发com组件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓端APP遥控树莓派小车
- 下一篇: 树莓派小车避障之——超声波控制(2.1)