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

深入解析COM組件模型核心基礎:從DLL到IUnknown的完整演進

     編輯:關於VC++

COM(Component Object Model)是Windows平台上最核心的元件技術,DirectX、ADO、OLE、Windows Shell擴展等無數系統級功能皆建構於此。本文將從動態連結庫(DLL)切入,層層深入解析COM的二進制標準、介面設計、GUID識別機制、參考計數與IUnknown三大核心方法,並以C++代碼示範完整組件實作邏輯。

第一部分:動態連結庫——COM的載體基石

DLL本質與兩種連結方式

DLL是包含共享函數、資源的可執行檔,讓多行程共享同一份記憶體副本。連結方式分兩種:

隱式連結(靜態連結)

  • 編譯期透過 .lib 導入庫解析符號
  • 程式啟動時系統自動載入DLL、映射至進程空間
  • 呼叫介面如同普通函數,無需顯式API
  • 專案設定:Project→Settings→Link 加入 .lib,或用 #pragma comment(lib, "xxx.lib")

顯式連結(動態連結)——COM採用方式

  • 執行期呼叫 LoadLibrary() 載入、FreeLibrary() 釋放
  • 透過 GetProcAddress() 取得函數位址,再以函數指標調用
  • 優勢:可依條件動態決定載入哪個DLL、支援插件架構、版本熱插拔

關鍵API與DllMain生命週期

// 載入DLL
HMODULE LoadLibrary(LPCTSTR lpFileName);

// 釋放DLL
BOOL FreeLibrary(HMODULE hModule);

// 取得函數位址
FARPROC GetProcAddress(HMODULE hModule, LPCSTR lpProcName);

// DLL進入點——每個DLL必須實作
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved);

DllMain 的四種觸發時機(fdwReason):

值觸發時機
DLL_PROCESS_ATTACHDLL被載入進程
DLL_THREAD_ATTACH進程建立新執行緒
DLL_THREAD_DETACH執行緒結束
DLL_PROCESS_DETACHDLL被卸載或進程結束

函數指標實戰範例

// 假設Math.dll導出 int Plus(int, int)
HMODULE hMod = LoadLibrary(_T("Math.dll"));
if (hMod) {
    // 定義函數指標型別
    typedef int (*PFN_PLUS)(int, int);
    PFN_PLUS pPlus = (PFN_PLUS)GetProcAddress(hMod, "Plus");
    
    if (pPlus) {
        int result = pPlus(10, 20); // 呼叫DLL函數
        printf("Result: %d\n", result);
    }
    FreeLibrary(hMod);
}

第二部分:COM組件物件模型——二進制標準的跨語言契約

為何需要COM?

90年代Windows開發語言百家爭鳴(C++、VB、Delphi、C#...),二進制不相容導致元件無法跨語言複用。微軟定義基於二進制的通用介面規範,解決:

  • 語言中立性:任何語言皆可實作/使用COM元件
  • 進程透明性:進程內/進程外/遠端呼叫統一介面
  • 封裝性:實作細節隱藏,僅暴露介面
  • 版本管理:介面不變,實作可獨立升級

核心概念:物件、介面、GUID

組件物件 vs C++物件對照

C++概念COM概念說明
classCoClass(組件類別)實作實體,含資料與方法
public虛函數Interface(介面)純虛類別,定義契約,無實作
繼承多重繼承介面一個CoClass實作多個介面
this指標介面指標指向vtable的指標

C++實作COM介面與物件範例

// 定義介面(純虛類別,繼承IUnknown)
class IPhysiology : public IUnknown {
public:
    virtual void Eat(Food*) = 0;
    virtual void Drink(Liquid*) = 0;
    virtual Waste* Toilet() = 0;
};

class IPsychics : public IUnknown {
public:
    virtual Sound Laugh() = 0;
    virtual Sound Cry() = 0;
};

class IDynamics : public IUnknown {
public:
    virtual Speed Run() = 0;
    virtual Speed Walk() = 0;
};

// 實作物件:多重繼承三個介面
class Human : public IPhysiology, public IPsychics, public IDynamics {
public:
    // IUnknown實作
    STDMETHOD(QueryInterface)(REFIID riid, void** ppv) override;
    STDMETHOD_(ULONG, AddRef)() override;
    STDMETHOD_(ULONG, Release)() override;
    
    // 介面方法實作...
    void Eat(Food* f) override { printf("Delicious!\n"); }
    Sound Laugh() override { return Sound("Haha"); }
    Speed Run() override { return Speed(20); }
    
private:
    ULONG m_ref = 0;
};

GUID(Globally Unique Identifier)——128位唯一標識

typedef struct _GUID {
    DWORD Data1;
    WORD  Data2;
    WORD  Data3;
    BYTE  Data4[8];
} GUID;

// C++定義方式
const GUID CLSID_Human = 
{0xABFA7022, 0x7E2F, 0x4D0E, {0x8A,0x4F,0xF5,0x8B,0xBC,0xEB,0xB2,0xDA}};

// 註冊表/IDL字串形式
// {ABFA7022-7E2F-4D0E-8A4F-F58BBCEBB2DA}
  • CLSID_ 前綴:組件類別識別碼
  • IID_ 前綴:介面識別碼
  • 衝突機率極低(2^128空間),可用 guidgen.exe 或VS工具產生

IUnknown——所有介面的根基

class IUnknown {
public:
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) = 0;
    virtual ULONG STDMETHODCALLTYPE AddRef() = 0;
    virtual ULONG STDMETHODCALLTYPE Release() = 0;
};

