不用ATL框架純手工實現COM程式外回撥

旋律1155665發表於2020-12-20

不用ATL框架純手工實現COM程式外回撥

COM全稱為(Component Object Model)元件物件模型。COM是由Microcsoft提出的元件標準,它定義了元件程式之間進行互動的標準。COM元件可分為程式內元件和程式外元件。本編文章主要講述程式外元件的回撥實現。

COM程式外元件模型

COM程式外元件是以獨立程式的形式向客戶提供物件服務的,客戶呼叫元件程式提供的服務,必然要跨程式呼叫。在COM中這是通過代理存根來實現的。

客戶程式呼叫COM程式外元件的過程主要為以下6步:

  1. 客戶程式呼叫COM介面函式
  2. 代理物件通過LPC呼叫服務程式中的存根DLL
  3. 在服務程式中的存根DLL呼叫COM元件物件提供的介面
  4. COM元件物件介面執行完畢返回結果給存根DLL
  5. 存根DLL將結果通過LPC傳遞給客戶程式中代理DLL
  6. 在客戶程式中的代理DLL再將結果返回給客戶

具體呼叫過程如下圖: COM程式外元件介面呼叫

客戶程式呼叫COM程式外元件的過程雖然複雜但是這些實現都是對開發人員透明的。開發人員不需要關心底層是怎麼實現,開發人員只要像呼叫正常函式那樣去使用COM介面函式就行。正如圖中虛線部分一樣。

代理存根除了完成LPC呼叫功能外,他還實現將我們的介面引數的序列化和反序列化。序列化即在客戶呼叫時將介面封裝成一個資料包,反序列化即在服務端時將資料包拆解還原。

通過登錄檔管理COM物件

COM規範使用128位的GUID來標識COM物件和介面,客戶程式通過GUID值來建立COM物件並與物件互動。因為客戶程式和元件程式是獨立的,客戶程式在建立COM物件時並不知道元件程式具體的位置。這時我們就需要通過系統登錄檔提供資訊來建立元件物件。 系統登錄檔是一個全作業系統範圍內的公用資訊倉庫,其中包含了COM元件必要的資訊。

元件程式需要把它實現的COM物件資訊儲存到登錄檔上,這個步驟叫做元件的註冊。

  • 登錄檔結構 如下圖可以看出登錄檔包含以下幾項:HKEY_CLASSES_ROOT、HKEY_CURRENT_USER、HKEY_LOCAL_MACHINE、HKEY_USERS、HKEY_CURRENT_CONFIG,每個項下面又有若干子項。
登錄檔
登錄檔
  • COM元件註冊資訊 COM元件的註冊資訊在HKEY_CLASSES_ROOT鍵下,最主要的資訊在CLSID鍵下,該鍵下列出了當前系統上所有的元件資訊。如下圖: CLSID

在每個CLSID子鍵下包含了元件物件相關的資訊,如程式內元件CLSID子鍵下包含了InprocServer32子鍵該鍵的預設值為元件的全路徑名。程式外元件則包含LocalServer32子鍵該鍵的預設值為程式外元件的全路徑名。如下圖: LocalServer32

對於程式外元件登錄檔中還必須有代理DLL和存根DLL的資訊, 這些資訊被儲存在子鍵名為ProxyStubClsid或者ProxyStubClsid32下。除了使用 CLSID來標識一個COM物件外還可以使用字串來表示,這個資訊被儲存在ProgID子鍵下。ProgID和CLSID可以通過CLSIDFromProgID和ProgIDFromCLSID互相轉換。

  • COM元件的註冊

COM元件的註冊可以使用RegSvr32, 但是元件必須有DllRegisterServer和DllUnregisterServer。

註冊時使用RegSvr32 aa.dll 反註冊時使用RegSvr32 /u aa.dll

對於程式外元件,為了支援自注冊必須支援/RegSever和/UnregSever兩個引數。

類廠

