真正理解 COM(Component Object Model)的最佳途徑,是**不依賴 ATL/MFC 宏與模板**,純手工用 C++ 實作一個完整的進程內 COM 伺服器。本教學將帶領你從介面定義開始,一步步建構一個「超快加法」COM 物件,並支援 VB 與 VC++ 客戶端呼叫。
建立 AddObj 專案(Win32 DLL),新增 IAdd.idl:
import "unknwn.idl";
[
object,
uuid(1221db62-f3d8-11d4-825d-00104b3646c0),
helpstring("介面 IAdd:實作超快加法演算法")
]
interface IAdd : IUnknown
{
HRESULT SetFirstNumber([in] long nX1);
HRESULT SetSecondNumber([in] long nX2);
HRESULT DoTheAddition([out, retval] long *pBuffer);
};
[
uuid(3ff1aab8-f3d8-11d4-825d-00104b3646c0),
helpstring("CodeGuru 數學演算法介面庫")
]
library CodeGuruMathLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
interface IAdd;
}
使用 uuidgen.exe 產生 GUID。介面繼承 IUnknown,定義三個方法:設定兩個被加數並回傳和。
執行 midl.exe IAdd.idl 產生:
IAdd.h:C++ 介面宣告(純虛擬類別)。IAdd_i.c:介面 IID 定義(IID_IAdd、CLSID_AddObject)。IAdd.tlb:二進位類型庫,供 VB/VC++ 客戶端匯入。dlldata.c、IAdd_p.c:Proxy/Stub 程式碼(跨進程/機器調用需用)。CAddObj#include "IAdd.h"
extern long g_nComObjsInUse;
class CAddObj : public IAdd
{
public:
// IUnknown
STDMETHOD(QueryInterface)(REFIID riid, void **ppObj);
STDMETHOD_(ULONG, AddRef)();
STDMETHOD_(ULONG, Release)();
// IAdd
STDMETHOD(SetFirstNumber)(long nX1);
STDMETHOD(SetSecondNumber)(long nX2);
STDMETHOD(DoTheAddition)(long *pBuffer);
private:
long m_nX1, m_nX2;
long m_nRefCount;
};
#include <objbase.h>
#include "AddObj.h"
#include "IAdd_i.c"
long g_nComObjsInUse = 0;
// IAdd 方法
STDMETHODIMP CAddObj::SetFirstNumber(long nX1) { m_nX1 = nX1; return S_OK; }
STDMETHODIMP CAddObj::SetSecondNumber(long nX2) { m_nX2 = nX2; return S_OK; }
STDMETHODIMP CAddObj::DoTheAddition(long *pBuffer) { *pBuffer = m_nX1 + m_nX2; return S_OK; }
// IUnknown 方法(執行緒安全參考計數)
STDMETHODIMP CAddObj::QueryInterface(REFIID riid, void **ppObj)
{
if (riid == IID_IUnknown || riid == IID_IAdd) {
*ppObj = static_cast<IAdd*>(this);
AddRef();
return S_OK;
}
*ppObj = NULL;
return E_NOINTERFACE;
}
STDMETHODIMP_(ULONG) CAddObj::AddRef() { return InterlockedIncrement(&m_nRefCount); }
STDMETHODIMP_(ULONG) CAddObj::Release()
{
long nRef = InterlockedDecrement(&m_nRefCount);
if (nRef == 0) delete this;
return nRef;
}
// 建構/解構維護全域物件計數
CAddObj::CAddObj() : m_nX1(0), m_nX2(0), m_nRefCount(0) { InterlockedIncrement(&g_nComObjsInUse); }
CAddObj::~CAddObj() { InterlockedDecrement(&g_nComObjsInUse); }
CAddFactory每個 COM DLL 必須提供 IClassFactory 實作,供 SCM(Service Control Manager)建立物件實例。
// AddObjFactory.h
class CAddFactory : public IClassFactory
{
public:
STDMETHOD(QueryInterface)(REFIID, void**);
STDMETHOD_(ULONG, AddRef)();
STDMETHOD_(ULONG, Release)();
STDMETHOD(CreateInstance)(IUnknown*, REFIID, void**);
STDMETHOD(LockServer)(BOOL);
private:
long m_nRefCount;
};
// AddObjFactory.cpp 關鍵實作
STDMETHODIMP CAddFactory::CreateInstance(IUnknown* pUnkOuter, REFIID iid, void** ppv)
{
if (pUnkOuter) return CLASS_E_NOAGGREGATION; // 不支援聚合
CAddObj* pObj = new CAddObj;
if (!pObj) return E_OUTOFMEMORY;
return pObj->QueryInterface(iid, ppv);
}
STDMETHODIMP CAddFactory::LockServer(BOOL) { return E_NOTIMPL; }
// IUnknown 實作同 CAddObj,操作 m_nRefCount
建立 Exports.cpp 實作四個標準匯出函式:
STDAPI DllGetClassObject(REFCLSID clsid, REFIID iid, void** ppv)
{
if (clsid != CLSID_AddObject) return CLASS_E_CLASSNOTAVAILABLE;
CAddFactory* pFact = new CAddFactory;
if (!pFact) return E_OUTOFMEMORY;
return pFact->QueryInterface(iid, ppv);
}
STDAPI DllCanUnloadNow()
{
return (g_nComObjsInUse == 0) ? S_OK : S_FALSE;
}
// DllRegisterServer / DllUnregisterServer 寫入/移除登錄機碼
// 詳細實作見原文 Registry.cpp
建立 Exports.def 模組定義檔指定匯出:
EXPORTS
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
IAdd.idl 加入專案,設定自訂建置步驟:命令列 midl $(InputPath),輸出檔案包含 IAdd.h;IAdd_i.c;IAdd.tlb 等。regsvr32 /s "$(TargetPath)",每次建置自動註冊。Dim iAdd As CodeGuruMathLib.IAdd
Set iAdd = CreateObject("CodeGuru.FastAddition")
iAdd.SetFirstNumber 100
iAdd.SetSecondNumber 200
MsgBox "total = " & iAdd.DoTheAddition()
專案需加入 IAdd.tlb 參考。
#import "AddObj.dll"(含內嵌 TLB)→ 智慧指標 IAddPtr 自動管理生命週期。IAdd.h,手動 CoCreateInstance + QueryInterface。// #import 範例
#import "AddObj.dll"
void main()
{
CoInitialize(NULL);
CodeGuruMathLib::IAddPtr pAdd;
pAdd.CreateInstance(L"CodeGuru.FastAddition");
pAdd->SetFirstNumber(100);
pAdd->SetSecondNumber(200);
long result = pAdd->DoTheAddition();
printf("Result: %d\n", result);
CoUninitialize();
}
資源視圖 → Import 選擇 IAdd.tlb,資源類型 TYPELIB,ID 為 1。此後僅需散佈 AddObj.dll 單一檔案。
本教學完整展示了 COM 物件從 IDL 定義、C++ 實作、類別工廠、DLL 匯出、登錄註冊到客戶端呼叫的全生命週期。理解這些基礎機制,對除錯 ATL/MFC 專案、處理跨語言互通、效能調優都極有幫助。