三大核心方法詳解

  1. QueryInterface:導航介面圖
    • 輸入:想要的介面IID、輸出指標雙重指標
    • 成功回傳 S_OK,*ppv 指向該介面
    • 失敗回傳 E_NOINTERFACE,*ppv = NULL
  2. AddRef:增加參考計數,回傳新值
  3. Release:減少參考計數,歸零時 delete this 自毀

參考計數機制——解決共享物件生命週期

多客戶端共享同一物件時,傳統 delete 會導致懸空指標崩潰。COM規定:

  • 取得介面指標 → 必須 AddRef()
  • QueryInterface 成功內部自動 AddRef()
  • 不再使用 → 必須 Release()
  • 計數歸零 → 物件自我銷毀

完整IUnknown實作範例

HRESULT Human::QueryInterface(REFIID riid, void** ppv) {
    if (riid == IID_IUnknown || riid == IID_IPhysiology) {
        *ppv = static_cast(this);
    } else if (riid == IID_IPsychics) {
        *ppv = static_cast(this);
    } else if (riid == IID_IDynamics) {
        *ppv = static_cast(this);
    } else {
        *ppv = NULL;
        return E_NOINTERFACE;
    }
    AddRef(); // 關鍵:成功時必須AddRef
    return S_OK;
}

ULONG Human::AddRef() {
    return InterlockedIncrement(&m_ref); // 執行緒安全
}

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

第三部分:COM介面規範五大原則

微軟強制要求所有COM介面實作遵循:

  1. IUnknown等價性:同一物件的 QueryInterface(IID_IUnknown) 必須回傳相同指標值,可用於判斷「是否同一物件」
  2. 自反性:pA->QueryInterface(IID_IA, &pB) 必須成功
  3. 對稱性:若 pA->QI(IB, &pB) 成功,則 pB->QI(IA, &pA) 必成功
  4. 傳遞性:A→B、B→C 成功 ⇒ C→A 必成功
  5. 時間無關性:任意時刻查詢同一介面結果一致

IDL描述與類型庫

介面定義語言(IDL)用於跨語言描述COM類型,經 midl.exe 編譯產生 .tlb 類型庫:

[
    object, uuid(6AAF876E-FCED-4ee0-B5D3-63CD6E2242F5),
    pointer_default(unique)
]
interface IPhysiology : IUnknown {
    void Eat([in] Food* in);
    void Drink([in] Liquid* in);
    Waste* Toilet();
};

[
    uuid(ABFA7022-7E2F-4D0E-8A4F-F58BBCEBB2DA),
    version(1.0)
]
library HumanLib {
    importlib("stdole2.tlb");
    [
        uuid(62FD0E39-DA84-4B19-BAB0-960A27AC2B71)
    ]
    coclass Human {
        [default] interface IPhysiology;
        interface IPsychics;
        interface IDynamics;
    };
}

COM物件結構圖

COM物件結構示意圖:CoClass實作多介面,透過IUnknown導航

總結與下篇預告

本文完整建立了:DLL載入機制 → COM二進制標準 → 介面/物件/GUID/IUnknown → 參考計數 → 介面規範 → IDL描述 的知識鏈。下一篇將實戰演示:

  • ATL專案建立與COM伺服器實作(In-proc DLL / Local EXE)
  • 註冊表註冊、類別工廠、DllGetClassObject 導出函數
  • 客戶端綁定(早期/晚期)、CoCreateInstance 內部流程
  • 免註冊COM、Side-by-Side組件、Registration-Free Activation

作者:釋雪 | MSN: Blue_Atlantis400@hotmail.com | QQ: 63068279

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