類廠就是COM類的工廠,COM庫通過類廠建立COM物件。IClassFactory是類廠的基類。定義如下:

    IClassFactory : public IUnknown
    {
    public:
        virtual /* [local] */ HRESULT STDMETHODCALLTYPE CreateInstance
            /* [unique][in] */ IUnknown *pUnkOuter,
            /* [in] */ REFIID riid,
            /* [iid_is][out] */ void **ppvObject)
 
0;
        
        virtual /* [local] */ HRESULT STDMETHODCALLTYPE LockServer
            /* [in] */ BOOL fLock)
 
0;
        
    };

IClassFactory中成員函式CreateInstance負責建立對應的COM物件。

COM程式外類廠程式碼實現:

// ComFactory.h
#ifndef __COMFACTORY__
#define __COMFACTORY__
#include <Unknwn.h>

class CCOMFactory : public IClassFactory
{
public:
 CCOMFactory();
 ~CCOMFactory();

 //IUnknown members
 HRESULT __stdcall QueryInterface(const IID& iid, void **ppv);
 ULONG __stdcall AddRef();
 ULONG __stdcall Release();

 //IClassFactory members
 HRESULT __stdcall CreateInstance(IUnknown *, const IID& iid, void **ppv);
 HRESULT __stdcall LockServer(BOOL);
public:
 static BOOL RegisterFactory();
 static void UnregisterFactory();
 static BOOL CanUnloadNow();

public:
 static CCOMFactory *theFactory;
 static DWORD dwRegister;

protected:
 LONG  m_Ref;
};

#endif // __COMFACTORY__

// ComFactory.cpp

#include "ComFactory.h"
#include "CComInterface.h"

extern LONG    g_lockNumber;
extern LONG    g_comTestNumber;
extern DWORD   g_dwMainThreadID;
extern "C" const GUID CLSID_COMTest;

CCOMFactory::CCOMFactory() : m_Ref(0)
{

}

CCOMFactory::~CCOMFactory()
{

}

HRESULT CCOMFactory::QueryInterface(const IID& iid, void **ppv)
{
 if (iid == IID_IUnknown)
 {
  *ppv = (IUnknown *) this;
  ((IUnknown *)(*ppv))->AddRef();
 }
 else if (iid == IID_IClassFactory)
 {
  *ppv = (IClassFactory *) this;
  ((IClassFactory *)(*ppv))->AddRef();
 }
 else
 {
  *ppv = NULL;
  return E_NOINTERFACE;
 }
 return S_OK;
}

ULONG CCOMFactory::AddRef()
{
 return  InterlockedIncrement(&m_Ref);
}

ULONG CCOMFactory::Release()
{
 if (InterlockedDecrement(&m_Ref) == 0)
 {
  delete this;
  return 0;
 }
 return  (ULONG)m_Ref;
}

HRESULT CCOMFactory::CreateInstance(IUnknown *pUnknownOuter, const IID& iid, void **ppv)
{
 CComInterface * pObj;
 HRESULT hr;

 *ppv = NULL;
 hr = E_OUTOFMEMORY;
 if (NULL != pUnknownOuter)
 {
  return CLASS_E_NOAGGREGATION;
 }

 pObj = CComInterface::GetVirtualObject();
 if (NULL == pObj)
 {
  return hr;
 }

 hr = pObj->QueryInterface(iid, ppv);

 if (hr != S_OK)
 {
  InterlockedDecrement(&g_comTestNumber);
  delete pObj;
 }
 return hr;
}

HRESULT CCOMFactory::LockServer(BOOL bLock)
{
 if (bLock)
 {
  InterlockedIncrement(&g_lockNumber);
 }
 else
 {
  InterlockedDecrement(&g_lockNumber);
  if (CanUnloadNow())
  {
  }
 }

 return NOERROR;
}

CCOMFactory *CCOMFactory::theFactory = NULL;
DWORD CCOMFactory::dwRegister = 0;

