用C完畢的一個基本COM接口IFoo?

用C完畢的一個基本COM接口IFoo(一)把該文中完畢的代碼收拾彙總到一個項目中。其時僅僅完畢到一箇中心時刻,關鍵在闡明COM接口的完畢原理,還沒有包括類廠的有些。往後還需連續添加類廠等高檔功用。

文件組成:

ifoo.h    COM接口IFoo,接口ID IID_IFoo 聲明文件。

outside.c   COM接口完畢。這裡完畢IFoo的是一個方案體COutside.

util.h    一些宏界說、大局函數、變量聲明文件。

main.c    筆者為完畢項目添加的文件。供給main函數、內存處置函數Alloc,Free的完畢(封裝C工作庫函數malloc和free.)、接口ID界說。

COM接口究竟是啥?

COM接口是一個指向虛函數表的指針。經過這個指針能夠拜訪內存中某處的各個功用塊,實施預界說的功用,完畢用戶的使命。這些功用塊以函數的辦法存在(想不出還有其他辦法:))並被調用。它們有一個一起點:都包括一個指針參數,指向這些功用要操作的數據地址。在C++中,這個地址就是政策的首地址,也就是類成員函數中隱含的this指針。在C函數中並沒有這種現成的便當,因而代碼完畢中在接口界說時仍運用了接口指針(HRESULT (__stdcall * QueryInterface)   (IFoo * This,  const IID * const, void **)),而在接口函數完畢時依據方案體方案方案,從這個接口指針核算得到政策實例指針。

typedef struct IFoo{ struct IFooVtbl * lpVtbl;} IFoo;typedef struct IFooVtbl IFooVtbl;struct IFooVtbl{

 HRESULT (__stdcall * QueryInterface)   (IFoo * This,  const IID * const, void **) ; ULONG (__stdcall * AddRef)    (IFoo * This) ; ULONG (__stdcall * Release)   (IFoo * This) ;

 HRESULT (__stdcall * SetValue)         (IFoo * This,  int) ; HRESULT (__stdcall * GetValue)         (IFoo * This,  int *) ;};

COM接口的需求:

每一個COM接口(指向的虛函數表)的頭三個函數有必要是IUnknown接口的函數:QueryInterface,AddRef和Release.在C++中,稱為從IUnknown接口承繼。

關於調用QueryInterface照顧查詢IID_IUnknwon得到的接口指針值,同一個政策完畢的悉數接口有必要一樣。這是差異兩個COM政策是不是是同一個政策的標準。

宏界說“#define IUNK_VTABLE_OF(x) ((IUnknownVtbl *)((x)->lpVtbl))”闡明在預處置輸出文件main.i中能夠找到IUnknownVtbl和IFooVtbl的聲明:

typedef struct IUnknownVtbl    {

        HRESULT ( __stdcall *QueryInterface )(            IUnknown * This,             const IID * const riid,             void **ppvObject);

        ULONG ( __stdcall *AddRef )(            IUnknown * This);

        ULONG ( __stdcall *Release )(            IUnknown * This);

    } IUnknownVtbl;

    struct IUnknown    {         struct IUnknownVtbl *lpVtbl;    };

struct IFooVtbl{

 HRESULT (__stdcall * QueryInterface)   (IFoo * This,  const IID * const, void **) ; ULONG (__stdcall * AddRef)    (IFoo * This) ; ULONG (__stdcall * Release)   (IFoo * This) ;

 HRESULT (__stdcall * SetValue)         (IFoo * This,  int) ; HRESULT (__stdcall * GetValue)         (IFoo * This,  int *) ;};

該宏界說的作用就是把IFoo接口中的IFooVtbl類型的指針拿出來((x)->lpVtbl)),並強逼轉換((IUnknownVtbl *))成IUnknownVtbl.

“強逼轉換”的作用是啥呢?是怎麼做到的呢?

