首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> 關於VC++

COM 程式設計進階:深入 COM 服務器內部機制與自訂介面實作

     編輯:關於VC++

前言:從客戶端轉向服務器端

上一篇介紹了 COM 客戶端程式設計。本文將深入 COM 服務器內部,解釋如何用 C++ 編寫自訂介面與服務器,並詳細剖析 CoCreateInstance() 呼叫時服務器端的運行機制。假設讀者精通 C++ 並掌握前文概念。

最簡進程內服務器的兩大必要條件

進程內服務器(In-process Server,DLL 形式)必須滿足:

  1. 在 HKEY_CLASSES_ROOT\CLSID 正確註冊。
  2. 匯出 DllGetClassObject() 函數。

註冊表需以服務器 GUID 為鍵名,包含 DLL 完整路徑與執行緒模型(本文採單執行緒 Apartment 模型)。

另外三個常見匯出函數:

  • DllCanUnloadNow():供 COM 庫查詢是否可卸載。
  • DllRegisterServer() / DllUnregisterServer():安裝/移除時自動註冊/反註冊。

服務器生命週期管理:DllCanUnloadNow()

標準 DLL 被動載入/卸載,但 COM 庫提供主動機制。當客戶端呼叫 CoFreeUnusedLibraries()(通常在閒置處理時),COM 庫遍歷已載入 DLL 服務器並呼叫 DllCanUnloadNow()。服務器透過全域引用計數決定是否可卸載:

extern UINT g_uDllRefCount; // 服務器引用計數
HRESULT DllCanUnloadNow() {
  return (g_uDllRefCount > 0) ? S_FALSE : S_OK;
}

引用計數管理細節見後續程式碼。

實作介面:從 IUnknown 開始

每個 Coclass 需自行實作 IUnknown。以 CUnknownImpl 為例:

class CUnknownImpl : public IUnknown {
public:
  CUnknownImpl(); virtual ~CUnknownImpl();
  ULONG AddRef(); ULONG Release();
  HRESULT QueryInterface(REFIID riid, void** ppv);
protected:
  UINT m_uRefCount; // 對象引用計數
};

建構子/解構子:管理服務器與對象引用計數


CUnknownImpl::CUnknownImpl() { m_uRefCount = 0; g_uDllRefCount++; }
CUnknownImpl::~CUnknownImpl() { g_uDllRefCount--; }

AddRef() / Release():控制對象生命週期


ULONG CUnknownImpl::AddRef() { return ++m_uRefCount; }
ULONG CUnknownImpl::Release() {
  ULONG uRet = --m_uRefCount;
  if (0 == m_uRefCount) delete this; // 假設對象在堆上建立
  return uRet;
}

正確呼叫 AddRef/Release 至關重要,否則對象過早銷毀或內存洩漏。單執行緒服務器下 ++/-- 足夠,因 COM 庫序列化方法呼叫。

QueryInterface():介面查詢核心邏輯


HRESULT CUnknownImpl::QueryInterface(REFIID riid, void** ppv) {
  HRESULT hrRet = S_OK;
  *ppv = NULL; // 標準初始化
  if (IsEqualIID(riid, IID_IUnknown)) {
    *ppv = (IUnknown*)this;
  } else {
    hrRet = E_NOINTERFACE;
  }
  if (S_OK == hrRet) ((IUnknown*)*ppv)->AddRef(); // 關鍵:新介面指標需增計數
  return hrRet;
}

三步驟:初始化輸出指標為 NULL、檢查請求 IID、成功時 AddRef()。強制轉型為 IUnknown* 是良好習慣。

深入 CoCreateInstance() 內部流程

  1. 客戶端呼叫 CoCreateInstance() 傳遞 CLSID 與 IID。
  2. COM 庫在註冊表 HKEY_CLASSES_ROOT\CLSID 查找服務器資訊。
  3. 讀取 DLL 路徑並載入客戶端進程空間。
  4. 呼叫服務器 DllGetClassObject() 取得類工廠。
  5. 服務器建立類工廠並返回。
  6. COM 庫呼叫類工廠 CreateInstance() 建立 COM 對象。
  7. 返回介面指標給客戶端。

COM 服務器註冊表結構

以 GUID 為鍵名(含大括號與連字號),預設值為可讀類別名。進程內服務器需建立 InProcServer32 子鍵,包含:

  • 預設值:DLL 完整路徑
  • ThreadingModel:Apartment(單執行緒公寓模型)

