上一篇介紹了 COM 客戶端程式設計。本文將深入 COM 服務器內部,解釋如何用 C++ 編寫自訂介面與服務器,並詳細剖析 CoCreateInstance() 呼叫時服務器端的運行機制。假設讀者精通 C++ 並掌握前文概念。
進程內服務器(In-process Server,DLL 形式)必須滿足:
HKEY_CLASSES_ROOT\CLSID 正確註冊。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() 內部流程CoCreateInstance() 傳遞 CLSID 與 IID。HKEY_CLASSES_ROOT\CLSID 查找服務器資訊。DllGetClassObject() 取得類工廠。CreateInstance() 建立 COM 對象。以 GUID 為鍵名(含大括號與連字號),預設值為可讀類別名。進程內服務器需建立 InProcServer32 子鍵,包含:
ThreadingModel:Apartment(單執行緒公寓模型)類工廠(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 運算子使用。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 專案,選單提供兩個測試命令:
DoSimpleMsgBox()、釋放介面。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();
}
介面宣告常用宏:
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" |
InProcServer32 | Default = [DLL 路徑]; ThreadingModel = "Apartment" |
SimpleComSvr.dsw) 包含服務器與客戶端兩個專案。ISimpleMsgBox.h(介面定義)、SimpleMsgBoxComDef.h(GUID 宣告,分離使客戶端不依賴實作細節)。

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