COM技術初探(一)

COM技術初探(一)



目錄


一. COM是一個更好的 C++
   1. COM 是什麼
   2. 從 C++ 到 DLL 再到 COM
      2.1 C++
      2.2 DLL
      2.3 COM

二. COM基礎
   1. COM基本知識
      1.1 返回值HRESULT
      1.2 初識idl
      1.3 IUnkown接口
   2. 一個比較簡單的COM
      2.1 interface.h文件
      2.2 math.h文件
      2.3 math.cpp文件
      2.4 simple.cpp文件
      2.5 Math組件的二進制結構圖
      2.6 小結

三. 純手工創建一個COM組件
   1. 從建工程到實現註冊
      1.1 創建一個類型爲win32 dll工程
      1.2 定義接口文件
      1.3 增加註冊功能
         1.3.1 增加一個MathCOM.def文件
         1.3.2 DllRegisterServer()和DllUnregisterServer()
      1.4 MathCOM.cpp文件
      1.5 小結
   2. 實現ISmipleMath,IAdvancedMath接口和DllGetClassObject()
      2.1 實現ISmipleMath和IAdvancedMath接口
      2.2 COM組件調入大致過程
      2.3 DllGetClassObject()實現
      2.4 客戶端
      2.5 小結
   3. 類廠

附錄  
A 我對dll的一點認識
一. 沒有lib的dll
   1.1 建一個沒有lib的dll
   1.2 調試沒有lib的dll
二. 帶有lib的dll
   2.1 創建一個帶有lib的dll
   2.2 調試帶有引用但沒有頭文件的dll
三. 帶有頭文件的dll
   3.1 創建一個帶有引出信息頭文件的dll
   3.2 調試帶有頭文件的dll
四. 小結



一、COM是一個更好的C++

1、COM 是什麼

Don Box 說"COM IS LOVE"。COM 的全稱是 Component Object Model 組件對象模型。

2、從 C++ 到 DLL 再到 COM

2.1 C++

如某一軟件廠商發佈一個類庫(CMath四則運算),此時類庫的可執行代碼將成爲客戶應用中不可分割的一部分。假設此類庫的所產生的機器碼在目標可執行文件中佔有4MB的空間。當三個應用程序都使用CMath庫時,那麼每個可執行文件都包含4MB的類庫代碼(見圖1.1)。當三個應用程序共同運行時,他們將會佔用12MB的虛擬內存。問題還遠不於此。一旦類庫廠商發現CMath類庫有一個缺陷後,發佈一個新的類庫,此時需要要求所有運用此類庫的應用程序。此外別無他法了。




圖1.1 CMath 的三個客戶

2.2 DLL

解決上面問題的一個技術是將CMath類做成動態鏈接庫(DLL ,Dynamic Link Library)的形式封裝起來 。
在使用這項技術的時候,CMath的所有方法都將被加到 CMath dll 的引出表(export list)中,而且鏈接器將會產生一個引入庫(import library)。這個庫暴露了CMath的方法成員的符號 。當客戶鏈接引入庫時,有一些存根會被引入到可執行文件中,它在運行時通知裝載器動態裝載 CMath Dll
當 CMath 位於dll中時,他的運行模型見圖1.2


圖1.2 CMath引入庫

2.3 COM

"簡單地把C++類定義從dll中引出來"這種方案並不能提供合理的二進制組件結構。因爲C++類那既是接口也是實現。這裏需要把接口從實現中分離出來才能提供二進制組件結構。此時需要有二個C++類,一個作爲接口類另一個作爲實現類。讓我們開始COM之旅吧。

二、COM基礎

1、 COM基本知識

1.1 返回值HRESULT

COM要求所有的方法都會返回一個HRESULT類型的錯誤號。HRESULT 其實就一個類型定義:

    typedef LONG HRESULT;

有關HRESULT的定義見 winerror.h 文件

//  Values are 32 bit values layed out as follows:
//
//  3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
//  1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
//  +-+----+-------------------------+---------------------------------+
//  |S| Res|     Facility            |     Code                        |
//  +-+----+-------------------------+---------------------------------+
//
//  where
//
//      S - is the severity code
//
//          0 - Success
//          1 - Error
//
//      Res- is a reserved bit
//
//      Facility - is the facility code
//
//      Code - is the facility''s status code

我們一般下面的宏來判斷方法是否成功:

#define SUCCEEDED(hr)(long(hr)>=0)
#define FAILED(hr)(long(hr)<0)

1.2 初識 IDL