BOOL CCOMFactory::RegisterFactory()
{
 theFactory = new CCOMFactory();
 theFactory->AddRef();
 IUnknown *pUnkForFactory = (IUnknown  *)theFactory;

 HRESULT hr = ::CoRegisterClassObject(
  CLSID_COMTest,
  pUnkForFactory,
  CLSCTX_LOCAL_SERVER,
  REGCLS_MULTIPLEUSE,
  &dwRegister);
 if (FAILED(hr))
 {
  theFactory->Release();
  return FALSE;
 }
 return TRUE;
}

void CCOMFactory::UnregisterFactory()
{
 if (dwRegister != 0)
 {
  ::CoRevokeClassObject(dwRegister);
 }

 if (theFactory != NULL)
 {
  theFactory->Release();
 }
}

BOOL CCOMFactory::CanUnloadNow()
{
 if (g_lockNumber > 0 || g_comTestNumber > 0)
 {
  return FALSE;
 }
 else
 {
  return TRUE;
 }
}

在CreateInstance函式中通過CComInterface::GetVirtualObject()單例獲取COM物件,並初始化了ppv指標。上述程式碼中有一些COM物件建立的程式碼和生命週期控制的程式碼。在下面的文章會提及。

在工廠類建立完後客戶因該如何利用這個工廠類來得到COM物件呢,客戶端可以利用CoGetClassObject、CoCreateInstance和CoCreateInstanceEx來建立物件。CoCreateInstance是對CoGetClassObject進行了封裝使客戶對COM工廠類透明,CoCreateInstanceEx可用來建立遠端的COM物件。

客戶程式和元件工廠互動過程
客戶程式和元件工廠互動過程

COM初始化和釋放需要使用CoInitialize和CoUninitialize函式。

程式外元件的啟動

當客戶呼叫CoCreateInstance函式時,COM會從登錄檔獲取元件的完整路徑並檢查元件程式是否啟動,若程式為啟動則COM庫則會啟動程式,若程式已經啟動則直接執行物件的建立。

所以程式外元件需要實現元件的自注冊到登錄檔,並且將類廠也註冊到登錄檔。程式碼實現如下:

// AutoRegistry.h
#ifndef __Registry_H__
#define __Registry_H__

HRESULT RegisterServer(const CLSID& clsid,
 const char *szFileName,
 const char* szProgID,
 const char* szDescription,
 const char* szVerIndProgID)
;


HRESULT UnregisterServer(const CLSID& clsid,
 const char* szProgID,
 const char* szVerIndProgID)
;

#endif
// AutoRegistry.cpp
#include <objbase.h>
#include <assert.h>

#include "AutoRegistry.h"

BOOL SetKeyAndValue(const char* pszPath,
 const char* szSubkey,
 const char* szValue)
;


void CLSIDtoString(const CLSID& clsid,
 char* szCLSID,
 int length)
;


LONG DeleteKey(HKEY hKeyParent, const char* szKeyString);


const int CLSID_STRING_SIZE = 39;


HRESULT RegisterServer(const CLSID& clsid,
 const char *szFileName,
 const char* szProgID,
 const char* szDescription,
 const char* szVerIndProgID)


{
 char szCLSID[CLSID_STRING_SIZE];
 CLSIDtoString(clsid, szCLSID, sizeof(szCLSID));

 char szKey[64];
 strcpy_s(szKey, _countof("CLSID\\"), "CLSID\\");
 strcat_s(szKey, _countof(szKey), szCLSID);

 SetKeyAndValue(szKey, NULL, szDescription);


 SetKeyAndValue(szKey, "LocalServer32", szFileName);


 if (szProgID != NULL) {
  SetKeyAndValue(szKey, "ProgID", szProgID);
  SetKeyAndValue(szProgID, "CLSID", szCLSID);
 }

 if (szVerIndProgID) {
  SetKeyAndValue(szKey, "VersionIndependentProgID",
   szVerIndProgID);

  SetKeyAndValue(szVerIndProgID, NULL, szDescription);
  SetKeyAndValue(szVerIndProgID, "CLSID", szCLSID);
  SetKeyAndValue(szVerIndProgID, "CurVer", szProgID);

  SetKeyAndValue(szProgID, NULL, szDescription);
  SetKeyAndValue(szProgID, "CLSID", szCLSID);
 }

 return S_OK;
}


HRESULT UnregisterServer(const CLSID& clsid,
 const char* szProgID,
 const char* szVerIndProgID)

{

 char szCLSID[CLSID_STRING_SIZE];
 CLSIDtoString(clsid, szCLSID, sizeof(szCLSID));

 char szKey[64];
 strcpy_s(szKey, _countof("CLSID\\"), "CLSID\\");
 strcat_s(szKey, _countof(szCLSID), szCLSID);

 LONG lResult = DeleteKey(HKEY_CLASSES_ROOT, szKey);

 if (szVerIndProgID != NULL)
  lResult = DeleteKey(HKEY_CLASSES_ROOT, szVerIndProgID);

 if (szProgID != NULL)
  lResult = DeleteKey(HKEY_CLASSES_ROOT, szProgID);

 return S_OK;
}

void CLSIDtoString(const CLSID& clsid,
 char* szCLSID,
 int length)

{
 assert(length >= CLSID_STRING_SIZE);
 LPOLESTR wszCLSID = NULL;
 HRESULT hr = StringFromCLSID(clsid, &wszCLSID);
 assert(SUCCEEDED(hr));

 size_t ret;
 wcstombs_s(&ret, szCLSID, length, wszCLSID, length);

 CoTaskMemFree(wszCLSID);
}


LONG DeleteKey(HKEY hKeyParent,           // Parent of key to delete
 const char* lpszKeyChild)
  // Key to delete
{
 // Open the child.
 HKEY hKeyChild;
 size_t dsize = strlen(lpszKeyChild) + 1;
 wchar_t* dest = new wchar_t[dsize];
 size_t i;
 mbstowcs_s(&i, dest, dsize, lpszKeyChild, strlen(lpszKeyChild));
 LONG lRes = RegOpenKeyEx(hKeyParent, dest, 0,
  KEY_ALL_ACCESS, &hKeyChild);
 delete[] dest;
 if (lRes != ERROR_SUCCESS)
 {
  return lRes;
 }

 FILETIME time;
 char szBuffer[256];
 DWORD dwSize = 256;
 while (RegEnumKeyExA(hKeyChild, 0, szBuffer, &dwSize, NULL,
  NULLNULL, &time) == S_OK)
 {
  lRes = DeleteKey(hKeyChild, szBuffer);
  if (lRes != ERROR_SUCCESS)
  {
   RegCloseKey(hKeyChild);
   return lRes;
  }
  dwSize = 256;
 }

 RegCloseKey(hKeyChild);

 return RegDeleteKeyA(hKeyParent, lpszKeyChild);
}


BOOL SetKeyAndValue(const char* szKey,
 const char* szSubkey,
 const char* szValue)

{
 HKEY hKey;
 char szKeyBuf[1024];

 strcpy_s(szKeyBuf, strlen(szKey) + 1, szKey);

 if (szSubkey != NULL)
 {
  strcat_s(szKeyBuf, _countof(szKeyBuf), "\\");
  strcat_s(szKeyBuf, _countof(szKeyBuf), szSubkey);
 }

 size_t dsize = strlen(szKeyBuf) + 1;
 wchar_t* dest = new wchar_t[dsize];
 size_t i;
 mbstowcs_s(&i, dest, dsize, szKeyBuf, strlen(szKeyBuf));
 long lResult = RegCreateKeyEx(HKEY_CLASSES_ROOT,
  dest,
  0NULL, REG_OPTION_NON_VOLATILE,
  KEY_ALL_ACCESS, NULL,
  &hKey, NULL);
 delete[] dest;
 if (lResult != ERROR_SUCCESS)
 {
  return FALSE;
 }

 // Set the Value.
 if (szValue != NULL)
 {
  RegSetValueExA(hKey, NULL0, REG_SZ,
   (BYTE *)szValue,
   strlen(szValue) + 1);
 }

 RegCloseKey(hKey);
 return TRUE;
}

注意執行註冊需要有管理員許可權才能寫入登錄檔。

COM回撥介面的實現

在COM中要實現COM服務和客戶程式的雙向通訊就需要COM回撥介面。COM回撥介面和回撥函式類似但是COM不支援回撥函式所以我們只能使用回撥介面或者使用連線點。本文章主要講解回撥介面的實現。

首先我們定義回撥介面

 MIDL_INTERFACE("8A26B267-20B6-48BE-A6A1-7E36B2074DD3")
 ICallBack : public IUnknown
 {
 public:
  virtual HRESULT __stdcall SendMsgToClinet(long port) 0;
 };

這個回撥介面很簡單只是傳給客戶一個long值。

接著我們定義我們的COM物件介面

 MIDL_INTERFACE("7799FC42-7048-4FDD-8035-DC1872A9F95E")
 IComInterface : public IUnknown
 {
 public:
  virtual HRESULT  __stdcall Write(long size) 0// COM物件成員函式
  virtual HRESULT  __stdcall Read(long* port) 0;  // COM物件成員函式
  virtual HRESULT  __stdcall SetCallBack(ICallBack* pCallBack, long* callBackID) 0;
  virtual HRESULT  __stdcall ReleaseCallBack(long callBackID) 0//釋放ICallBack介面
  virtual HRESULT  __stdcall Test(void) 0//測試函式
 };

SetCallBack函式將ICallBack介面給COM元件並返回一個識別符號callBackID用於釋放ICallBack介面。

//IComInterface.h
#ifndef __IComInterface_h__
#define __IComInterface_h__
#include <Unknwn.h>
#include <OAIdl.h>

#include "rpc.h"
#include "rpcndr.h"

#define COM_DLL_EXPORTS
#ifdef COM_DLL_EXPORTS
#   define COM_DLL_API __declspec(dllexport)
#else
#   define COM_DLL_API __declspec(dllimport)
#endif


#pragma warning( disable: 4049 )

#ifndef __ICallBack_FWD_DEFINED__
#define __ICallBack_FWD_DEFINED__
typedef interface ICallBack ICallBack;
#endif

#ifndef __IComInterface_FWD_DEFINED__
#define __IComInterface_FWD_DEFINED__
typedef interface IComInterface IComInterface;
#endif

#ifdef __cplusplus
typedef class CComInterface CComInterface;
#else
typedef struct CComInterface CComInterface;
#endif

#ifdef __cplusplus
extern "C"{
#endif 
 // {A83B5F67-1530-4A1B-AB2F-89B99E653335}
 const GUID CLSID_COMTest = { 0xa83b5f67, 0x1530, 0x4a1b,
 { 0xab, 0x2f, 0x89, 0xb9, 0x9e, 0x65, 0x33, 0x35 } };

 // {7799FC42-7048-4FDD-8035-DC1872A9F95E}
 const GUID IID_IComInterface = { 0x7799fc42, 0x7048, 0x4fdd,
 { 0x80, 0x35, 0xdc, 0x18, 0x72, 0xa9, 0xf9, 0x5e } };

 // {8A26B267-20B6-48BE-A6A1-7E36B2074DD3}
 const GUID IID_CallBack = { 0x8a26b267, 0x20b6, 0x48be,
 { 0xa6, 0xa1, 0x7e, 0x36, 0xb2, 0x7, 0x4d, 0xd3 } };

 /// {9BBD854F-970A-4ABD-917E-FF736C8C86CB}
 const GUID IID_IComInterfaceLib = { 0x9bbd854f, 0x970a, 0x4abd,
 { 0x91, 0x7e, 0xff, 0x73, 0x6c, 0x8c, 0x86, 0xcb } };

 // {6779B5C9-3F74-47CA-8800-5DC8A7F9FD55}
 const GUID IID_IComInterfaceObject = { 0x6779b5c9, 0x3f74, 0x47ca,
 { 0x88, 0x0, 0x5d, 0xc8, 0xa7, 0xf9, 0xfd, 0x55 } };

 MIDL_INTERFACE("8A26B267-20B6-48BE-A6A1-7E36B2074DD3")
 ICallBack : public IUnknown
 {
 public:
  virtual HRESULT __stdcall SendMsgToClinet(long port) = 0;
 };

 MIDL_INTERFACE("7799FC42-7048-4FDD-8035-DC1872A9F95E")
 IComInterface : public IUnknown
 {
 public:
  virtual HRESULT  __stdcall Write(long size) = 0;
  virtual HRESULT  __stdcall Read(long* port) = 0;
  virtual HRESULT  __stdcall SetCallBack(ICallBack* pCallBack, long* callBackID) = 0;
  virtual HRESULT  __stdcall ReleaseCallBack(long callBackID) = 0;
  virtual HRESULT  __stdcall Test(void) = 0;
 };

 class DECLSPEC_UUID("6779B5C9-3F74-47CA-8800-5DC8A7F9FD55") CComInterface;

#ifdef __cplusplus
}
#endif

#endif
// CComInterface.h
#pragma once
#include "IComInterface.h"
#include <map>

class CComInterface : public IComInterface
{
public:
 static CComInterface* GetVirtualObject();
 ~CComInterface();

private:
 CComInterface();

public:
 // IUnknown member function
 virtual HRESULT __stdcall QueryInterface(const IID& iid, void **ppv);
 virtual ULONG __stdcall AddRef();
 virtual ULONG __stdcall Release();

 // IVirtualChannel member function
 virtual HRESULT  __stdcall Write(long size);
 virtual HRESULT  __stdcall Read(long* port);
 virtual HRESULT  __stdcall SetCallBack(ICallBack* pCallBack, long* callBackID);
 virtual HRESULT  __stdcall ReleaseCallBack(long callBackID);
 virtual HRESULT  __stdcall Test(void);

private:
 static CComInterface* m_obj;
 LONG m_ref{ 0 }; // 元件引用計數
 std::map<int, ICallBack *> m_pCalls;
};

//CComInterface.cpp
#include "CComInterface.h"
#include "ComFactory.h"
#include <stdio.h>

LONG    g_lockNumber = 0;
LONG    g_comTestNumber = 0;
DWORD g_dwMainThreadID = 0;
LONG    g_ser = 0;

CComInterface* CComInterface::m_obj = NULL;
CComInterface* CComInterface::GetVirtualObject()
{
 if (m_obj == NULL)
 {
  m_obj = new CComInterface();
 }
 return m_obj;
}

CComInterface::~CComInterface()
{
 if (CCOMFactory::CanUnloadNow())
 {
  // TODO: 結束COM
 }
}


CComInterface::CComInterface()
{
 InterlockedIncrement(&g_comTestNumber);
}


HRESULT CComInterface::QueryInterface(const IID& iid, void **ppv)
{
 if (iid == IID_IUnknown)
 {
  *ppv = (IComInterface *) this;
  ((IComInterface *)(*ppv))->AddRef();
 }
 else if (iid == IID_IComInterface)
 {
  *ppv = (IComInterface *) this;
  ((IComInterface *)(*ppv))->AddRef();
 }
 else
 {
  *ppv = NULL;
  return E_NOINTERFACE;
 }

 return S_OK;
}

ULONG CComInterface::AddRef()
{
 return  InterlockedIncrement(&m_ref);
}

ULONG CComInterface::Release()
{
 if (InterlockedDecrement(&m_ref) == 0) {
  InterlockedDecrement(&g_comTestNumber);
  delete this;
  return 0;
 }
 return  (ULONG)m_ref;
}

HRESULT CComInterface::Write(long size)
{
 printf("Write %d", size);
 return S_OK;
}

HRESULT CComInterface::Read(long* port)
{
 *port = 10;
 printf("Read %d", *port);
 return S_OK;
}

HRESULT CComInterface::SetCallBack(ICallBack* pCallBack, long* callBackID)
{
 printf("pCallBack\n");
 if (pCallBack == NULL)
 {
  return S_FALSE;
 }

 *callBackID = g_ser;
 m_pCalls[g_ser] = pCallBack;
 g_ser++;
 ULONG ref = pCallBack->AddRef();
 return S_OK;
}

HRESULT CComInterface::ReleaseCallBack(long callBackID)
{
 printf("ReleaseCallBack\n");
 auto& it = m_pCalls.find(callBackID);
 if (it != m_pCalls.end())
 {
  ICallBack* back = it->second;
  back->Release();
  back = NULL;
  m_pCalls.erase(it);
  return S_OK;
 }
 return S_FALSE;
}

HRESULT CComInterface::Test(void)
{
 printf("Test\n");
 for (auto&it : m_pCalls)
 {
  it.second->SendMsgToClinet(3);
 }
 return S_OK;
}

在CComInterface.h 檔案定義m_pCalls map字典用於存放客戶註冊的回撥介面,當呼叫SetCallBack函式時將ICallBack介面放入m_pCalls,當有事件觸發時依次呼叫介面。如Test函式做的一樣。

至此整個程式外元件已經實現完畢。

代理的實現

idl檔案編寫

import "unknwn.idl";
import "oaidl.idl";
import "ocidl.idl";

[
 object,
 uuid(8A26B267-20B6-48BE-A6A1-7E36B2074DD3),
 helpstring("ICallBack 介面"),
 pointer_default(unique)
]
interface ICallBack : IUnknown
{
 [helpstring("方法SendMsgToClinet")]
 HRESULT SendMsgToClinet([in] long port) = 0;
};

[
 object,
 uuid(7799FC42-7048-4FDD-8035-DC1872A9F95E),
 helpstring("IComInterface 介面"),
 pointer_default(unique)
]
interface IComInterface : IUnknown
{
 [helpstring("方法Write")]
 HRESULT  Write( [in] long size) = 0;

 [helpstring("方法Read")]
 HRESULT  Read([out] long* port) = 0;

 [helpstring("方法SetCallBack")]
 HRESULT  SetCallBack([in]ICallBack* pCallBack, [out]long* callBackID) = 0;

 [helpstring("方法ReleaseCallBack")]
 HRESULT  ReleaseCallBack([in]long callBackID) = 0;

 [helpstring("方法Test")]
 HRESULT  Test(void) = 0;
};

[
 uuid(9BBD854F-970A-4ABD-917E-FF736C8C86CB),
 version(1.0),
 helpstring("IComInterfaceLib 1.0 型別庫")
]
library IComInterfaceLib
{
 importlib("stdole2.tlb");
 [
  uuid(6779B5C9-3F74-47CA-8800-5DC8A7F9FD55),
  helpstring("IComInterfaceLib Class")
 ]
 coclass CComInterface
 {
  [default] interface IComInterface;
 };
};

LIBRARY "COMProxy"

EXPORTS
 DllRegisterServer PRIVATE 
 DllUnregisterServer  PRIVATE
 DllGetClassObject      PRIVATE
 DllCanUnloadNow       PRIVATE
 GetProxyDllInfo        PRIVATE

新建一個DLL專案並新增idl和def檔案後開始編譯,這時會報錯。不用擔心我們需要對專案的屬性做些設定。

  • 預處理修改

如圖在預處理中新增DICTPRXY_EXPORTS,REGISTER_PROXY_DLL,點選確定

  • 附加依賴項 如圖:附加依賴項中新增rpcrt4.lib,Rpcns4.lib,點選確定

再次編譯專案發行成功生成DLL。

使用regSvr32 COMProxy註冊到登錄檔。

客戶端程式編寫

  • 回撥介面類實現
//Sink.h
//回撥例項類
class CSink :public ICallBack
{
protected:
 ULONG m_cRef;
public:
 CSink(void);
 ~CSink(void);
 STDMETHOD(QueryInterface)(const struct _GUID &iid,void ** ppv);
 ULONG __stdcall AddRef(void);
 ULONG __stdcall Release(void);
 
 HRESULT __stdcall SendMsgToClinet(long port);
};

//Sink.cpp
#include "sink.h"
#include <stdio.h>
#include <stdint.h>

CSink::CSink(void) :m_cRef(0)
{
}

CSink::~CSink(void)
{
}

STDMETHODIMP CSink::QueryInterface(const struct _GUID &iid, void ** ppv)
{
 *ppv = NULL;

 if (IID_IUnknown == iid || IID_CallBack == iid)
 {
  *ppv = this;
 }
 if (NULL != *ppv)
 {
  ((LPUNKNOWN)*ppv)->AddRef();
  return  S_OK;
 }

 return ResultFromScode(E_NOINTERFACE);
}

ULONG __stdcall CSink::AddRef()
{
 return InterlockedIncrement(&m_cRef);;
}

ULONG __stdcall CSink::Release()
{
 if (InterlockedDecrement(&m_cRef) == 0)
 {
  delete this;
  return 0;
 }

 return  (ULONG)m_cRef;
}


HRESULT __stdcall CSink::SendMsgToClinet(long port)
{
 printf("wwwww port=%d\n", port);
 return S_OK;
}

CSink 繼承了ICallBack介面實現ICallBack介面。

  • 客戶程式呼叫COM物件
#include <comutil.h>
#include <stdio.h>
#include <OAIdl.h>
#include "..\COMOut\IComInterface.h"
#include "Sink.h"
int main()
{
 IUnknown *pUnknown = NULL;
 IComInterface *pIComInterface = NULL;
 HRESULT hResult;
 if (CoInitialize(NULL) != S_OK)
 {
  printf("Initialize COM library failed!\n");
  Sleep(3000);
  CoUninitialize();
  return -1;
 }

 hResult = CoCreateInstance(CLSID_COMTest, NULL,
  CLSCTX_LOCAL_SERVER, IID_IUnknown, (void **)&pUnknown);
 if (FAILED(hResult))
 {
  printf("Create object failed!\n");
  Sleep(3000);
  CoUninitialize();
  return -2;
 }

 hResult = pUnknown->QueryInterface(IID_IComInterface, (void **)&pIComInterface);
 if (FAILED(hResult))
 {
  pUnknown->Release();
  printf("QueryInterface IID_IComInterface failed hResult=%d erro=%d!\n", hResult, GetLastError());
  Sleep(3000);
  CoUninitialize();
  return -3;
 }

 CSink *m_pSinkCallBack = new CSink();
 m_pSinkCallBack->AddRef();
 long backID;
 pIComInterface->SetCallBack(m_pSinkCallBack, &backID);
 while (true)
 {
  pIComInterface->Test();
  Sleep(1000);
 }

 pIComInterface->Release();
 CoUninitialize();
 return 0;
}

至此COM回撥介面已經完全實現了。

試驗結果

專案目錄結構:ComClient為客戶程式、COMProxy為存根DLL、COMOut為程式外元件。

啟動客戶程式ComClient.exe,COMOut.exe隨之啟動並設定了回撥介面和執行了回撥函式。

結束語

此為手動實現COM各種細節,可以方便於改造舊程式。對於新開發的程式如果要使用COM程式外元件建議讀者可利用Micsoft的框架來實現如ATL等。最後關注公眾號“智子開天科技“ 回覆“COM程式外元件”可獲得原始碼哦。

長按圖片關注我哦,更多windows程式設計精彩內容~~~~
長按圖片關注我哦,更多windows程式設計精彩內容~~~~
- END -

相關文章