在 C++ 開發中使用 COM 元件(如 ADO、WMI、Shell 介面)時,正確的 COM 程式庫初始化與釋放是穩定性的關鍵。錯誤的初始化時機或重複釋放會導致難以重現的崩潰、記憶體洩漏或「RPC 伺服器不可用」錯誤。本文詳細說明 CoInitialize/CoUninitialize 的正確使用模式,特別針對 DLL 邊界、多執行緒模型、巢狀呼叫等實戰情境。
HRESULT CoInitialize(LPVOID pvReserved); // 單執行緒單元 (STA) 初始化
HRESULT CoInitializeEx(LPVOID pvReserved, DWORD dwCoInit); // 進階模式控制
void CoUninitialize(); // 釋放 COM 程式庫
dwCoInit 常用旗標:
COINIT_APARTMENTTHREADED (0):STA 模式,適用 UI 執行緒、需訊息迴圈的情境。COINIT_MULTITHREADED (0x0):MTA 模式,適用背景工作執行緒、高併發運算。COINIT_DISABLE_OLE1DDE (0x4):停用 OLE 1.0 DDE 相容性。COINIT_SPEED_OVER_MEMORY (0x8):優先速度而非記憶體。int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
if (FAILED(hr)) return -1; // 初始化失敗直接退出
// 應用程式主邏輯:建立視窗、訊息迴圈...
Application::Run();
CoUninitialize(); // 程式結束前配對呼叫
return 0;
}
DLL 被多個載入者(EXE、其他 DLL、COM 宿主)共用,無法假設呼叫者已初始化 COM。錯誤做法:在 DllMain 中直接 CoInitialize —— 這會導致載入順序死結、執行緒附加/分離時機錯誤。
class ComInitializer
{
LONG m_refCount = 0;
bool m_initializedByUs = false;
public:
HRESULT Initialize(DWORD coInit = COINIT_APARTMENTTHREADED)
{
LONG ref = InterlockedIncrement(&m_refCount);
if (ref == 1) // 第一次初始化
{
HRESULT hr = CoInitializeEx(NULL, coInit);
if (hr == S_OK || hr == S_FALSE) // S_FALSE 表示已由他人初始化
{
m_initializedByUs = (hr == S_OK);
return S_OK;
}
InterlockedDecrement(&m_refCount);
return hr;
}
return S_OK; // 已初始化
}
void Uninitialize()
{
if (InterlockedDecrement(&m_refCount) == 0 && m_initializedByUs)
{
CoUninitialize();
m_initializedByUs = false;
}
}
};
static ComInitializer g_comInit;
extern "C" __declspec(dllexport) HRESULT __stdcall MyComFunction()
{
HRESULT hr = g_comInit.Initialize();
if (FAILED(hr)) return hr;
// 使用 COM 介面:ADO Connection、WMI 查詢等
_ConnectionPtr conn;
hr = conn.CreateInstance(__uuidof(Connection));
// ...
g_comInit.Uninitialize();
return hr;
}
若呼叫者已初始化 COM,CoInitializeEx 回傳 S_FALSE。此時不可呼叫 CoUninitialize,否則會破壞外層呼叫者的 COM 狀態。守衛類別透過 m_initializedByUs 旗標精確控制。
// 背景工作執行緒使用 MTA
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
// 執行非 UI 相關 COM 作業...
CoUninitialize();
注意:STA 執行緒必須跑訊息迴圈 (GetMessage/DispatchMessage),否則 COM 呼叫會卡死。
class CMyModule : public CComModule
{
public:
HRESULT Init(_ATL_OBJMAP_ENTRY* p, HINSTANCE h, const GUID* plibid = 0)
{
return CComModule::Init(p, h, plibid); // 內部自動 CoInitialize
}
void Term()
{
CComModule::Term(); // 內部自動 CoUninitialize
}
};
Managed 程式碼預設已在 MTA。若需呼叫 STA 元件,使用 Thread.CurrentThread.SetApartmentState(ApartmentState.STA) 並在該執行緒上執行。
| 現象 | 可能原因 | 排查方向 |
|---|---|---|
| RPC_E_CHANGED_MODE | 同一執行緒重複初始化且模式衝突 (STA vs MTA) | 檢查所有進入點是否使用一致的 COINIT 旗標 |
| CO_E_NOTINITIALIZED | 未初始化即呼叫 COM API | 確認所有 DLL 導出函式、執行緒進入點都有守衛初始化 |
| 應用程式關閉時 Hang | CoUninitialize 早於 COM 物件釋放 | 使用智慧指標 (_com_ptr_t, CComPtr) 確保 RAII 順序 |
| 記憶體洩漏 | CoInitialize 成功但未配對 CoUninitialize | 使用守衛類別、啟用 CRT 偵錯堆疊 (_CrtDumpMemoryLeaks) |
WinMain 頭尾配對初始化/釋放,模式固定為 STA。DllMain 初始化。提供導出函式由呼叫者控制,或使用參考計數守衛類別內部自管。_com_ptr_t、CComPtr、wil::com_ptr 管理介面生命週期,避免手動 Release() 遺漏。HRESULT,善用 _com_error 轉換為可讀訊息。掌握 COM 初始化的生命週期管理,是 C++ 系統級開發從「能跑」邁向「穩定可維護」的關鍵門檻。