每個標準的COM組件都需要一個接口定義文件,文件的擴展名爲IDL。讓我們看IUnknow接口的定義文件是怎樣的。

[
  local,
  object,
  uuid(00000000-0000-0000-C000-000000000046),
  pointer_default(unique)
]

interface IUnknown
{
    typedef [unique] IUnknown *LPUNKNOWN;

cpp_quote("//////////////////////////////////////////////////////////////////")
cpp_quote("// IID_IUnknown and all other system IIDs are provided in UUID.LIB")
cpp_quote("// Link that library in with your proxies, clients and servers")
cpp_quote("//////////////////////////////////////////////////////////////////")

    HRESULT QueryInterface(
        [in] REFIID riid,
        [out, iid_is(riid)] void **ppvObject);
    ULONG AddRef();
    ULONG Release();
}

[local]屬性禁止產生網絡代碼。
[object]屬性是表明定義的是一個COM接口,而不是DEC風格的接口。
[uuid]屬性給接口一個GUID。
[unique]屬性表明null(空)指針爲一個合法的參數值。
[pointer_defaul]屬性所有的內嵌指針指定一個默認指針屬性
typedef [unique] IUnknown *LPUNKNOWN;這是一個類型定義
cpp_quote這個比較有趣,這是一個在idl文件寫註解的方法。這些註解將保存到***.h和***_i.c文件中
[in]表示這個參數是入參
[out]表示這個參數是出參
[iid_is(riid)]表示這個參數需要前一個的riid 參數。

注意:所有具有out屬性的參數都需要是指針類型。

1.3 IUnkown接口

在整個例子除了IUnkown這個東西,其他應該不會感到陌生吧!COM要求(最基本的要求)所有的接口都需要從IUnknown接口直接或間接繼承,所以IUnknown接口有"萬惡之源"之稱。
IUnkown接口定義了三個方法。

HRESULT QueryInterface([in] REFIID riid,[out] void **ppv);
ULONG AddRef();
ULONG Release();    

其中 AddReft() 和Release()負責對象引用計數用的,而 QueryInterface()方法是用於查詢所實現接口用的。每當COM組件被引用一次就應調用一次AddRef()方法。而當客戶端在釋放COM組件的某個接口時就需要調用Release()方法。
這裏所講的請在下面的例子仔細體會。

2、一個比較簡單的COM

此例子共有四個文件組成:
 

文件名 說明
Interface.h 接口類定義文件
Math.h和Math.cpp 實現類文件
Simple.cpp 主函數文件 這裏用來當作COM的客戶端

2.1 interface.h 文件

#ifndef INTERFACE_H
#define INTERFACE_H
#include <unknwn.h>

//{7C8027EA-A4ED-467c-B17E-1B51CE74AF57}
static const GUID IID_ISimpleMath = 
{ 0x7c8027ea, 0xa4ed, 0x467c, { 0xb1, 0x7e, 0x1b, 0x51, 0xce, 0x74, 0xaf, 0x57 } };

//{CA3B37EA-E44A-49b8-9729-6E9222CAE84F}
static const GUID IID_IAdvancedMath = 
{ 0xca3b37ea, 0xe44a, 0x49b8, { 0x97, 0x29, 0x6e, 0x92, 0x22, 0xca, 0xe8, 0x4f } };

interface ISimpleMath : public IUnknown
{
public:
	virtual int Add(int nOp1, int nOp2) = 0;		
	virtual int Subtract(int nOp1, int nOp2) = 0;
	virtual int Multiply(int nOp1, int nOp2) = 0;
	virtual int Divide(int nOp1, int nOp2) = 0;
};

interface IAdvancedMath : public IUnknown
{
public:
	virtual int Factorial(int nOp1) = 0;
	virtual int Fabonacci(int nOp1) = 0;
};
#endif    

此文件首先 #include <unknwn.h> 將 IUnknown 接口定義文件包括進來。
接下來定義了兩個接口,GUID(Globally Unique Identifier全局唯一標識符)它能保證時間及空間上的唯一。
ISmipleMath接口裏定義了四個方法,而IAdvancedMath接口裏定義了二個方法。這些方法都是虛函數,而整個 ISmipleMath 與 IAdvancedMath 抽象類就作爲二進制的接口。

2.2 math.h文件

#include "interface.h"

class CMath : public ISimpleMath,
			  public IAdvancedMath
{
private:
	ULONG m_cRef;

private:
	int calcFactorial(int nOp);
	int calcFabonacci(int nOp);

public:
	//IUnknown Method
	STDMETHOD(QueryInterface)(REFIID riid, void **ppv);
	STDMETHOD_(ULONG, AddRef)();
	STDMETHOD_(ULONG, Release)();

	//	ISimpleMath Method
	int Add(int nOp1, int nOp2);
	int Subtract(int nOp1, int nOp2);
	int Multiply(int nOp1, int nOp2);
	int Divide(int nOp1, int nOp2);

	//	IAdvancedMath Method
	int Factorial(int nOp);
	int Fabonacci(int nOp);
};    

此類爲實現類,他實現了ISmipleMath和IAdvancedMath兩個接口類(當然也可以只實現一個接口類)。
請注意:m_cRef 是用來對象計數用的。當 m_cRef 爲0組件對象應該自動刪除。

2.3 math.cpp文件

#include "interface.h"
#include "math.h"

STDMETHODIMP CMath::QueryInterface(REFIID riid, void **ppv)
{//	這裏這是實現dynamic_cast的功能,但由於dynamic_cast與編譯器相關。
	if(riid == IID_ISimpleMath)
		*ppv = static_cast(this);
	else if(riid == IID_IAdvancedMath)
		*ppv = static_cast(this);
	else if(riid == IID_IUnknown)
		*ppv = static_cast(this);
	else {
		*ppv = 0;
		return E_NOINTERFACE;
	}

	reinterpret_cast(*ppv)->AddRef();	//這裏要這樣是因爲引用計數是針對組件的
	return S_OK;
}

STDMETHODIMP_(ULONG) CMath::AddRef()
{
	return ++m_cRef;
}

STDMETHODIMP_(ULONG) CMath::Release()
{
	ULONG res = --m_cRef;	// 使用臨時變量把修改後的引用計數值緩存起來
	if(res == 0)		// 因爲在對象已經銷燬後再引用這個對象的數據將是非法的
		delete this;
	return res;
}

int CMath::Add(int nOp1, int nOp2)
{
	return nOp1+nOp2;
}

int CMath::Subtract(int nOp1, int nOp2)
{
	return nOp1 - nOp2;
}

int CMath::Multiply(int nOp1, int nOp2)
{
	return nOp1 * nOp2;
}

int CMath::Divide(int nOp1, int nOp2)
{
	return nOp1 / nOp2;
}

int CMath::calcFactorial(int nOp)
{
	if(nOp <= 1)
		return 1;

	return nOp * calcFactorial(nOp - 1);
}

int CMath::Factorial(int nOp)
{
	return calcFactorial(nOp);
}

int CMath::calcFabonacci(int nOp)
{
	if(nOp <= 1)
		return 1;

	return calcFabonacci(nOp - 1) + calcFabonacci(nOp - 2);
}

int CMath::Fabonacci(int nOp)
{
	return calcFabonacci(nOp);
}
 CMath::CMath()
{
	m_cRef=0;
}   

此文件是CMath類定義文件。

2.4 simple.cpp文件

#include "math.h"
#include <iostream>

using namespace std;

int main(int argc, char* argv[])
{
	ISimpleMath *pSimpleMath = NULL;//聲明接口指針
	IAdvancedMath *pAdvMath = NULL;

	//創建對象實例,我們暫時這樣創建對象實例,COM有創建對象實例的機制
	CMath *pMath = new CMath;	

	//查詢對象實現的接口ISimpleMath
	pMath->QueryInterface(IID_ISimpleMath, (void **)&pSimpleMath);	
	if(pSimpleMath)
		cout << "10 + 4 = " << pSimpleMath->Add(10, 4) << endl;

	//查詢對象實現的接口IAdvancedMath
	pSimpleMath->QueryInterface(IID_IAdvancedMath, (void **)&pAdvMath);
	if(pAdvMath)
		cout << "10 Fabonacci is " << pAdvMath->Fabonacci(10) << endl;	

	pAdvMath->Release();
	pSimpleMath->Release();
	return 0;
}

此文件相當於客戶端的代碼,首先創建一個CMath對象,再根據此對象去查詢所需要的接口,如果正確得到所需接口指針,再調用接口的方法,最後再將接口的釋放掉。

2.5 Math組件的二進制結構圖



                              圖1.3 Math組件二進制結構圖

2.6 小結

此例子從嚴格意義上來並不是真正的COM組件(他不是dll),但他已符合COM的最小要求(實現IUnknown接口)。接下來我們來做一COM dll(但還不用ATL)。

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