很明顯,作用就是得到的指針不再是IFooVtbl *類型,而是變成了IUnknownVtbl *類型。至於做法,體系大概記載每一個變量、表達式的類型。當進行強逼類型轉換時,就(暫時地)修改其類型為轉換到的類型。

同理,QueryInterface, AddRef, Release宏界說中的(IUnknown *)也是這種用法。

能夠看到,宏“IUNK_VTABLE_OF”的作用是供宏QueryInterface,宏AddRef,宏Release引證,把IFooVtbl *類型轉換為IUnknownVtbl *類型,完畢抵達調用IUnknownVtbl中界說的三個QueryInterface,AddRef,Release函數。

那麼,這種大費周章的意圖是啥呢?為何不以IFooVtbl中三個函數的界說辦法(不經過強逼轉換來轉換成有必要的類型),直接調用IFooVtbl中界說的函數呢?儘管強逼轉換在參數值上並不會構成改動,完畢調用的也是IFooVtbl界說的函數(FooQueryInterface,FooAddRef,FooRelease)。

為何一定要經過IUnknown接口指針調用這三個函數呢?修改QueryInterface宏界說如下:

#define QueryInterface(pif, iid, pintf) \

(((pif)->lpVtbl)->QueryInterface(pif, iid, (void **)(pintf)))

即經過IFoo接口指針來調用由IUnknown引進的函數,有啥不對的當地嗎?

實驗標明,將QueryInterface宏界說如下也能夠編譯經過,實施起來也沒有呈現任何異常。

#define QueryInterface(pif, iid, pintf) \

(((pif)->lpVtbl)->QueryInterface(pif, iid, (void **)(pintf)))

關於IUnknown接口的三個函數,調用時傳遞的參數是IUnknown *類型(見QueryInterface, AddRef, Release宏界說),而函數界說中(FooQueryInterface, FooAddRef, FooRelease)聲明的參數是IFoo *類型,這種紛歧起的狀況是怎麼呈現的?這種紛歧起不會有疑問嗎?

這種紛歧起的發生是由於從紛歧樣的視點看待致使的。若是從IUnknown接口來看,那麼接口函數中的第一個參數類型就是IUnknown *;若是從IFoo來看,那麼第一個參數的類型就是IFoo *.

這種紛歧起性僅僅鍼關於編譯器關於類型的編譯需求有意義的,在接口完畢及運用時,傳遞給lpVtbl->QueryInterface, lpVtbl->AddRef,lpVtbl->Release的第一個參數在值上都是一樣的,都是完畢該接口的內存地址(在本例中是COutside政策的首地址)。一些語法表象回想

函數指針變量界說、賦值及調用。

HRESULT (__stdcall * pQI)   (IFoo * This,  const IID * const, void **) ;

界說一個函數指針變量pQI,該變量指向“回來HRESULT,取3個參數別離為類型IFoo *,const IID * const, void **”的函數。

typedef HRESULT (__stdcall * QIType)   (IFoo * This,  const IID * const, void **) ;

界說一個函數指針類型,該類型的指針指向“回來HRESULT,取3個參數別離為類型IFoo *,const IID * const, void **”的函數。

HRESULT __stdcall QueryInterface(IFoo * This,  const IID * const, void **) ;//函數聲明示例pQI = 0; // 函數指針賦值,0標明不指向任何函數。pQI = QueryInterface; // 函數指針賦值,pQI指向QueryInterface。pQI = &QueryInterface; // 與上面等價。

QueryInterface(&this->ifoo, riid, ppv); // 運用函數名直接調用pQI(&this->ifoo, riid, ppv); // 函數指針調用(*pQI)(&this->ifoo, riid, ppv); // 第二種函數指針調用辦法

宏界說、翻開規矩

關於宏,一向有一種水中望月的感受,好像很隨意,怎麼來都行,比方:

#define AddRef(pif) \

(IUNK_VTABLE_OF(pif)->AddRef((IUnknown *)(pif)))

宏界說大概是能夠嵌套的,即宏界說的“內容”中還能夠包括(嵌套)宏,如本例,“IUNK_VTABLE_OF”就是嵌套宏。在翻開的時分,將嵌套的宏也一起翻開(替換成界說的內容),直到不再有宏間斷。

那麼就有兩個疑問:

1.若是被嵌套的宏包括(直接或直接)界說的宏,那麼翻開就沒完沒了,死循環了。

2.若是界說的內容中有跟界說的宏同名的字符串(比方上面的比方IUNK_VTABLE_OF),那麼怎麼差異這同名的東東是嵌套的宏(需求翻開),仍是通常的字符串(不需求翻開)?

函數調用標準約好、main函數調用標準。

一開始把幾個文件彙總到項目裡時,編譯通不過,過失提示大致意思是,不能把一種調用標準的函數指針轉換成另一種調用標準的函數指針。後來把調用標準改為   /Gz(__stdcall),編譯為(Compile As)改為/TC(Compile As C Code)就好了。

想來是關於。c文件,編譯器缺省運用的是__cdecl,而IFoo中的接口宏界說在win32下翻開成了__stdcall,所以呈現了仇視。而運用/Gz強逼未聲明調用標準的函數運用__stdcall,完畢就與聲明一起了。

(size_t)&(((s *)0)->m)

c++程序員或許都曉得,拜訪地址“0”處的成員是一大忌,會構成GP.可是,取地址“0”處的成員的地址,卻是個合法的操作。儘管地址“0”處並沒有啥內容,可是,若是在地址0處寄存一個內容,那麼該內容中的成員也是有地址的。本例中正是美好地運用這種辦法,從接口地址核算得出完畢該接口的實例地址,進而拜訪實例的內部變量。用C完畢的一個基本COM接口IFoo(二)在C完畢COM接口系列1中完畢的com接口IFoo與運用它的客戶耦合在一起,沒有完畢在各自別離的模塊,因而不符合模塊化編程思維。本期添加類廠支撐,以使接口的完畢與接口的運用相別離。

---------------------------------------------------

類廠的作用究竟是啥?

將接口的完畢與客戶運用別離開來嗎?

不盡然。運用CoCreateInstance,客戶能夠徹底不用曉得類廠的存在,而創立組件,獲取組件完畢的接口並運用。

即COM庫能夠徹底拋開類廠的概念,而是供給一個這樣的函數原型:CoCreateObject(REFID rclsid,……,REFID riid,void **ppItf);用戶在調用的時分能夠對riid供給IID_Unknown或許特定於該政策的一個接口,直接獲取該政策的IUnknown或特定的接口指針。

能夠看到,這正是CoCreateInstance所作的工作。

1 類廠供給了直接創立類政策的辦法:用戶能夠先獲取並持有類廠接口指針,經過該指針所指向的類廠接口創立類政策。適用於需求創立多個(或重複創立)類政策的當地,削減了每次都要定位政策庫並把政策庫裝入內存的開支。

2 類廠供給了確保組件庫留在內存不被卸載出去的另一種辦法:類廠接口函數LockServer.組件庫保護一個庫計劃計數器,只需該計數器為0時,組件庫才容許自個被卸載出內存。(與此相對,引證計數是類政策計劃的,經過該類完畢的各個接口來保護。若是一個類政策的引證計數抵達0,那麼該政策佔有的內存就被開釋,該政策上的接口指針也不再有用。)

除了調用LockServer判定組件庫以外,當創立的組件個數大於0時,組件庫也不能被卸載。也能夠說,調用一次LockServer()的作用相當於創立了一個組件。

-----------------------------------------------------------------------

客戶一側:1 運用一個接口需求曉得哪些信息?

備選:接口IID類政策(類廠)CLSID(或ProgID)

接口函數原型(參數個數,類型,回來值)

完畢接口組件的線程模型(進程內、進程外、長途)?

類型庫typelib信息?

效能一側:2 完畢一個組件和接口以供客戶調用,需求供給哪些東西?

備選:悉數客戶運用組件和接口所需的內容額定的還有:

--------------------------------------------------------------------

為dll添加。def文件與直接在需求導出的函數界說處指定_declspec( dllexport )有差異嗎?若是有是啥差異?我發如今outdll.c中這樣指定:

__declspec( dllexport ) HRESULT DllGetClassObject (REFCLSID rclsid, REFIID riid, void **ppv)

會發生編譯過失:

1>------ Build started: Project: outside, Configuration: Debug Win32 ------1>Compiling...1>outdll.c1>d:\outside-cf\outside\outdll.c(19) : error C2375: 'DllGetClassObject' : redefinition; different linkage1> c:\program files\microsoft visual studio 8\vc\platformsdk\include\objbase.h(833) : see declaration of 'DllGetClassObject'1>Build log was saved at "file://d:\outside-cf\outside\Debug\BuildLog.htm"1>outside - 1 error(s), 0 warning(s)========== Build: 0 succeeded, 1 failed, 1 up-to-date, 0 skipped ==========

c2375的闡明意思是犯錯的函數運用的聯接指示符與之前聲明的紛歧樣。

Compiler Error C2375

'function' : redefinition; different linkage

The function is already declared with a different linkage specifier.

objbase.h中聲明白DllGetClassObject()函數:

STDAPI  DllGetClassObject(IN REFCLSID rclsid, IN REFIID riid, OUT LPVOID FAR* ppv);

而運用。def文件就沒有疑問。初度實施作用:

疑問就是總有一個分配的內存沒有開釋:

依據打印出來的內存地址能夠差異,大概是先創立的類廠政策的內存沒有開釋。

查看代碼,main()中並沒有忘記調用Release(pCF)開釋類廠政策。打印Release(pCF)的回來值,發現是1,即在類廠接口指針上少調用了一次Release,那麼,究竟是哪裡少的呢?

main()函數中有關類廠政策引證計數的當地就是CoGetClassObject和Release(CreateInstance跟類廠自個的引證計數無關),這是一對添加引證計數和削減引證計數的對應操作,所以,main()中大概沒有疑問。

那麼,就只需創立類廠政策的時分了。下面看一下類廠政策是如何創立的。

首要,main調用CoGetClassObject,該函數就調用dll中的DllGetClassObject.由於是初度調用(不思考其他客戶運用該dll的狀況),程序實施到CreateClassFactory(……),該函數實施完後,類廠政策的引證計數是1.

由於創立成功,因而持續向下實施到QueryInterface,此刻,類廠政策的引證計數變成了2.然後,DllGetClassObject回來,com庫函數CoGetClassObject也大概回來。注意,此刻的類廠政策引證計數已經是2了!因而,疑問就出在這裡。main調用一次CoGetClassObject後,類廠政策的引證計數是2,而不是我想向中的1.所以,後邊調用一次Release也就當然無法開釋掉類場政策了。

1 HRESULT DllGetClassObject (REFCLSID rclsid, REFIID riid, void **ppv)

  2 {

  3     *ppv = 0;

  4     if (IsEqualCLSID (rclsid, &CLSID_Outside))

  5     {

  6

  7         if (!vpcfOutside)

  8

  9         {

  10

  11             HRESULT hr = CreateClassFactory (&CLSID_Outside, CreateOutside,

  12                                              &IID_IClassFactory, &vpcfOutside);

  13

  14                 if (hr != NOERROR)

  15

  16                     return hr;

  17         }

  18

  19         return QueryInterface (vpcfOutside, riid, ppv);

  20

  21     }

  22

  23     return E_FAIL;

  24 }找到了緣由,改正就很簡略了。這裡我覺得需求把DllGetClassObject作如下修改:

1 HRESULT DllGetClassObject (REFCLSID rclsid, REFIID riid, void **ppv)

  2 {

  3     *ppv = 0;

  4     if (IsEqualCLSID (rclsid, &CLSID_Outside))

  5     {

  6

  7         if (!vpcfOutside)

  8

  9         {

  10

  11             HRESULT hr = CreateClassFactory (&CLSID_Outside, CreateOutside,

  12                                              &IID_IClassFactory, &vpcfOutside);

  13

  14                 if (hr != NOERROR)

  15

  16                     return hr;

  17

  18     if(IsEqualIID(riid,&IID_IClassFactory))

  19     {

  20      *ppv = vpcfOutside;// Set *ppv to vpcfOutside directly instead of QueryInterface if first time creation

  21      return NOERROR;

  22     }

  23     else

  24     {

  25      Release(vpcfOutside);// Any interface requested (riid) other than IID_ClassFactory and IID_Unknown not support by class factory,

  26                           // call Release to free the memory.

  27      return E_FAIL;

  28     }

  29

  30         }

  31

  32         return QueryInterface (vpcfOutside, riid, ppv);

  33

  34     }

  35

  36     return E_FAIL;

  37 }

修改後在實施,內存都正常開釋了。CreateClassFactory代碼闡明

1 HRESULT CreateClassFactory (REFCLSID rclsid,

  2     HRESULT (*pfnCreate)(IUnknown *, REFIID, void **),

  3     REFIID riid, void **ppv)

  4 {

  5     ClassFactory *this;

  6     HRESULT hr;

  7

  8     *ppv = 0;

  9     if (hr = Alloc (sizeof (ClassFactory), &this))

  10     return hr;

  11

  12     this->icf.lpVtbl = &vtblClassFactory;

  13     this->cRef = 1;  // After this call, cRef==1

  14

  15     this->pfnCreate = pfnCreate;

  16

  17     hr = QueryInterface (&this->icf, riid, ppv);  // After this call, cRef==2

  18     Release (&this->icf);  // Corresponds to "this->cRef = 1", ater this call, cRef==1

  19

  20     return hr;

  21 }

能夠看到,兩行代碼的作用是對引證計數增1及減1,這兩行代碼實施後,對引證計數的影響相互抵消,等於沒有改動引證計數。那麼,把這兩行一起註釋掉,是不是能夠呢?

我的答覆是:在本例中能夠。由於這兩行代碼之間的QueryInterface總是能夠實施成功的(由於是用IDD_ClassFactory來調用該函數的)。所以,即便把這兩行代碼一起註釋掉,CreateClassFactory實施完畢後,類廠政策的引證計數也增了1,往後調用Release就能夠開釋掉類廠政策佔用的內存。

可是,若是CFQueryInterface的代碼編寫中除了過失,比方,像這樣寫:

1 static HRESULT CFQueryInterface (IClassFactory *pcf, REFIID riid, void **ppv)

  2 {

  3     ClassFactory *this = IMPL (ClassFactory, icf, pcf);

  4

  5     if (IsEqualIID (riid, &IID_IUnknown)

  6 //            IsEqualIID (riid, &IID_IClassFactory))   // Comment out this condition to create an error

  7         *ppv = &this->icf;

  8     else

  9     {

  10         *ppv = 0;

  11         return E_NOINTERFACE;

  12     }

  13

  14     AddRef ((IClassFactory *)*ppv);

  15

  16     return NOERROR;

  17 }

那麼,這兩行代碼之間的QueryInterface就會實施犯錯,那麼類廠政策佔用的內存就永世沒有機緣開釋了。

也就是說,AddRef和Release儘管在作用上對引證計數來說相互抵消,但Release函數供給了開釋政策內存的機緣(當引證計數為0時),若是不成對的調用他們,也就失去了處置政策內存(開釋政策佔用的內存)的機緣。

組件庫outside文件闡明:

IFoo.h      IFoo接口聲明

outside.c   組件政策、IFoo接口完畢

cf.c        類廠政策、IClassFactory接口完畢

outdll.c    組件庫導出函數完畢

outside.def 組件庫模塊界說文件,導出函數聲明

outside.reg 組件庫註冊文件

相關問題答案