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

C 語言實現 COM 介面 IFoo(二):類別工廠與模組分離

     編輯:關於C語言

前言:從耦合到解耦

上一篇實作的 IFoo 介面與客戶端緊耦合,未分離實作模組,違背模組化原則。本篇引入類別工廠,實現介面實作與使用的完全分離。

類別工廠的核心價值

雖然 CoCreateInstance 讓客戶端無需感知類別工廠即可建立物件,但類別工廠提供兩大關鍵能力:

  1. 間接建立物件,降低重複開銷:客戶端可持有類別工廠介面指標,反覆呼叫 CreateInstance 建立多個物件,避免每次定位並載入 DLL 的開銷。
  2. 鎖定 DLL 留駐記憶體:透過 LockServer 維護 DLL 級計數器。計數器非零時 DLL 不被卸載。這與物件級的參考計數(AddRef/Release 控制物件生命週期)形成互補:LockServer 一次 ≈ 建立一個物件對 DLL 留駐的貢獻。

雙方視角的資訊需求

客戶端服務端(組件實作方)
介面 IID
類別/類別工廠 CLSID/ProgID
介面函式原型
執行緒模型
型別庫資訊
客戶端所需所有資訊
+ DLL 導出函式(DllGetClassObject 等)
+ 註冊資訊(.reg)
+ 模組定義檔

實戰踩坑:DllGetClassObject 導出與連結錯誤

嘗試在 outdll.c 直接用 __declspec(dllexport) 修飾 DllGetClassObject 導致 C2375 錯誤:重新定義,連結屬性不同。原因在於 objbase.h 已宣告該函式為 STDAPI(隱含 extern "C" 與特定呼叫慣例),直接加 dllexport 衝突。解決方案:改用 .def 模組定義檔導出,避免衝突。

記憶體洩漏排查:類別工廠參考計數之謎

初次執行發現類別工廠物件記憶體未釋放。分析 DllGetClassObject 流程:

  1. 首次呼叫 → CreateClassFactory 建立工廠,參考計數 = 1。
  2. 隨即呼叫 QueryInterface → 計數變 2。
  3. 返回客戶端,客戶端單次 Release 只能降至 1,物件無法釋放。

根因:CreateClassFactory 內部為了取得介面指標,刻意 QueryInterface (+1) 再 Release (-1) 抵銷,但此舉導致 DllGetClassObject 再次 QueryInterface 時計數變 2。

修正策略

在 DllGetClassObject 中,首次建立工廠時,若請求介面為 IClassFactory,直接回傳指標並設定計數為 1,避免額外 QueryInterface;若請求其他介面則釋放工廠並返回失敗。後續呼叫走正常 QueryInterface 流程。

HRESULT DllGetClassObject(REFCLSID rclsid, REFIID riid, void **ppv) {
    *ppv = 0;
    if (IsEqualCLSID(rclsid, &CLSID_Outside)) {
        if (!vpcfOutside) {
            HRESULT hr = CreateClassFactory(&CLSID_Outside, CreateOutside,
                                           &IID_IClassFactory, &vpcfOutside);
            if (hr != NOERROR) return hr;
            // 首次建立,直接處理 IClassFactory 請求
            if (IsEqualIID(riid, &IID_IClassFactory)) {
                *ppv = vpcfOutside;
                return NOERROR;
            } else {
                Release(vpcfOutside); // 不支援的介面,釋放工廠
                return E_FAIL;
            }
        }
        return QueryInterface(vpcfOutside, riid, ppv); // 非首次,正常 QI
    }
    return E_FAIL;
}

修正後記憶體正常釋放。

CreateClassFactory 參考計數管理細節

HRESULT CreateClassFactory(...) {
    ClassFactory *this;
    HRESULT hr;
    *ppv = 0;
    if (hr = Alloc(sizeof(ClassFactory), &this)) return hr;
    this->icf.lpVtbl = &vtblClassFactory;
    this->cRef = 1;               // 初始計數 = 1
    this->pfnCreate = pfnCreate;
    hr = QueryInterface(&this->icf, riid, ppv); // QI 使計數 = 2
    Release(&this->icf);          // Release 使計數回到 1
    return hr;
}

此處 AddRef/Release 看似抵銷,但 Release 提供了釋放記憶體的機會(計數歸零時)。若中間 QueryInterface 失敗(如介面不支援),缺少這對呼叫會導致記憶體永不釋放。因此成對呼叫是安全防線。

組件庫檔案結構說明

  • IFoo.h:介面宣告
  • outside.c:組件物件與 IFoo 實作
  • cf.c:類別工廠物件與 IClassFactory 實作
  • outdll.c:DLL 導出函式實作
  • outside.def:模組定義檔(導出宣告)
  • outside.reg:註冊腳本
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved