程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> COM技術初探(1)

COM技術初探(1)

編輯:關於VC++

一、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)。

(待續)

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved