ifoo.h:介面定義(IFoo, IFooVtbl, IID_IFoo)。outside.c:COutside 結構體實作 IFoo。util.h:巨集、全域函數宣告。main.c:main()、記憶體管理 Alloc/Free、IID 定義。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 *);
};
IFoo 實例記憶體佈局:首欄位指向唯讀虛函數表 IFooVtbl。C++ 編譯器隱含 this 指標;C 語言需显式在每個函數首參數傳遞 This。
QueryInterface, AddRef, Release(繼承 IUnknown)。QueryInterface(IID_IUnknown) 回傳指標必須恆定,作為物件身分比較依據。AddRef 增、Release 減,歸零時銷毀物件。#define IUNK_VTABLE_OF(x) ((IUnknownVtbl *)((x)->lpVtbl))
#define QueryInterface(pif, iid, ppv) \
(IUNK_VTABLE_OF(pif)->QueryInterface((IUnknown*)(pif), iid, (void**)(ppv)))
#define AddRef(pif) (IUNK_VTABLE_OF(pif)->AddRef((IUnknown*)(pif)))
#define Release(pif) (IUNK_VTABLE_OF(pif)->Release((IUnknown*)(pif)))
為何要轉型為 IUnknownVtbl*?
雖然 IFooVtbl 前三成員與 IUnknownVtbl 記憶體佈局相同,但 C 型別系統要求指標型別匹配。巨集執行:
1. 取 lpVtbl(IFooVtbl*)。
2. 強轉為 IUnknownVtbl*(編譯器僅修改型別資訊,位址值不變)。
3. 呼叫對應函數,首參數同樣強轉為 IUnknown*。
實際執行時仍呼叫 COutside 實作的 FooQueryInterface 等函數,參數值(物件位址)完全一致。
typedef struct COutside {
IFoo ifoo; // 嵌入介面,必須為第一個成員
int refCount;
int value;
} COutside;
// QueryInterface 實作
HRESULT __stdcall FooQueryInterface(IFoo *This, const IID *riid, void **ppv) {
COutside *this = (COutside *)((char *)This - offsetof(COutside, ifoo));
// 或利用 (size_t)&(((COutside*)0)->ifoo) 計算偏移
if (IsEqualIID(riid, &IID_IUnknown) || IsEqualIID(riid, &IID_IFoo)) {
*ppv = &this->ifoo;
this->refCount++;
return S_OK;
}
*ppv = NULL;
return E_NOINTERFACE;
}
關鍵技巧:offsetof 或 (size_t)&(((COutside*)0)->ifoo) 編譯期計算成員偏移,利用「位址 0 合法取偏移」特性(不解參考不觸發 GP),從介面指標反推物件基址。
/Gz(預設 __stdcall)統一介面函數慣例。/TC(強制 C 模式),避免 C++ 名稱修飾干擾。.i 檔可驗證巨集展開與虛函數表結構。HRESULT (__stdcall *pQI)(IFoo*, const IID*, void**); // 變數宣告
typedef HRESULT (__stdcall *QIType)(IFoo*, const IID*, void**); // 型別別名
pQI = QueryInterface; // 賦值
pQI(this, &iid, &ptr); // 呼叫
(*pQI)(this, &iid, &ptr); // 等價寫法
IUNK_VTABLE_OF 展開後再展開 QueryInterface)。純 C 實作 COM 揭示了物件模型的底層機制:虛函數表 + 显式 This 指標 + 參考計數 + 偏移計算。理解這層機制,不僅能在無 C++ 環境下實作二進位相容介面,更能深入除錯 C++ 物件佈局、多重繼承指標調整、COM 聚合等進階主題。