類工廠:COM 對象的製造者

類工廠(Class Factory)本身是實作 IClassFactory 的 Coclass,唯一職責是建立 COM 對象。COM 庫透過 DllGetClassObject() 取得類工廠,再呼叫其 CreateInstance()。


struct IClassFactory : public IUnknown {
  HRESULT CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppvObject);
  HRESULT LockServer(BOOL fLock);
};
  • CreateInstance():建立 COM 對象實例。
  • LockServer():強制增加/減少服務器引用計數,防止過早卸載。

完整範例:自訂介面 ISimpleMsgBox 服務器

介面定義


struct ISimpleMsgBox : public IUnknown {
  // IUnknown 方法
  STDMETHOD_(ULONG, AddRef)() PURE;
  STDMETHOD_(ULONG, Release)() PURE;
  STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
  // 自訂方法
  STDMETHOD(DoSimpleMsgBox)(HWND hwndParent, BSTR bsMessageText) PURE;
};
struct __declspec(uuid("{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}")) ISimpleMsgBox;
  • 所有方法回傳 HRESULT,輸出參數用指標。
  • BSTR 為 COM 標準字串(長度前綴),供 VB/腳本客戶端使用。
  • __declspec(uuid) 指定 GUID,配合 __uuidof 運算子使用。

Coclass 實作:CSimpleMsgBoxImpl


class CSimpleMsgBoxImpl : public ISimpleMsgBox {
public:
  CSimpleMsgBoxImpl(); virtual ~CSimpleMsgBoxImpl();
  // IUnknown
  ULONG AddRef(); ULONG Release();
  HRESULT QueryInterface(REFIID riid, void** ppv);
  // ISimpleMsgBox
  HRESULT DoSimpleMsgBox(HWND hwndParent, BSTR bsMessageText);
protected:
  ULONG m_uRefCount;
};
class __declspec(uuid("{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}")) CSimpleMsgBoxImpl;

客戶端建立方式:


ISimpleMsgBox* pIMsgBox;
HRESULT hr = CoCreateInstance(__uuidof(CSimpleMsgBoxImpl), NULL,
              CLSCTX_INPROC_SERVER, __uuidof(ISimpleMsgBox), (void**)&pIMsgBox);

類工廠實作:CSimpleMsgBoxClassFactory


class CSimpleMsgBoxClassFactory : public IClassFactory {
public:
  // IUnknown 方法...
  // IClassFactory 方法
  HRESULT CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppv);
  HRESULT LockServer(BOOL fLock);
protected:
  ULONG m_uRefCount;
};

LockServer()


HRESULT CSimpleMsgBoxClassFactory::LockServer(BOOL fLock) {
  fLock ? g_uDllLockCount++ : g_uDllLockCount--;
  return S_OK;
}

CreateInstance() 核心邏輯


HRESULT CSimpleMsgBoxClassFactory::CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppv) {
  if (NULL != pUnkOuter) return CLASS_E_NOAGGREGATION; // 不支援聚合
  if (IsBadWritePtr(ppv, sizeof(void*))) return E_POINTER;
  *ppv = NULL;

  CSimpleMsgBoxImpl* pMsgbox = new CSimpleMsgBoxImpl;
  if (NULL == pMsgbox) return E_OUTOFMEMORY;

  HRESULT hrRet = pMsgbox->QueryInterface(riid, ppv); // 透過 QI 取得請求介面
  if (FAILED(hrRet)) delete pMsgbox; // QI 失敗則刪除對象
  return hrRet;
}

DllGetClassObject() 實作


HRESULT DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv) {
  if (!InlineIsEqualGUID(rclsid, __uuidof(CSimpleMsgBoxImpl)))
    return CLASS_E_CLASSNOTAVAILABLE;
  if (IsBadWritePtr(ppv, sizeof(void*))) return E_POINTER;
  *ppv = NULL;

  CSimpleMsgBoxClassFactory* pFactory = new CSimpleMsgBoxClassFactory;
  if (NULL == pFactory) return E_OUTOFMEMORY;

  pFactory->AddRef(); // 先增計數 (COUNT=1)
  HRESULT hrRet = pFactory->QueryInterface(riid, ppv); // QI 成功再增 (COUNT=2)
  pFactory->Release(); // 用完釋放 (COUNT=1 或 0 觸發自我刪除)
  return hrRet;
}

此處採「先 AddRef() 再 QueryInterface()」模式,確保引用計數正確。

類工廠的 QueryInterface()


HRESULT CSimpleMsgBoxClassFactory::QueryInterface(REFIID riid, void** ppv) {
  HRESULT hrRet = S_OK;
  if (IsBadWritePtr(ppv, sizeof(void*))) return E_POINTER;
  *ppv = NULL;
  if (InlineIsEqualGUID(riid, IID_IUnknown))
    *ppv = (IUnknown*)this;
  else if (InlineIsEqualGUID(riid, IID_IClassFactory))
    *ppv = (IClassFactory*)this;
  else
    hrRet = E_NOINTERFACE;
  if (S_OK == hrRet) ((IUnknown*)*ppv)->AddRef();
  return hrRet;
}

實作多介面(IUnknown + IClassFactory)的典型範例。

ISimpleMsgBox::DoSimpleMsgBox() 實作


HRESULT CSimpleMsgBoxImpl::DoSimpleMsgBox(HWND hwndParent, BSTR bsMessageText) {
  _bstr_t bsMsg = bsMessageText; // ATL 智慧指標自動轉換
  LPCTSTR szMsg = (TCHAR*)bsMsg; // 轉 TCHAR (ANSI/Unicode 視專案設定)
  MessageBox(hwndParent, szMsg, _T("Simple Message Box"), MB_OK);
  return S_OK;
}

客戶端測試程式

基於 Win32 Hello World 專案,選單提供兩個測試命令:

  1. Test MsgBox COM Server:建立對象、呼叫 DoSimpleMsgBox()、釋放介面。
  2. Call CoFreeUnusedLibraries:觸發服務器 DllCanUnloadNow() 測試卸載。

測試程式選單截圖


void DoMsgBoxTest(HWND hMainWnd) {
  ISimpleMsgBox* pIMsgBox;
  HRESULT hr = CoCreateInstance(__uuidof(CSimpleMsgBoxImpl), NULL,
                  CLSCTX_INPROC_SERVER, __uuidof(ISimpleMsgBox), (void**)&pIMsgBox);
  if (FAILED(hr)) return;
  pIMsgBox->DoSimpleMsgBox(hMainWnd, _bstr_t("Hello COM!"));
  pIMsgBox->Release();
}

COM 宏與 .DEF 檔

介面宣告常用宏:

  • STDMETHOD():含 virtual、回傳型別、呼叫慣例。
  • STDMETHOD_():可指定不同回傳型別。
  • PURE:展開為 =0(純虛擬)。
  • 實作對應宏:STDMETHODIMP、STDMETHODIMP_()。
  • 匯出函數用 STDAPI(含回傳型別與呼叫慣例),不可與 __declspec(dllexport) 同用,需透過 .DEF 檔匯出。

.DEF 檔範例:


EXPORTS
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE

PRIVATE 關鍵字表示「匯出但不放入匯入庫」,防止客戶端直接呼叫。

服務器註冊/反註冊實作

DllRegisterServer() 建立以下註冊表項目:

鍵名鍵值
HKEY_CLASSES_ROOT\CLSID\{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}Default = "SimpleMsgBox class"
InProcServer32Default = [DLL 路徑]; ThreadingModel = "Apartment"

專案結構與偵錯技巧

  • 單一 Workspace (SimpleComSvr.dsw) 包含服務器與客戶端兩個專案。
  • 共用標頭檔:ISimpleMsgBox.h(介面定義)、SimpleMsgBoxComDef.h(GUID 宣告,分離使客戶端不依賴實作細節)。
  • 偵錯服務器 DLL:方法一:設服務器專案為啟動專案,偵錯時指定客戶端 EXE 路徑。方法二:設客戶端為啟動專案,加入專案相依性,並在客戶端專案設定 Additional DLLs 載入服務器符號。

專案相依性設定

Additional DLLs 設定

總結

本文完整展示了從零建立一個進程內 COM 服務器的全過程:註冊表結構、類工廠機制、IUnknown 實作細節、CoCreateInstance() 內部流程、自訂介面定義與實作、客戶端測試、宏簡化與偵錯設定。掌握這些核心機制,即可開發健壯、可擴展的 COM 組件。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved