ATL线程模型和套间
概述
- 在
COM
开发中,线程模式(Threading Model
) 和 套间(Apartment
) 是确保组件在多线程环境下安全运行的核心机制 ATL
(Active Template Library
)通过模板类(如CComObjectRootEx
)和线程模型宏,简化了线程安全的管理COM
定义了三种套间类型,用于隔离不同线程对 COM 对象的访问,确保线程安全ATL
提供多种模板类来定义线程模型,需通过CComObjectRootEx
传递给组件基类
SAT(单线程套间)
- 每个套间绑定到一个线程(通常是
UI
线程) - 对象只能在创建它的线程中被调用,跨线程调用需通过消息泵(
Message Pump
)代理
MAT(多线程套间)
- 所有
MTA
线程共享一个套间 - 对象可以被任意线程直接访问,但需自行处理线程同步
Neutral(中性套间)
- 对象不绑定到任何线程,任何线程均可直接调用
- 需要组件自身保证线程安全
单线程模型(STA
)
CComSingleThreadModel
- 单线程模型(非线程安全)
- 引用计数直接操作(
++
/--
) - 适用于
STA
套间
多线程模型(MTA
)
CComMultiThreadModel
- 多线程模型(线程安全)
- 使用
InterlockedIncrement
/Decrement
原子操作引用计数 - 适用于
MTA
套间或自由线程组件
自由线程模型(Free-Threaded
)
- 多线程模型,但去除了临界区(
Critical Section
)保护 - 需手动处理同步,适用于高性能场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class CMyComponent : public CComObjectRootEx<CComMultiThreadModelNoCS>, public IMyInterface { public: STDMETHODIMP Add(int a, int b, int* result) { // 直接操作参数(无需同步,因参数为线程栈局部变量) *result = a + b; return S_OK; } STDMETHODIMP UpdateCounter() { // 操作共享资源,需手动同步 InterlockedIncrement(&m_counter); // 原子操作 return S_OK; } private: LONG m_counter = 0; // 共享变量 }; |
ThreadingModel
- 在
.rgs
文件中设置的ThreadingModel
值(如Apartment
、Free
、Both
)是 套间模型(Apartment Model
) 的注册表配置- 而非直接对应
ATL
的线程模式类(如CComMultiThreadModel
)
- 而非直接对应
- 作用
ThreadingModel
是注册表项,用于告诉COM
运行时 该组件支持的套间类型- 它的值决定了组件实例化时如何绑定到线程套间
- 举例说明1:
- 若注册表中设置
ThreadingModel=Free
(MTA
套间),但组件使用CComSingleThreadModel
(非线程安全),则会导致 数据竞争和崩溃
- 若注册表中设置
- 举例说明2:
- 若注册表中设置
ThreadingModel=Apartment
(STA
套间),但组件使用CComMultiThreadModel
(线程安全),虽然合法,但可能浪费性能
- 若注册表中设置
ThreadingModel 值 |
对应的套间模型 | 适用场景 |
Apartment |
单线程套间(STA) | 组件仅能在创建它的线程中被调用(如 UI 组件)。 |
Free |
多线程套间(MTA) | 组件可被任意线程直接调用(需自行处理线程安全)。 |
Both |
同时兼容 STA 和 MTA(组件可运行在任何套间中,由调用者决定)。 | 通用组件,需内部保证线程安全。 |
Neutral |
中性套间(组件不绑定线程,但必须在支持中性套间的 COM 版本中)。 | .NET 互操作或高并发组件(需 Windows 2000+ 支持)。 |
- 正确配置示例:
1 2 3 4 5 6 7 8 9 10 11 12 |
// ATL 组件类声明(线程安全) class CMyComponent : public CComObjectRootEx<CComMultiThreadModel>, // 多线程模型类 public CComCoClass<CMyComponent, &CLSID_MyComponent>, public IMyInterface { public: DECLARE_REGISTRY_RESOURCEID(IDR_MYCOMPONENT) BEGIN_COM_MAP(CMyComponent) COM_INTERFACE_ENTRY(IMyInterface) END_COM_MAP() // ... }; |
1 2 3 4 5 6 7 8 9 10 |
// 注册脚本(.rgs 文件) HKCR { NoRemove CLSID { ForceRemove {CLSID_MyComponent} = s 'MyComponent Class' { InprocServer32 = s '%MODULE%' { val ThreadingModel = s 'Free' // 多线程套间(MTA) } } } } |
组件线程模式的声明
- 在
ATL
组件类中,通过继承CComObjectRootEx
并指定线程模型类来声明线程模式
1 2 3 4 5 6 7 |
class CMyComponent : public CComObjectRootEx<CComMultiThreadModel>, // 使用多线程模型 public CComCoClass<CMyComponent, &CLSID_MyComponent>, public IMyInterface { public: // ... }; |
- 线程模型决定引用计数的实现:
CComSingleThreadModel
:直接操作m_dwRef
(非线程安全)CComMultiThreadModel
:通过InterlockedIncrement
保证原子性
- 套间模型由注册表决定:
- 在
.rgs
注册脚本中指定ThreadingModel
:
- 在
1 2 3 |
InprocServer32 = s '%MODULE%' { val ThreadingModel = s 'Apartment' // 或 'Free', 'Both', 'Neutral' } |
线程模式和套间的匹配规则
线程模型类 | 兼容的套间模型 | 典型应用场景 |
CComSingleThreadModel |
STA | UI 控件、Office 插件 |
CComMultiThreadModel |
MTA 或 Neutral | 后台服务、高性能计算组件 |
CComMultiThreadModelNoCS |
MTA(需手动同步) | 低延迟、高频调用的组件 |
跨套间调用的处理
- 当客户端线程与组件套间不匹配时,
COM
运行时自动通过 代理(Proxy
) 和 存根(Stub
) 进行跨套间调用STA
→STA
:调用通过消息泵同步,确保线程安全STA
→MTA
:调用被转发到 MTA 套间,可能引发性能损耗MTA →
MTA:直接调用,无需代理
- 代码示例:
STA
组件的跨线程调用
1 2 3 4 5 6 7 8 9 |
// 在 STA 组件中,跨线程调用需通过消息泵同步 HRESULT CMySTAComponent::Method() { if (GetCurrentThreadId() != m_creatorThreadId) { // 通过 PostMessage 或 CoMarshalInterThreadInterfaceInStream 转发调用 return E_NOTIMPL; // 需实现跨线程调用逻辑 } // 实际逻辑 return S_OK; } |
线程安全与同步机制
- 临界区(
Critical Section
)- 通过
CComAutoCriticalSection
或CCriticalSection
保护共享资源
- 通过
1 2 3 4 5 6 7 8 9 10 |
class CMyComponent : public CComObjectRootEx<CComMultiThreadModel> { public: STDMETHODIMP UpdateData() { CComCritSecLock<CComAutoCriticalSection> lock(m_cs); // 自动加锁 // 修改共享数据 return S_OK; } private: CComAutoCriticalSection m_cs; }; |
- 线程本地存储(
TLS
)- 使用
CComTLS
管理线程局部变量:
- 使用
1 2 3 4 5 6 7 8 9 10 11 |
CComTLS<int> g_tlsData; HRESULT CMyComponent::Method() { int* pData = g_tlsData.GetData(); if (!pData) { pData = new int(0); g_tlsData.SetData(pData); } (*pData)++; return S_OK; } |
智能指针
CComPtr
- 基础
COM
接口指针管理 - 核心功能
- 自动管理引用计数:在构造时调用
AddRef
,析构时调用Release
- 支持接口指针的赋值、比较和操作(如
->
运算符) - 显式释放控制:通过
Release
方法手动释放资源
- 自动管理引用计数:在构造时调用
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <atlbase.h> void UseCComPtr() { CComPtr<IMyInterface> spMyInterface; // 创建对象并获取接口指针(自动调用 AddRef) HRESULT hr = spMyInterface.CoCreateInstance(CLSID_MyComponent); if (SUCCEEDED(hr)) { spMyInterface->Method(); // 通过 -> 调用方法 } // 析构时自动调用 Release } |
- 关键
1 2 |
CComPtr<IMyInterface> sp1; CComPtr<IMyInterface> sp2 = sp1; // 正确:sp2 增加引用计数 |
1 |
spMyInterface.Release(); // 手动释放并置空指针 |
- 注意事项
- 避免循环引用:若两个
CComPtr
互相引用,需手动打破循环 - 不支持跨线程直接传递:需结合
CoMarshalInterThreadInterfaceInStream
- 避免循环引用:若两个
CComQIPtr
- 支持接口查询的智能指针
- 核心功能
- 继承自
CComPtr
,额外支持QueryInterface
功能 - 简化接口查询:通过构造函数或赋值操作自动查询目标接口
- 继承自
1 2 3 4 5 6 7 8 9 10 |
void UseCComQIPtr() { CComPtr<IUnknown> spUnknown; spUnknown.CoCreateInstance(CLSID_MyComponent); // 自动查询 IMyInterface 接口 CComQIPtr<IMyInterface> spMyInterface = spUnknown; if (spMyInterface) { spMyInterface->Method(); } } |
- 区别与
CComPtr
- 构造函数支持
IUnknown*
:自动调用QueryInterface
- 更简洁的接口转换:无需手动调用
QueryInterface
- 构造函数支持
CComWeakPtr
ATL
中的弱引用智能指针- 核心目标是 打破
COM
对象间的循环引用,避免内存泄漏 - 它不会增加对象的引用计数,因此不会阻止对象的销毁
- 核心目标是 打破
- 弱引用核心作用
- 问题背景:当两个或多个
COM
对象通过CComPtr
相互引用时,会形成循环引用(如A→B→A
),导致引用计数无法归零,对象无法释放 - 解决方案:将其中一个引用改为弱引用(
CComWeakPtr
),使其不增加引用计数
- 问题背景:当两个或多个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// 对象 A 的接口 class IA : public IUnknown { public: virtual HRESULT SetB(IB* pB) = 0; }; // 对象 B 的接口 class IB : public IUnknown { public: virtual HRESULT SetA(IA* pA) = 0; }; // 对象 A 的实现类 class CA : public CComObjectRootEx<CComMultiThreadModel>, public IA { public: CComPtr<IB> m_spB; // 强引用对象 B STDMETHODIMP SetB(IB* pB) override { m_spB = pB; return S_OK; } }; // 对象 B 的实现类 class CB : public CComObjectRootEx<CComMultiThreadModel>, public IB { public: CComWeakPtr<IA> m_wpA; // 弱引用对象 A STDMETHODIMP SetA(IA* pA) override { m_wpA.Attach(pA); // 绑定弱引用 return S_OK; } }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
CComObject<CA>* pA = nullptr; CComObject<CA>::CreateInstance(&pA); pA->AddRef(); CComObject<CB>* pB = nullptr; CComObject<CB>::CreateInstance(&pB); pB->AddRef(); // 设置相互引用(A 强引用 B,B 弱引用 A) pA->SetB(pB); pB->SetA(pA); // 当释放 pA 和 pB 时,循环引用被打破,对象正确释放 pA->Release(); pB->Release(); |
- 实现原理
CComWeakPtr
内部通过IWeakRef
接口跟踪目标对象,其工作流程如下:- 绑定到对象:通过
Attach
或构造函数绑定到目标对象的IWeakRef
- 获取强引用:通过
Lock
方法将弱引用升级为临时强引用(CComPtr
) - 对象销毁:当目标对象释放时,所有关联的弱引用自动失效
1 2 3 4 5 6 7 |
// 声明弱引用指针 CComWeakPtr<IMyInterface> wpMyInterface; // 从 CComPtr 初始化 CComPtr<IMyInterface> spMyInterface; spMyInterface.CoCreateInstance(CLSID_MyComponent); wpMyInterface.Attach(spMyInterface); // 绑定弱引用 |
1 2 3 4 5 6 7 |
// 升级为临时强引用 CComPtr<IMyInterface> spTemp = wpMyInterface.Lock(); if (spTemp) { spTemp->Method(); // 安全调用 } else { // 对象已被释放 } |
1 2 |
wpMyInterface.Detach(); // 解除弱引用绑定(不释放对象) wpMyInterface.Release(); // 释放弱引用(等效于 Detach) |
- 对比
CComPtr
特性 | CComPtr |
CComWeakPtr |
引用计数影响 | 增加引用计数(强引用) | 不增加引用计数(弱引用) |
对象生命周期管理 | 延长对象生命周期 | 不阻止对象销毁 |
安全访问 | 直接访问(对象一定存活) | 需通过 Lock 获取临时强引用 |
典型用途 | 常规所有权管理 | 打破循环引用、观察者模式、缓存 |
- 注意事项
- 线程安全
CComWeakPtr
本身不保证线程安全
若在多线程环境中使用,需通过锁(如CComAutoCriticalSection
)保护弱引用的绑定和访问 - 对象存活检查
调用Lock
后必须检查返回的CComPtr
是否有效,避免访问已释放对象: - 避免裸指针转换
不要直接通过wp.p
访问对象,需通过Lock
升级为强引用:
- 线程安全
1 2 |
CComPtr<IMyInterface> sp = wp.Lock(); if (sp) { /* 安全操作 */ } |
1 2 3 4 5 6 7 |
// ❌ 危险操作(对象可能已被释放) IMyInterface* pRaw = wpMyInterface.p; pRaw->Method(); // ✅ 安全操作 CComPtr<IMyInterface> sp = wpMyInterface.Lock(); if (sp) sp->Method(); |
资源管理
CComBSTR
- 封装
BSTR
字符串 - 核心功能
- 自动管理
BSTR
内存:构造时分配,析构时释放 - 支持
BSTR
与wchar_t\*
的转换 - 提供字符串操作方法(如
Append
、ToLower
)
- 自动管理
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void UseCComBSTR() { CComBSTR bstrValue(OLESTR("Hello")); // 构造 BSTR // 修改字符串 bstrValue.Append(OLESTR(" World")); // 转换为 wchar_t*(临时指针,无需手动释放) const wchar_t* psz = bstrValue; MessageBoxW(nullptr, psz, L"Message", MB_OK); // 显式释放(可选) bstrValue.Empty(); } |
- 注意事项
- 避免多次释放:不要将
CComBSTR
管理的BSTR
手动传给SysFreeString
- 跨方法传递:通过
Detach()
转移所有权(避免拷贝开销)
- 避免多次释放:不要将
CComVariant
- 封装
VARIANT
类型 - 核心功能
- 自动管理
VARIANT
生命周期:构造时初始化,析构时调用VariantClear
- 支持类型转换:自动处理
VARIANT
的类型转换(如LONG
→BSTR
)
- 自动管理
1 2 3 4 5 6 7 |
void UseCComVariant() { CComVariant varInt(100); // 初始化为 LONG varInt.ChangeType(VT_BSTR); // 转换为字符串类型 // 获取 BSTR 值 CComBSTR bstrValue = varInt.bstrVal; } |
- 常见操作
- 赋值:
varInt = 200;
(自动更新类型) - 类型检查:
if (varInt.vt == VT_I4) { ... }
- 赋值:
CComHeapPtr
- 管理堆内存
- 核心功能
- 封装
malloc
/free
或CoTaskMemAlloc
/CoTaskMemFree
- 适用于非
COM
内存(如通过CoTaskMemAlloc
分配的内存)
- 封装
1 2 3 4 5 6 7 8 |
void UseCComHeapPtr() { CComHeapPtr<wchar_t> spBuffer; spBuffer.Allocate(1024); // 分配 1024 个 wchar_t wcscpy_s(spBuffer, 1024, L"Data"); // 析构时自动释放内存 } |
资源管理-其他
BSTR
BSTR
是COM
中用于表示字符串的数据类型,其本质是一个 带有长度前缀的Unicode
(UTF-16
)字符串- 内存布局
- 前
4
字节:字符串长度(字节数,不包括终止符) - 后续字节:Unicode 字符内容,以双空字符(
\0\0
)结束
- 前
1 2 |
// 内存示例(字符串 "Hello"): [4字节长度=10] [H][e][l][l][o][\0][\0] |
- 核心特点
- 长度前缀:允许快速获取字符串长度(无需遍历到结尾)
- 显式内存管理:必须使用
COM
API
分配和释放内存(SysAllocString
、SysFreeString
) - 支持嵌入空字符:不同于普通
C
字符串,BSTR
可以包含\0
(如二进制数据)
- 常用
API
函数 | 作用 |
SysAllocString |
分配 BSTR 内存(根据输入字符串)。 |
SysAllocStringLen |
分配指定长度的 BSTR。 |
SysFreeString |
释放 BSTR 内存。 |
SysStringLen |
获取 BSTR 的字符数(非字节数)。 |
1 2 3 4 5 6 |
// 创建 BSTR BSTR bstr = SysAllocString(L"Hello"); // 使用 BSTR wprintf(L"BSTR: %s, Length: %d\n", bstr, SysStringLen(bstr)); // 释放 BSTR SysFreeString(bstr); |
- 注意事项
- 内存泄漏:忘记调用
SysFreeString
会导致内存泄漏 - 跨模块传递:
BSTR
必须由同一内存堆分配和释放(如组件与客户端使用不同CRT
库时需谨慎) ATL
包装类:CComBSTR
可自动管理BSTR
生命周期
- 内存泄漏:忘记调用
1 2 |
CComBSTR bstrValue(L"Hello"); bstrValue.Append(L" World"); // 自动处理内存 |
VARIANT
VARIANT
是一个联合体(Union
),能够存储多种数据类型(如整数、浮点数、字符串、对象引用等)- 核心字段
vt
:数据类型标记(如VT_I4
表示4
字节整数)- 其他字段:根据
vt
的值选择对应成员(如lVal
、bstrVal
)
1 2 3 4 5 6 7 8 9 10 11 12 |
typedef struct tagVARIANT { VARTYPE vt; // 数据类型标记 WORD wReserved1; WORD wReserved2; WORD wReserved3; union { LONG lVal; // VT_I4 BSTR bstrVal; // VT_BSTR IUnknown* punkVal; // VT_UNKNOWN // ... 其他成员 }; } VARIANT; |
- 核心特点
- 动态类型:运行时根据
vt
决定实际数据类型 - 自动化兼容:支持跨语言传递复杂数据(如脚本语言与
C++
组件交互) - 内存管理:需手动初始化(
VariantInit
)和清理(VariantClear
)
- 动态类型:运行时根据
- 常用
API
函数 | 作用 |
VariantInit |
初始化 VARIANT(设为 VT_EMPTY )。 |
VariantClear |
释放 VARIANT 中持有的资源(如 BSTR)。 |
VariantCopy |
深拷贝一个 VARIANT。 |
VariantChangeType |
转换 VARIANT 到指定类型。 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
VARIANT var; VariantInit(&var); // 初始化 // 赋值整数 var.vt = VT_I4; var.lVal = 42; // 转换为字符串 VARIANT varStr; VariantInit(&varStr); VariantChangeType(&varStr, &var, 0, VT_BSTR); wprintf(L"String: %s\n", varStr.bstrVal); // 清理 VariantClear(&var); VariantClear(&varStr); |
- 注意事项
- 类型安全:必须正确设置
vt
字段,否则访问错误成员会导致未定义行为 - 资源泄漏:忘记调用
VariantClear
会泄漏内存(如未释放BSTR
或IUnknown*
) ATL
包装类:CComVariant
自动管理生命周期
- 类型安全:必须正确设置
1 2 3 |
CComVariant var(100); // VT_I4 var.ChangeType(VT_BSTR); // 转换为字符串 CComBSTR bstr = var.bstrVal; |
VARIANT 如何存储数组
- 使用
VT_ARRAY
标记,结合SAFEARRAY
类型
1 2 3 4 |
SAFEARRAY* psa = SafeArrayCreateVector(VT_I4, 0, 10); VARIANT var; var.vt = VT_ARRAY | VT_I4; var.parray = psa; |
BSTR
和VARIANT
对比
特性 | BSTR | VARIANT |
用途 | 专用于字符串 | 通用数据类型容器 |
内存管理 | SysAllocString /SysFreeString |
VariantInit /VariantClear |
线程安全 | 是(COM 内存分配器线程安全) | 依赖具体实现 |
跨语言支持 | 是(自动化兼容) | 是(自动化核心类型) |
跨进程通信与代理/存根(Proxy
/Stub
)
概述
- 在
COM
(Component Object Model
)中,跨进程通信(IPC
,Inter-Process Communication
) 允许客户端进程调用另一个进程或远程机器上的COM
对象 - 为实现这一目标,
COM
使用 代理(Proxy
) 和 存根(Stub
) 机制,而ATL
(Active Template Library
)通过工具和模板简化了其实现
跨进程通信的核心原理
- 代理(
Proxy
):- 位于 客户端进程,模仿实际对象的接口
- 将客户端的调用参数 列集(
Marshaling
) 为网络或跨进程可传输的格式(如字节流) - 将请求发送到服务端进程
- 存根(
Stub
):- 位于 服务端进程,接收代理发送的数据
- 散集(
Unmarshaling
) 参数,调用实际对象的接口方法 - 将结果列集后返回给代理
- 通信流程
- 客户端调用代理接口方法 → 代理列集参数 → 发送到服务端
- 存根接收数据 → 散集参数 → 调用实际对象方法 → 列集结果 → 返回给代理
- 代理散集结果 → 返回给客户端
ATL
中代理/存根的生成
MIDL
编译器- 根据
IDL
(Interface Definition Language
)文件生成代理/存根代码(.h
、_p.c
、_i.c
)
- 根据
ATL
模板- 提供默认的列集实现(如
IMarshal
的自动支持)
- 提供默认的列集实现(如
- 具体步骤如下:
- 步骤一:定义接口(
IDL
文件)
1 2 3 4 5 6 7 8 9 |
// Example.idl [ object, uuid(6B29FC40-CA47-1067-B31D-00DD010662DA), pointer_default(unique) ] interface IExample : IUnknown { HRESULT Add([in] int a, [in] int b, [out, retval] int* result); }; |
- 步骤二:编译
IDL
生成代理/存根代码
1 |
midl Example.idl |
- 生成文件如下:
Example_p.c
:代理/存根实现代码Example.h
:接口的C++
定义Example_i.c
:接口的GUID
定义
- 创建代理/存根
DLL
项目- 在
ATL
项目中添加生成的_p.c
和_i.c
文件 - 实现
DllGetClassObject
和DllRegisterServer
函数
- 在
注册代理/存根 DLL
- 注册命令
1 |
regsvr32 ExamplePS.dll # 注册代理/存根 DLL |
- 注册表项:代理/存根
CLSID
1 2 3 4 5 6 7 8 9 10 11 12 13 |
HKCR { CLSID { {PROXY_CLSID} = s 'Example Proxy' { InprocServer32 = s '%MODULE%' { val ThreadingModel = s 'Both' } } } } |
处理自定义数据类型的列集
- 标准列集
- 默认支持类型:基本类型(
int
、BSTR
)、COM
接口指针、SAFEARRAY
等 - 自动列集:
MIDL
生成的代码自动处理这些类型
- 默认支持类型:基本类型(
- 自定义类型列集
- 若接口方法包含自定义结构或复杂类型,需手动实现列集逻辑:
- 定义类型序列化规则:
实现IMarshal
接口或使用IPersistStream
- 在
IDL
中标记自定义类型:
1 2 3 4 5 6 7 8 9 10 11 12 |
typedef struct MyStruct { int x; float y; } MyStruct; [ object, uuid(...) ] interface IExample : IUnknown { HRESULT ProcessStruct([in] MyStruct data); }; |
- 自定义代理/存根代码:
- 在
_p.c
文件中添加对MyStruct
的列集处理
- 在
ATL
中的默认列集优化
ATL
提供 标准列集(Standard Marshaling
) 的自动化支持,通过以下方式简化开发:DECLARE_REGISTRY_RESOURCEID
- 自动注册组件的代理/存根信息
CComCoClass
- 自动生成类工厂,支持代理/存根实例化
跨进程通信的线程模型
- 套间模型(
Apartment
):- 若客户端和服务端线程模型不同(如
STA
↔MTA
),COM
运行时自动通过代理/存根同步调用
- 若客户端和服务端线程模型不同(如
- 线程安全
- 使用
CComMultiThreadModel
确保组件线程安全
- 使用
完整示例
IDL
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// Example.idl import "oaidl.idl"; import "ocidl.idl"; [ object, uuid(6B29FC40-CA47-1067-B31D-00DD010662DA), pointer_default(unique) ] interface IExample : IUnknown { HRESULT Add([in] int a, [in] int b, [out, retval] int* result); }; [ uuid(12345678-ABCD-EF12-3456-7890ABCDEF01), version(1.0) ] library ExampleLib { importlib("stdole32.tlb"); [ uuid(23456789-ABCD-EF12-3456-7890ABCDEF02) ] coclass CExample { [default] interface IExample; }; }; |
- 代理/存根
DLL
项目- 将
Example_p.c
和Example_i.c
添加到ATL
DLL
项目中 - 编译生成
ExamplePS.dll
- 将
- 客户端调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include "Example.h" #include <iostream> int main() { CoInitialize(nullptr); CComPtr<IExample> spExample; CoCreateInstance( CLSID_CExample, nullptr, CLSCTX_LOCAL_SERVER, // 跨进程调用 IID_IExample, (void**)&spExample ); if (spExample) { int result = 0; spExample->Add(3, 5, &result); std::cout << "Result: " << result << std::endl; } CoUninitialize(); return 0; } |
调试
ATLASSERT
- 断言宏
- 功能
- 条件检查:验证表达式是否为真,若为假则触发断言失败,中断程序执行
- 仅调试模式生效:在
Release
构建中自动禁用,无性能开销
1 2 3 4 5 6 |
HRESULT CMyComponent::Method(int value) { ATLASSERT(value >= 0); // 确保输入非负 if (value < 0) return E_INVALIDARG; // ... 业务逻辑 return S_OK; } |
1 2 |
Assertion Failed: value >= 0 File: MyComponent.cpp, Line 42 |
- 配置
- 禁用断言:在项目属性中定义
ATL_NO_ASSERT
宏,或使用#undef ATLASSERT
- 禁用断言:在项目属性中定义
ATLTRACE
- 调试输出宏
- 功能
- 日志输出:在调试输出窗口(如
Visual Studio
的Output
窗口)打印格式化消息 - 分级输出:支持不同调试级别(如
TRACE_LEVEL_INFO
、TRACE_LEVEL_ERROR
)
- 日志输出:在调试输出窗口(如
1 2 3 4 |
void CMyComponent::Method() { ATLTRACE(L"Method called with value = %d\n", m_value); ATLTRACE2(TRACE_LEVEL_ERROR, L"Critical error occurred!\n"); } |
1 2 |
MyComponent.cpp(50): Method called with value = 42 MyComponent.cpp(55): [ERROR] Critical error occurred! |
- 配置
- 启用/禁用:通过
ATLTRACE_LEVEL
宏控制输出级别(默认启用所有级别) - 输出目标:默认输出到调试器,可重定向到文件或日志系统
- 启用/禁用:通过
_ATL_DEBUG_INTERFACES
- 接口引用跟踪
- 功能
- 接口泄漏检测:跟踪所有
AddRef
和Release
调用,输出未释放的接口指针 - 线程安全分析:记录接口操作的线程
ID
,帮助排查多线程问题
- 接口泄漏检测:跟踪所有
- 配置
- 在
stdafx.h
或项目预处理器定义中添加:
- 在
1 |
#define _ATL_DEBUG_INTERFACES |
1 2 3 |
Interface: IUnknown, AddRef: 1, Release: 0, Address: 0x00A3F8D4 Interface: IMyInterface, AddRef: 3, Release: 2, Address: 0x00A3F8E0 *** Leaked Interface: IMyInterface (1 reference) |
_ATL_DEBUG_QI
- 接口查询跟踪
- 功能
QueryInterface
调用跟踪:记录所有QueryInterface
请求,帮助识别接口查询失败或冗余调用
- 配置
1 |
#define _ATL_DEBUG_QI |
1 2 |
QueryInterface for IID_IMyInterface succeeded. QueryInterface for IID_IUnknown failed (E_NOINTERFACE). |
_ATL_DEBUG_REFCOUNT
- 引用计数跟踪
- 功能
- 对象生命周期跟踪:记录对象创建、销毁及引用计数变化
- 适用场景:检测对象过早释放或未释放
- 配置
1 |
#define _ATL_DEBUG_REFCOUNT |
1 2 3 4 |
Object created: 0x00A3F8D4, RefCount = 1 AddRef called: 0x00A3F8D4, RefCount = 2 Release called: 0x00A3F8D4, RefCount = 1 Object destroyed: 0x00A3F8D4 |
CrtDbg
系列函数(CRT
调试支持)
- 功能
- 内存泄漏检测:结合
_CrtSetDbgFlag
和_CrtDumpMemoryLeaks
,定位未释放的堆内存 - 堆分配跟踪:记录内存分配点(文件名和行号)
- 内存泄漏检测:结合
1 2 3 4 5 6 7 8 |
#include <crtdbg.h> int main() { _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); int* p = new int(42); // 忘记 delete p; return 0; } |
1 2 3 4 |
Detected memory leaks! Dumping objects -> MyApp.cpp(5): {123} normal block at 0x00A3F8D4, 4 bytes long. Data: < * > 2A 00 00 00 |
OutputDebugString
- 作用
- 将字符串发送到调试器的输出窗口(如
Visual Studio
的Output
窗口)或系统调试通道
- 将字符串发送到调试器的输出窗口(如
- 场景
- 跟踪程序执行流程
- 输出变量值、错误信息或诊断数据
- 调试无法附加调试器的环境(如服务进程)
- 函数原型
- 根据项目字符集设置(
Unicode
或多字节),通常直接使用宏OutputDebugString
:
- 根据项目字符集设置(
1 2 3 4 5 |
// ANSI 版本 void OutputDebugStringA(LPCSTR lpOutputString); // Unicode 版本 void OutputDebugStringW(LPCWSTR lpOutputString); |
1 2 3 4 5 |
#ifdef UNICODE #define OutputDebugString OutputDebugStringW #else #define OutputDebugString OutputDebugStringA #endif |
- 示例
- 由于
OutputDebugString
不支持直接格式化字符串,需结合CString
、std::wstring
或swprintf_s
:
- 由于
1 2 3 4 5 6 7 8 |
#include <Windows.h> void MyFunction(int value) { OutputDebugString(L"MyFunction called\n"); if (value < 0) { OutputDebugString(L"Error: value is negative\n"); } } |
1 2 3 4 5 |
void LogValue(int value) { wchar_t buffer[128]; swprintf_s(buffer, L"Current value = %d\n", value); OutputDebugString(buffer); } |
- 结合
ATLTRACE
ATL
的ATLTRACE
宏内部封装了OutputDebugString
,并提供格式化支持:
1 |
ATLTRACE(L"Value: %d, Name: %s\n", value, name); // 自动格式化并调用 OutputDebugString |
其他工具与技巧
Visual Studio
调试器- 条件断点:在
AddRef
或Release
处设置断点,检查引用计数变化 - 内存窗口:直接查看对象内存布局,分析虚函数表和成员变量
- 条件断点:在
Application Verifier
- 内存越界检测:检查堆溢出、使用已释放内存等问题
COM
兼容性检查:验证组件是否符合COM
规范
Process Monitor
- 注册表/文件访问监控:排查组件注册失败或路径错误
连接点与事件机制
概述
- 在
COM
(Component Object Model
)中,连接点(Connection Point
) 是组件向客户端发送事件(Event
)的核心机制 ATL
(Active Template Library
)通过IConnectionPoint
和IConnectionPointImpl
模板类简化了连接点的实现
连接点与事件机制的核心概念
- 角色定义
- 事件源(
Source
):组件(Server
)定义事件接口,并在特定条件下触发事件 - 事件接收器(
Sink
):客户端(Client
)实现事件接口,订阅组件的事件
- 事件源(
- 核心接口
IConnectionPointContainer
:组件实现此接口,用于管理多个连接点(每个连接点对应一个事件接口)IConnectionPoint
:每个连接点实现此接口,用于管理客户端的订阅(如添加、删除事件接收器)- 事件接口:自定义接口(如
IMyEvent
),客户端需实现其方法以接收事件
实现步骤
- 定义事件接口(
IDL
文件)- 在
IDL
文件中定义事件接口,并用[source]
标记其方向(从组件到客户端):
- 在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// EventExample.idl [ object, uuid(A4F4B3E0-1234-5678-9ABC-DEF123456789), // IID_IMyEvent pointer_default(unique) ] interface IMyEvent : IUnknown { [helpstring("事件触发时调用")] HRESULT OnValueChanged([in] int newValue); }; [ uuid(12345678-ABCD-EF12-3456-7890ABCDEF01), // LIBID_EventExampleLib version(1.0) ] library EventExampleLib { importlib("stdole32.tlb"); [ uuid(23456789-ABCD-EF12-3456-7890ABCDEF02), // CLSID_CMyComponent noncreatable // 客户端不能直接创建事件接口 ] coclass CMyComponent { [default] interface IMyComponent; [source] interface IMyEvent; // 标记为事件源接口 }; }; |
- 生成代理/存根代码
- 运行
MIDL
编译器生成EventExample_p.c
和EventExample.h
,确保事件接口可跨进程传递
- 运行
ATL
实现连接点
- 组件类继承
IConnectionPointContainerImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
#include "EventExample.h" class ATL_NO_VTABLE CMyComponent : public CComObjectRootEx<CComMultiThreadModel>, public CComCoClass<CMyComponent, &CLSID_CMyComponent>, public IConnectionPointContainerImpl<CMyComponent>, // 连接点容器 public IMyComponent, public IConnectionPointImpl<CMyComponent, &IID_IMyEvent> { // 连接点实现 public: DECLARE_REGISTRY_RESOURCEID(IDR_MYCOMPONENT) BEGIN_COM_MAP(CMyComponent) COM_INTERFACE_ENTRY(IMyComponent) COM_INTERFACE_ENTRY(IConnectionPointContainer) END_COM_MAP() BEGIN_CONNECTION_POINT_MAP(CMyComponent) CONNECTION_POINT_ENTRY(IID_IMyEvent) // 注册连接点 END_CONNECTION_POINT_MAP() // IMyComponent 方法 STDMETHODIMP SetValue(int value) { m_value = value; Fire_OnValueChanged(value); // 触发事件 return S_OK; } private: int m_value = 0; // 触发事件的方法 HRESULT Fire_OnValueChanged(int newValue) { T* pT = static_cast<T*>(this); int cConnections = m_vec.GetSize(); for (int i = 0; i < cConnections; i++) { pT->Lock(); CComPtr<IUnknown> punkConnection = m_vec.GetAt(i); pT->Unlock(); IMyEvent* pEvent = nullptr; if (SUCCEEDED(punkConnection->QueryInterface(IID_IMyEvent, (void**)&pEvent))) { pEvent->OnValueChanged(newValue); pEvent->Release(); } } return S_OK; } }; |
- 关键代码解析
BEGIN_CONNECTION_POINT_MAP
宏:
定义组件支持的所有连接点(每个事件接口一个条目)Fire_OnValueChanged
方法:
历所有订阅客户端的IMyEvent
接口,调用其OnValueChanged
方法
客户端订阅事件
- 实现事件接收器
Sink
- 客户端需实现事件接口
IMyEvent
:
- 客户端需实现事件接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class CMyEventSink : public IMyEvent { public: STDMETHODIMP QueryInterface(REFIID riid, void** ppv) override { if (riid == IID_IMyEvent || riid == IID_IUnknown) { *ppv = static_cast<IMyEvent*>(this); AddRef(); return S_OK; } return E_NOINTERFACE; } STDMETHODIMP_(ULONG) AddRef() override { return 1; } // 简单引用计数 STDMETHODIMP_(ULONG) Release() override { return 1; } // 事件处理逻辑 STDMETHODIMP OnValueChanged(int newValue) override { printf("Value changed to %d\n", newValue); return S_OK; } }; |
- 订阅事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
CComPtr<IMyComponent> spComponent; spComponent.CoCreateInstance(CLSID_CMyComponent); CComPtr<IConnectionPointContainer> spCPC; spComponent->QueryInterface(IID_IConnectionPointContainer, (void**)&spCPC); CComPtr<IConnectionPoint> spCP; spCPC->FindConnectionPoint(IID_IMyEvent, &spCP); CMyEventSink sink; DWORD dwCookie; spCP->Advise(&sink, &dwCookie); // 订阅事件 // 触发事件(客户端会收到 OnValueChanged 调用) spComponent->SetValue(42); // 取消订阅 spCP->Unadvise(dwCookie); |
ATL
的自动化支持
IConnectionPointImpl
- 自动管理连接列表:
- 通过
m_vec
成员(CComDynamicUnkArray
)存储客户端的IUnknown
指针
- 通过
- 简化事件触发:
- 提供
Fire_OnValueChanged
的通用实现(需手动遍历调用)
- 提供
IDispEventImpl
(基于调度的连接点)
- 若事件接口继承自
IDispatch
(自动化兼容),可用IDispEventImpl
进一步简化:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class CMyComponent : public IDispEventImpl<1, CMyComponent, &IID_IMyEvent, &LIBID_EventExampleLib> { public: BEGIN_SINK_MAP(CMyComponent) SINK_ENTRY_EX(1, IID_IMyEvent, DISPID_ONVALUECHANGED, OnValueChanged) END_SINK_MAP() // 事件处理方法 HRESULT __stdcall OnValueChanged(int newValue) { // 处理事件 return S_OK; } }; |
相关理解
- 我的
COM
组件实现IConnectionPointContainer
这个接口后,组件就有能力管理多个连接点- 每个事件接口(如
IID_MyEvent
)对应一个连接点,管理客户端的订阅列表 Advise()
:客户端订阅事件Unadvise()
:客户端取消订阅
- 每个事件接口(如
- 客户端要使用我的
COM
组件的某一个事件(如IID_MyEvent
),它就要实现这个事件的接口(如IMyEvent
),提供具体的回调方法- 然后客户端还需要通过
IConnectionPoint::Advise()
注册事件接收器 - 当调用组件的某个事件接口时,会触发事件,然后客户端里写的接收器的回调方法被调用
- 然后客户端还需要通过
1 2 3 4 5 6 7 8 9 |
+------------------+ +-----------------------+ | COM 组件 | | 客户端 | | | | | | 1. 实现 | <------+ | 2. 实现 | | IConnectionPointContainer | IMyEvent (Sink) | | & IConnectionPoint | | | | | | | 3. 触发事件 | -------> | 4. 回调 OnValueChanged| +------------------+ +-----------------------+ |
- 示例代码:组件端
ATL
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 组件类继承连接点容器和连接点模板 class CMyComponent : public CComObjectRootEx<CComMultiThreadModel>, public IConnectionPointContainerImpl<CMyComponent>, public IConnectionPointImpl<CMyComponent, &IID_IMyEvent> { BEGIN_CONNECTION_POINT_MAP(CMyComponent) CONNECTION_POINT_ENTRY(IID_IMyEvent) END_CONNECTION_POINT_MAP() // 触发事件的方法 void Fire_ValueChanged(int newValue) { // 遍历所有订阅的客户端,调用其 OnValueChanged for (int i = 0; i < m_vec.GetSize(); i++) { CComPtr<IMyEvent> spEvent; m_vec.GetAt(i)->QueryInterface(&spEvent); if (spEvent) { spEvent->OnValueChanged(newValue); } } } }; |
- 示例代码:客户端订阅事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// 客户端实现事件接收器 class CMyEventSink : public IMyEvent { STDMETHODIMP OnValueChanged(int newValue) override { std::cout << "Value changed to: " << newValue << std::endl; return S_OK; } // 省略 QueryInterface/AddRef/Release... }; // 订阅事件 CComPtr<IMyComponent> spComponent; spComponent.CoCreateInstance(CLSID_MyComponent); CComPtr<IConnectionPointContainer> spCPC; spComponent->QueryInterface(&spCPC); CComPtr<IConnectionPoint> spCP; spCPC->FindConnectionPoint(IID_IMyEvent, &spCP); CMyEventSink sink; DWORD dwCookie; spCP->Advise(&sink, &dwCookie); // 订阅 // 触发事件(例如调用组件方法) spComponent->SetValue(42); // 内部调用 Fire_ValueChanged // 取消订阅 spCP->Unadvise(dwCookie); |
- 总结理解
- 通过
COM
的连接点和事件机制,客户端可以在调用COM
组件的某些接口时,触发组件向客户端注册的接收器(Sink
)回调,执行客户端自己实现的函数(如OnValueChanged
)
- 通过
应用场景
- 用户界面(
UI
)交互与更新COM
组件执行耗时操作(如文件下载、数据处理),完成后通知客户端更新界面- 后台下载组件在下载完成时触发
OnDownloadComplete
事件,客户端收到事件后刷新界面
- 异步操作的状态反馈
- 组件执行异步任务(如网络请求、数据库查询),客户端需要实时获取进度或结果
- 数据库查询组件在每次获取一批数据时触发
OnDataReceived
事件,客户端逐步显示结果
- 实时数据监控
- 组件持续生成数据(如传感器读数、股票行情),客户端需要实时接收并处理
- 传感器组件每秒触发
OnSensorUpdate
事件,客户端实时绘制波形图
- 插件或扩展系统
- 主程序(如浏览器、
IDE
)通过COM
组件支持插件,插件需要响应主程序事件 Visual Studio
插件在代码编译完成后接收OnBuildComplete
事件,执行自定义分析
- 主程序(如浏览器、
- 分布式系统的事件通知
- 在分布式系统中,服务端组件向多个客户端广播状态变化(如聊天消息、订单状态)
- 聊天服务组件在用户发送消息时触发
OnNewMessage
事件,所有在线客户端实时显示消息
- 自动化控制与脚本交互
- 脚本语言(如
VBScript
、Python
)通过COM
控制应用程序,需要事件回调支持 Excel
的COM
组件在单元格内容修改时触发OnCellChanged
事件,脚本自动执行数据校验
- 脚本语言(如
多播事件与线程安全
概述
- 在
COM
开发中,多播事件(Multicast Events
) 指一个事件接口被多个客户端订阅,组件触发事件时需遍历所有订阅的客户端并调用其回调方法 - 线程安全(
Thread Safety
) 则是确保在多线程环境下,事件订阅列表的遍历、修改以及回调过程不会导致数据竞争或资源冲突
多播事件的核心挑战
- 多客户端管理
- 订阅列表动态变化:客户端可能随时通过
Advise
/Unadvise
订阅或取消订阅,需确保列表遍历过程中不被意外修改 - 多线程可能同时触发事件或修改订阅列表,导致竞态条件(
Race Condition
)
- 订阅列表动态变化:客户端可能随时通过
- 线程安全的必要
- 数据一致性:避免订阅列表在遍历时被其他线程修改(如删除或添加客户端),导致崩溃或遗漏通知
- 回调安全:确保客户端回调方法在不同线程中执行时,不会因共享资源冲突导致未定义行为
线程安全实现方案
- 使用线程模型类(
CComMultiThreadModel
)ATL
的线程模型类(如CComMultiThreadModel
)提供线程安全的引用计数和同步原语(如临界区)- 继承线程模型:组件类继承
CComObjectRootEx<CComMultiThreadModel>
- 保护订阅列表:在遍历或修改客户端列表时加锁
- 同步机制(临界区与锁)
- 使用
CComAutoCriticalSection
或CCriticalSection
保护订阅列表的访问
- 使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class CMyComponent : public CComObjectRootEx<CComMultiThreadModel>, public IConnectionPointImpl<CMyComponent, &IID_IMyEvent> { private: CComAutoCriticalSection m_cs; // 临界区保护订阅列表 public: HRESULT Fire_OnValueChanged(int newValue) { CComCritSecLock<CComAutoCriticalSection> lock(m_cs); // 自动加锁 int cConnections = m_vec.GetSize(); for (int i = 0; i < cConnections; i++) { CComPtr<IUnknown> punkConnection = m_vec.GetAt(i); IMyEvent* pEvent = nullptr; if (SUCCEEDED(punkConnection->QueryInterface(&pEvent))) { pEvent->OnValueChanged(newValue); pEvent->Release(); } } return S_OK; } }; |
- 客户端的动态管理
- 添加/删除客户端时加锁:确保
Advise
和Unadvise
操作线程安全 - 使用线程安全容器:如
CComDynamicUnkArray
(内部通过锁保护)
- 添加/删除客户端时加锁:确保
多线程环境下的回调安全
- 客户端线程模型适配
STA
客户端:事件回调需通过消息泵(Message Pump
)同步到主线程,避免跨线程调用UI
组件MTA
客户端:允许直接回调,但需客户端自行处理线程安全
- 避免死锁
- 锁粒度控制:确保在回调客户端方法前释放锁,防止客户端回调中再次请求同一锁
1 2 3 4 5 6 7 8 9 10 11 |
HRESULT Fire_OnValueChanged(int newValue) { // 复制订阅列表(加锁期间) CComAutoCriticalSectionLock lock(m_cs); CComDynamicUnkArray vecCopy = m_vec; // 复制列表 lock.Unlock(); // 遍历副本,避免持有锁调用客户端代码 for (int i = 0; i < vecCopy.GetSize(); i++) { // 调用客户端接口 } } |
- 客户端接口指针的生命周期
- 使用
CComPtr
管理指针:避免客户端在回调期间被释放 - 引用计数保护:在回调期间增加接口引用计数,防止客户端在回调中调用
Unadvise
导致指针失效
- 使用
Fire_XXX
- 在多播事件机制中,
Fire_XXX
函数(如Fire_OnValueChanged
)是COM
组件触发事件的核心方法,负责遍历所有订阅的客户端,并调用其事件接口的回调方法 - 以下是一个线程安全的
Fire_OnValueChanged
实现示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// 组件类定义 class CMyComponent : public CComObjectRootEx<CComMultiThreadModel>, // 多线程模型 public IConnectionPointImpl<CMyComponent, &IID_IMyEvent> { private: CComAutoCriticalSection m_cs; // 临界区保护订阅列表 public: // 触发事件的方法 HRESULT Fire_OnValueChanged(int newValue) { // 1. 加锁保护订阅列表 CComCritSecLock<CComAutoCriticalSection> lock(m_cs); // 2. 遍历所有订阅的客户端 int cConnections = m_vec.GetSize(); for (int i = 0; i < cConnections; i++) { // 3. 获取客户端的 IUnknown 指针 CComPtr<IUnknown> punkConnection = m_vec.GetAt(i); if (!punkConnection) continue; // 4. 查询事件接口 CComQIPtr<IMyEvent> spEvent = punkConnection; if (spEvent) { // 5. 调用客户端的回调方法 spEvent->OnValueChanged(newValue); } } return S_OK; } }; |
Fire_XXX
函数是 开发者自定义的成员函数,通常直接定义在 组件的类声明中(如CMyComponent
类)- 其作用是根据业务逻辑触发事件,遍历所有订阅的客户端并调用其回调方法
1 2 3 4 5 6 7 8 9 10 11 12 |
// MyComponent.h class CMyComponent : public CComObjectRootEx<CComMultiThreadModel>, public IConnectionPointImpl<CMyComponent, &IID_IMyEvent> { public: // 手动定义的 Fire_XXX 函数 HRESULT Fire_OnValueChanged(int newValue) { // 遍历订阅列表,调用客户端的 OnValueChanged 方法 // ... return S_OK; } }; |
Fire_XXX
的命名与接口关联- 函数名和参数应与事件接口定义一致
1 2 3 |
interface IMyEvent : IUnknown { HRESULT OnValueChanged([in] int newValue); }; |
1 |
HRESULT Fire_OnValueChanged(int newValue); |
- 简化开发的辅助宏:
BEGIN_CONNECTION_POINT_MAP
和END_CONNECTION_POINT_MAP
- 作用:声明组件支持的连接点列表,但 不生成
Fire_XXX
函数
- 作用:声明组件支持的连接点列表,但 不生成
1 2 3 |
BEGIN_CONNECTION_POINT_MAP(CMyComponent) CONNECTION_POINT_ENTRY(IID_IMyEvent) END_CONNECTION_POINT_MAP() |
- 简化开发的辅助宏:
IDispEventImpl
(基于调度的自动化事件)- 适用场景:若事件接口继承自
IDispatch
(自动化兼容),可以使用IDispEventImpl
自动生成部分代码
- 适用场景:若事件接口继承自
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class CMyComponent : public IDispEventImpl<1, CMyComponent, &IID_IMyEvent, &LIBID_MyLib> { public: BEGIN_SINK_MAP(CMyComponent) SINK_ENTRY_EX(1, IID_IMyEvent, DISPID_ONVALUECHANGED, OnValueChanged) END_SINK_MAP() // 自动生成的 Fire_OnValueChanged(通过 IDispatch 调用) // 注意:仍需手动调用此方法触发事件 void Fire_OnValueChanged(int newValue) { IDispEventSimpleImpl<1, CMyComponent, &IID_IMyEvent>::_DispatchCall( DISPID_ONVALUECHANGED, VT_I4, 1, &newValue); } }; |
ATL 与 WTL 结合开发 GUI
概述
ATL
提供了一套轻量级的窗口类模板,用于简化Win32
窗口的创建和管理。其核心目标是替代MFC
的窗口封装,减少代码冗余和运行时开销,同时支持COM
组件的无缝集成
CWindow
- 基础窗口封装
- 功能
- 封装窗口句柄(
HWND
),提供直接调用Win32
API
的方法(如ShowWindow
、MoveWindow
)
- 封装窗口句柄(
- 用途
- 适用于简单窗口操作,无需消息处理逻辑
1 2 3 |
CWindow wnd; wnd.Create(L"STATIC", NULL, CWindow::rcDefault, L"Hello ATL", WS_VISIBLE); wnd.ShowWindow(SW_SHOW); |
CWindowImpl
- 继承自
CWindow
,支持消息处理 - 功能
- 继承自
CWindow
,通过模板和宏实现消息映射(Message Map),类似 MFC 的BEGIN_MESSAGE_MAP
- 继承自
1 2 3 4 5 6 |
// 消息映射宏 BEGIN_MSG_MAP(CMyWindow) MESSAGE_HANDLER(WM_PAINT, OnPaint) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) END_MSG_MAP() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// 示例代码 class CMyWindow : public CWindowImpl<CMyWindow> { public: DECLARE_WND_CLASS(L"MyWindowClass") // 注册窗口类 BEGIN_MSG_MAP(CMyWindow) MESSAGE_HANDLER(WM_PAINT, OnPaint) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) END_MSG_MAP() LRESULT OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { PAINTSTRUCT ps; HDC hdc = BeginPaint(&ps); TextOut(hdc, 10, 10, L"Hello ATL Window!", 15); EndPaint(&ps); bHandled = TRUE; return 0; } LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { PostQuitMessage(0); bHandled = TRUE; return 0; } }; |
CDialogImpl
- 对话框封装
- 功能
- 简化对话框的创建和消息处理,支持资源模板(
.rc
文件中的对话框ID
)
- 简化对话框的创建和消息处理,支持资源模板(
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class CMyDialog : public CDialogImpl<CMyDialog> { public: enum { IDD = IDD_MYDIALOG }; // 对话框资源 ID BEGIN_MSG_MAP(CMyDialog) COMMAND_ID_HANDLER(IDOK, OnOK) COMMAND_ID_HANDLER(IDCANCEL, OnCancel) END_MSG_MAP() LRESULT OnOK(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { EndDialog(IDOK); return 0; } LRESULT OnCancel(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { EndDialog(IDCANCEL); return 0; } }; // 显示对话框 CMyDialog dlg; dlg.DoModal(); |
ATL窗口类优势
- 轻量级:
- 无
MFC
的庞大运行时库依赖,适合小型组件或性能敏感场景
- 无
COM
集成:- 可直接嵌入
COM
组件(如ActiveX
控件)
- 可直接嵌入
- 模板化设计:
- 通过
CRTP
(奇异递归模板模式)实现零成本抽象
- 通过
ActiveX
控件开发
- 概述
ActiveX
控件是基于COM
的可重用UI
组件,支持在多种容器(如网页、VB
、C#
)中嵌入ATL
提供CComControl
等模板类简化其开发
- 开发步骤如下:
- 创建
ATL
项目- 在
Visual Studio
中选择ATL Project
,勾选Support Control
选项 - 添加
ATL Control
向导生成控件框架
- 在
- 定义控件接口(
IDL
文件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// 控件接口 [ object, uuid(...), dual, pointer_default(unique) ] interface IMyControl : IDispatch { [propget, id(1)] HRESULT Value([out, retval] int* pVal); [propput, id(1)] HRESULT Value([in] int newVal); }; // 事件接口 [ object, uuid(...), pointer_default(unique) ] interface IMyControlEvents : IUnknown { [id(1)] HRESULT OnValueChanged([in] int newValue); }; // 类型库 library MyControlLib { importlib("stdole32.tlb"); [ uuid(...), control ] coclass MyControl { [default] interface IMyControl; [default, source] interface IMyControlEvents; // 事件源 }; }; |
- 实现控件类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
class ATL_NO_VTABLE CMyControl : public CComObjectRootEx<CComMultiThreadModel>, public CComControl<CMyControl>, public IMyControl, public IConnectionPointContainerImpl<CMyControl>, public IConnectionPointImpl<CMyControl, &IID_IMyControlEvents> { public: DECLARE_REGISTRY_RESOURCEID(IDR_MYCONTROL) BEGIN_COM_MAP(CMyControl) COM_INTERFACE_ENTRY(IMyControl) COM_INTERFACE_ENTRY(IConnectionPointContainer) END_COM_MAP() BEGIN_CONNECTION_POINT_MAP(CMyControl) CONNECTION_POINT_ENTRY(IID_IMyControlEvents) END_CONNECTION_POINT_MAP() // IMyControl 方法 STDMETHODIMP get_Value(int* pVal) override { *pVal = m_value; return S_OK; } STDMETHODIMP put_Value(int newVal) override { m_value = newVal; Fire_OnValueChanged(newVal); // 触发事件 return S_OK; } // 绘制控件 HRESULT OnDraw(ATL_DRAWINFO& di) { RECT& rc = *(RECT*)di.prcBounds; HDC hdc = di.hdcDraw; FillRect(hdc, &rc, (HBRUSH)COLOR_WINDOW); TextOut(hdc, 10, 10, L"MyControl", 9); return S_OK; } private: int m_value = 0; }; |
- 添加属性页(可选)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 属性页类 class CMyPropertyPage : public COlePropertyPage { DECLARE_DYNCREATE(CMyPropertyPage) DECLARE_OLECREATE_EX(CMyPropertyPage) // 对话框资源 ID enum { IDD = IDD_PROPPAGE_MYCONTROL }; // 数据交换 BEGIN_MSG_MAP(CMyPropertyPage) COMMAND_HANDLER(IDC_EDIT_VALUE, EN_CHANGE, OnValueChanged) END_MSG_MAP() LRESULT OnValueChanged(...) { // 更新控件属性 return 0; } }; |
- 注册于测试
- 编译生成
.ocx
文件,运行regsvr32 MyControl.ocx
注册控件 - 在
VB
、C#
或网页中测试:
- 编译生成
1 |
<object id="MyControl" classid="clsid:..."></object> |
ATL 对 ActiveX 的核心支持
类/模板 | 功能 |
CComControl |
提供控件基础功能(如绘制、窗口管理)。 |
IViewObjectEx |
实现控件的可视化(如 OnDraw 方法)。 |
IPersistStreamInit |
支持控件状态的序列化与反序列化。 |
IOleControl |
处理控件的焦点和键盘事件。 |
ATL 与 WTL 结合开发 GUI 示例
WTL的增强功能
- 高级控件:
- 提供
CListViewCtrl
、CTreeViewCtrl
等封装,简化复杂UI
开发
- 提供
- 布局管理:
- 支持
Docking
和Splitter
窗口(类似MFC
)
- 支持
- 消息链:
- 通过
CHAIN_MSG_MAP
实现消息路由到父窗口或子控件
- 通过
场景
ActiveX
控件:- 嵌入网页或传统桌面应用(如数据可视化、工业控制界面)
- 轻量级
GUI
工具:- 需要最小化依赖的配置工具或后台服务监控界面
COM
组件集成:- 在
COM
服务中嵌入UI
用于调试或状态展示
- 在
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class CMyWTLWindow : public CWindowImpl<CMyWTLWindow> { public: BEGIN_MSG_MAP(CMyWTLWindow) COMMAND_ID_HANDLER(ID_BUTTON_OK, OnOK) CHAIN_MSG_MAP_MEMBER(m_edit) // 将消息路由到子控件 END_MSG_MAP() LRESULT OnOK(...) { CString text; m_edit.GetWindowText(text); MessageBox(text); return 0; } private: CEdit m_edit; // WTL 的 CEdit 控件封装 }; |
COM 服务器类型与部署
进程内(DLL)服务器(In-Process Server
)
- 形式
- 动态链接库
DLL
- 动态链接库
- 运行方式
- 加载到 客户端进程的地址空间 中执行
- 特点
- 高性能:无跨进程通信开销,调用速度快。
- 低隔离性:若服务器崩溃,可能导致客户端进程崩溃。
- 共享资源:可直接访问客户端的内存和资源
- 适用场景
- 高性能要求的组件(如图形渲染、数学计算)
- 需要紧密集成的功能(如浏览器插件)
- 注册方式:
- 自注册逻辑:
DLL
需导出DllRegisterServer
和DllUnregisterServer
函数
- 自注册逻辑:
1 2 |
regsvr32 MyComponent.dll # 注册 regsvr32 /u MyComponent.dll # 注销 |
- 注册表项
1 2 3 4 5 6 7 8 9 10 11 12 13 |
HKCR { CLSID { {CLSID_MyComponent} = s 'MyComponent Class' { InprocServer32 = s '%MODULE%' { val ThreadingModel = s 'Apartment' } } } } |
进程外(EXE)
- 形式
- 可执行文件
EXE
- 可执行文件
- 运行方式
- 在 独立的进程 中运行,通过
RPC
(远程过程调用)与客户端通信
- 在 独立的进程 中运行,通过
- 特点
- 高隔离性:服务器崩溃不会影响客户端。
- 跨进程/机器支持:支持
DCOM
(分布式COM
),可远程调用。 - 通信开销:参数需列集(
Marshaling
),性能较低
- 适用场景
- 需要高稳定性的服务(如后台数据处理)。
- 分布式系统(如远程数据库访问)
- 注册方式:
- 运行
EXE
时附带注册参数 - 自注册逻辑:
EXE
需解析命令行参数,调用CoRegisterClassObject
注册类工厂
- 运行
1 2 |
MyComponent.exe /regserver # 注册 MyComponent.exe /unregserver # 注销 |
- 注册表项
1 2 3 4 5 6 7 8 9 10 |
HKCR { CLSID { {CLSID_MyComponent} = s 'MyComponent Class' { LocalServer32 = s '%MODULE%' } } } |
类型库(.tlb
)嵌入
- 作用
- 将类型库(
.tlb
)作为资源嵌入DLL
或EXE
,简化客户端开发
- 将类型库(
- 实现步骤如下:
- 添加资源文件:
- 在
.rc
文件中添加类型库资源:
- 在
1 |
IDR_TYPELIB TYPELIB "MyComponent.tlb" |
- 注册类型库
- 在
.rgs
注册脚本中引用资源ID
:
- 在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
HKCR { TypeLib { {LIBID_MyComponentLib} { 1.0 = s 'MyComponent Library' { FLAGS = s '0' { 0 = s '%MODULE%' } } } } } |
- 代码支持
- 使用
DECLARE_REGISTRY_RESOURCEID
宏关联资源ID
:
- 使用
1 |
DECLARE_REGISTRY_RESOURCEID(IDR_TYPELIB) |
实际应用场景
- 进程内服务器
- 图像处理库(如滤镜效果)
- 将
ImageFilter.dll
和嵌入的.tlb
分发给客户端 - 客户端通过
regsvr32
注册后直接调用接口
- 进程外服务器
- 财务计算服务(需长时间运行)
- 在服务器机器上注册
FinanceService.exe
- 客户端通过
DCOM
配置远程访问
- 类型库嵌入
1 2 3 4 5 6 7 |
// 引用嵌入的类型库 [ComImport, Guid("CLSID_MyComponent")] class MyComponent { } // 创建实例 dynamic obj = new MyComponent(); obj.Method(); |
免注册 COM(Registration-Free COM)
原理
- 通过清单文件(
.manifest
)描述COM
类信息,避免写入注册表
步骤
- 生成清单文件
- 使用
mt.exe
从DLL/EXE
提取COM
信息:
- 使用
1 |
mt.exe -dll MyComponent.dll -outputresource:MyComponent.dll;2 -out:MyComponent.manifest |
- 客户端清单
- 客户端应用清单中声明依赖:
1 2 3 4 5 |
<dependency> <dependentAssembly> <assemblyIdentity type="win32" name="MyComponent" version="1.0.0.0" /> </dependentAssembly> </dependency> |
优点
- 绿色部署(无需管理员权限)
- 避免注册表污染
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ C++_运算符优先级&&相关性12/15
- ♥ Dump分析:未捕获的异常,查看内存相关命令03/25
- ♥ STL_heap06/15
- ♥ Bkwin一12/01
- ♥ Dump分析:调试方法与实践,空指针访问03/15
- ♥ C++11_第五篇12/08