使用 WRL 创建和使用经典 COM 组件






4.73/5 (14投票s)
概述了使用 Windows Runtime C++ 模板库为桌面应用程序创建和使用经典 COM 组件
概述
Windows 8 引入了一类新的应用程序,称为 Windows 应用商店应用程序。这些应用程序不在桌面上运行,而是在一个称为 Windows Runtime(也称为 WinRT)的新环境中运行。Windows Runtime 通常用不同的词语来描述:“平台同构应用程序架构”、“运行时”、“层”、“对象模型”、“一组库”等。它本质上是一个与 Win32 并存的新的运行时引擎,由多层(服务、设备、媒体、通信等,当然还有 UI)组成。其 API 以 C++ 编写,并设计为异步的。它们本质上是基于 COM 的,尽管并非所有经典 COM 功能都可用(例如,IDispatch
)。Windows Runtime 组件和应用程序可以使用多种语言和技术来构建:C++、C#、VB.NET、JavaScript/HTML/CSS。在 C++ 中,可以使用 C++ 组件扩展(也称为 C++/CX)—— 类似于 C++/CLI 的语言扩展 —— 来创建和使用组件,或者使用 Windows Runtime C++ 模板库(也称为 WRL)—— 这是一个具有低级支持的模板库,包括引用计数或测试 HRESULT 值。C++/CLI 和 WRL 之间的一个重要区别是,前者将 COM HRESULT
表示为异常,而后者使用 HRESULT
值并且不抛出异常。
然而,鲜为人知的是,WRL 可以用于创建和使用经典 COM 组件用于桌面应用程序。WRL 通常与 ATL(Active Template Library)进行比较,它与 ATL 共享工厂、接口显式注册、模块等概念。但是,WRL
是一个轻量级库,主要用于支持 Windows Runtime 组件的需求。它不支持 COM 功能,如:IDispatch
(或双接口)、OLE 嵌入、ActiveX 控件、聚合、拆分接口、连接点、标准实现或 COM+。如果您需要使用任何这些功能,仍然应该使用 ATL 进行开发。
WRL 提供了表示基本 COM 概念的几种类型。它们包含在 <wrl.h>
(在 Microsoft::WRL
命名空间下)的几个头文件中。其中包括 ComPtr<T>
(表示 interface T
的智能指针类型)、RuntimeClass
(表示继承了一个或多个接口、实现了 IUnknown
方法并帮助管理模块总体引用计数的已实例化类)和 Module
(对象集合),我将在后续段落中使用它们来创建和使用一些经典 COM 组件。
问题
在本文的第一部分,我将实现一个提供电器对象的 COM 服务器。电器可以是洗碗机、微波炉、收音机或电视机。所有这些电器都有共同的功能,如开关机。因此,它们都将实现一个名为 IAppliance
的公共接口,该接口定义了两个方法:TurnOn
和 TurnOff
。然而,其中一些电器更智能,支持额外的功能,如远程控制。这些电器还将实现一个名为 ISmartAppliances
的接口,该接口定义了两个方法:RemoteTurnOn
和 RemoteTurnOff
。我将一步一步地指导您完成从定义接口到提供服务器的自注册过程。在文章的第二部分,我将创建一个实例化电器并与其交互的客户端。
使用 WRL 创建 COM 服务器
让我们从创建一个新的 Visual Studio 空解决方案开始,然后添加一个 DLL 类型的 Win32
项目。我们将把这个项目命名为 AppliancesServer
。
首先要做的是定义前面提到的接口。有几种方法可以做到这一点。可以将它们定义为常规的 abstract
类,并用 __declspec(uuid())
进行装饰,以将 GUID 与它们关联起来,或者我们可以使用 IDL 文件,就像您可能习惯的 ATL 开发一样。我将采用这种方法,因为我预计它对大多数 COM 开发人员来说都很熟悉。所以,让我们添加一个名为 Appliances.idl 的文件,内容如下:
import "oaidl.idl";
import "ocidl.idl";
[uuid(D0A11BFA-77D9-4073-B5AB-835EAE0B53EC)]
interface IAppliance : IUnknown
{
HRESULT TurnOn();
HRESULT TurnOff();
}
[uuid(D59DA186-20E7-47BF-931B-2AA9178424D7)]
interface ISmartAppliance : IUnknown
{
HRESULT RemoteTurnOn();
HRESULT RemoteTurnOff();
}
[uuid(313CD489-E503-43CE-880F-4B8D64DD3D9E)]
library ApplianceLibrary
{
[uuid(ABD628DC-EC82-4751-99F8-A68219B196CA)]
coclass TVSet
{
[default] interface IAppliance;
interface ISmartAppliance;
}
}
构建项目时,.idl 文件将与 midl.exe 编译器一起编译,后者会生成几个文件(请注意,它们不会自动添加到您的项目中):
- Appliances_h.h:包含 IDL 中定义的所有接口和 coclass 的类型定义和函数声明的头文件
- Appliances_i.c:定义 IDL 文件中所有接口和 coclass 的 GUID
- Appliances_p.c:用于客户端和服务器的代理/存根文件,包含代理/存根入口点
- dlldata.c:定义创建代理/存根 DLL 所必需的实体
就本文而言,我们将只使用 Appliances_h.h 头文件,而忽略其余部分。
定义好接口后,我们就可以开始创建实现这些接口的电器了。让我们向项目中添加另一个文件,名为 Appliances.cpp。在定义一个 appliance
类之前,我们必须包含前面生成的 Appliances_h.h 头文件以及用于 Windows Runtime C++ 模板库的 <wrl.h>
。我将定义一个名为 TVSet
的单一电器对象。这是一个智能电器,实现了 IAppliance
和 ISmartAppliance
。该文件的内容如下:
#include "Appliances_h.h"
#include <wrl.h>
using namespace Microsoft::WRL;
class TVSet : public RuntimeClass<RuntimeClassFlags<ClassicCom>, IAppliance, ISmartAppliance>
{
public:
virtual HRESULT __stdcall TurnOn() override
{
OutputDebugString(L"TV was turned on\n");
return S_OK;
}
virtual HRESULT __stdcall TurnOff() override
{
OutputDebugString(L"TV was turned off\n");
return S_OK;
}
virtual HRESULT __stdcall RemoteTurnOn() override
{
OutputDebugString(L"TV was turned on with remote control\n");
return S_OK;
}
virtual HRESULT __stdcall RemoteTurnOff() override
{
OutputDebugString(L"TV was turned off with remote control\n");
return S_OK;
}
};
CoCreatableClass(TVSet);
这里有几点需要注意:
TVSet
继承自RuntimeClass
,它提供了核心的 COM 组件支持,如实现AddRef
、Release
和QueryInterface
(来自IUnknown
的方法)。这是一个模板类。第一个参数是一个无符号整数,指定类标志,并为了表明这是一个经典的 COM 组件,我们必须提供RuntimeClassFlags<ClassicCom>
。其他类型参数(在当前实现中最多九个)是此运行时类实现的接口。IAppliance
和ISmartAppliance
接口的实现被简化到最少,只是在调试器的输出窗口中显示文本。CoCreatableClass(TVSet)
宏为TVSet
类定义了一个类工厂。当 COM 服务器客户端请求时,这个类工厂将实例化TVSet
对象。WRL 定义了一个SimpleClassFactory
模板类,它基本上实现了IClassFactory
接口的方法CreateInstance
和LockServer
(除了IUnknown
方法,因为IClassFactory
像任何 COM 接口一样继承自IUnknown
)。
下一步是定义和实现 COM 服务器的实际导出。必需的最少导出有:
DllRegisterServer
:COM 服务器添加到 Windows 注册表中以支持其类和类对象的必要条目的入口点。当您执行 regsvr32.exe 并传递一个模块时,它会从模块中找到并调用此函数。DllUnregisterServer
:COM 服务器用于从 Windows 注册表中删除DllRegisterServer
之前添加的条目的入口点。当您执行regsvr32.exe /u
并传递一个模块时,它会从模块中找到并调用此函数。DllGetClassObject
:检索由提供的 CLSID 指定的类对象(工厂)。这由CoGetClassObject
调用,用于实例化类对象,或由CoCreateInstance
/CoCreateInstanceEx
调用,用于创建单个 COM 对象(本地或远程)。DllCanUnloadNow
:确定 DLL 是否仍在管理对象。如果其对象总引用计数为0
,则 DLL 可以被调用者从内存中卸载。
要定义这些导出,请向项目添加一个模块定义文件(命名为 Appliances.def),并定义以下条目(请注意,这里的 PRIVATE
意味着这些函数不应在 .lib 文件中可见,因此静态链接 .lib 文件的客户端可能看不到并调用这些函数)。
EXPORTS
DllGetClassObject PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
DllCanUnloadNow PRIVATE
这些导出的实现是在 DllMain.cpp 中完成的。如入门段落所述,WRL 提供了一个名为 Module
的类,它代表一个 COM 服务器模块。虽然其功能被保持在最低限度,并且某些方法实际上未实现,但它确实提供了两个方法来帮助实现 DllGetClassObject
和 DllCanUnloadNow
。这些方法是 GetClassObject
(检索实现 IClassFactory
的类对象)和 GetObjectCount
(检索模块管理的对象的数量)。因此,这些导出的实现可能看起来像这样:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, _COM_Outptr_ void** ppv)
{
return Module<InProc>::GetModule().GetClassObject(rclsid, riid, ppv);
}
STDAPI DllCanUnloadNow()
{
return Module<InProc>::GetModule().Terminate() ? S_OK : S_FALSE;
}
不幸的是,Module
类不支持自注册,因此 DllRegisterServer
和 DllUnregisterServer
的实现需要更多的工作。为了将功能分组并使导出具有更清晰的实现,我想定义一个 module
类来在内部处理这些操作,而导出函数只调用它们。您可以继续向项目添加一个新类,名为 AppliancesModule
。其声明如下:
class AppliancesModule
{
// additional private methods
public:
HRESULT RegisterServer();
HRESULT UnregisterServer();
HRESULT GetClassObject(REFCLSID rclsid, REFIID riid, void** ppv);
HRESULT CanUnloadNow();
};
在 DllMain.cpp 中,我们定义了这个类型的 static
对象,并实现导出以使用它。
#include <windows.h>
#include "AppliancesModule.h"
static AppliancesModule s_Module;
HRESULT __stdcall DllRegisterServer()
{
return s_Module.RegisterServer();
}
HRESULT __stdcall DllUnregisterServer()
{
return s_Module.UnregisterServer();
}
HRESULT __stdcall DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv)
{
return s_Module.GetClassObject(rclsid, riid, ppv);
}
HRESULT __stdcall DllCanUnloadNow()
{
return s_Module.CanUnloadNow();
}
BOOL __stdcall DllMain(HINSTANCE module, DWORD reason, void*)
{
if(reason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(module);
}
return TRUE;
}
此时应该清楚 GetClassObject
和 CanUnloadNow
如何实现。至于 RegisterServer
和 UnregisterServer
,事情就变得更复杂了。但是,我会尽量将细节保持在最低限度,因为它们基本上超出了本文的范围。只要在 HKEY_LOCAL_MACHINE
注册表项下的 Software\Classes\CLSID 键中有几个条目,就可以从客户端实例化一个类。
对于每个管理的 objRequest,模块都必须创建一个具有类 CLSID
的键,并提供其类名。InProcServer32
子键是必不可少的,因为在该键下,我们必须定义模块的路径和线程模型。其他子键,如 TypeLib
和 Version
是可选的,运行时在没有它们的情况下也能正常工作。我将定义一个名为 RegistryEntry
的类,它为单个类提供所有这些信息。该模块将使用此类条目的静态数组来添加和删除键值到 Registry
。
struct RegistryEntry
{
wchar_t const * Guid;
wchar_t const * Name;
wchar_t const * ThreadingModel;
wchar_t const * TypelibGuid;
wchar_t const * Version;
};
以下是 ApplicationModule
类的实现:
#include "AppliancesModule.h"
#include "Registration.h"
#include <string>
#include <KtmW32.h>
#pragma comment(lib, "KtmW32")
#include <wrl\module.h>
using namespace Microsoft::WRL;
EXTERN_C IMAGE_DOS_HEADER __ImageBase;
static RegistryEntry s_regTable [] = {
{
L"{ABD628DC-EC82-4751-99F8-A68219B196CA}",
L"TVSet",
L"Apartment",
L"{313CD489-E503-43CE-880F-4B8D64DD3D9E}",
L"1.0"
},
};
class registry_handle
{
HKEY handle;
public:
registry_handle(HKEY const & key): handle(key)
{
}
registry_handle(registry_handle&& rh)
{
handle = rh.handle;
rh.handle = nullptr;
}
~registry_handle()
{
if(handle != nullptr)
RegCloseKey(handle);
}
registry_handle& operator=(registry_handle&& rh)
{
if(this != &rh)
{
if(handle != nullptr)
RegCloseKey(handle);
handle = rh.handle;
rh.handle = nullptr;
}
return *this;
}
HKEY* get() throw()
{
return &handle;
}
operator HKEY() const
{
return handle;
}
};
registry_handle RegistryCreateKey(wchar_t const * keyPath, HANDLE hTransaction)
{
registry_handle hKey = nullptr;
auto result = ::RegCreateKeyTransacted(
HKEY_LOCAL_MACHINE,
keyPath,
0,
nullptr,
REG_OPTION_NON_VOLATILE,
KEY_WRITE,
nullptr,
hKey.get(),
nullptr,
hTransaction,
nullptr);
if (ERROR_SUCCESS != result)
{
SetLastError(result);
hKey = nullptr;
}
return const_cast<registry_handle&&>(hKey);
}
bool RegistryCreateNameValue(HKEY hKey, wchar_t const * name, wchar_t const * value)
{
auto result = ::RegSetValueEx(
hKey,
name,
0,
REG_SZ,
reinterpret_cast<BYTE const*>(value),
static_cast<DWORD>(sizeof(wchar_t)*(wcslen(value) + 1)));
if (ERROR_SUCCESS != result)
{
::SetLastError(result);
return false;
}
return true;
}
bool RegistryDeleteTree(wchar_t const * keyPath, HANDLE hTransaction)
{
registry_handle hKey = nullptr;
auto result = ::RegOpenKeyTransacted(
HKEY_LOCAL_MACHINE,
keyPath,
0,
DELETE | KEY_ENUMERATE_SUB_KEYS | KEY_QUERY_VALUE | KEY_SET_VALUE,
hKey.get(),
hTransaction,
nullptr);
if (ERROR_SUCCESS != result && ERROR_FILE_NOT_FOUND != result)
{
SetLastError(result);
return false;
}
if(ERROR_SUCCESS == result)
{
result = ::RegDeleteTree(hKey, nullptr);
if (ERROR_SUCCESS != result)
{
RegCloseKey(hKey);
::SetLastError(result);
return false;
}
}
RegCloseKey(hKey);
return true;
}
bool AppliancesModule::Unregister(HANDLE hTransaction)
{
for(auto const & entry : s_regTable)
{
if(!RegistryDeleteTree((std::wstring(L"Software\\Classes\\CLSID\\") + entry.Guid).data(),
hTransaction))
return false;
}
return true;
}
bool AppliancesModule::Register(HANDLE hTransaction)
{
if(!Unregister(hTransaction))
return false;
wchar_t filename[MAX_PATH] = { 0 };
auto const length = ::GetModuleFileName(
reinterpret_cast<HMODULE>(&__ImageBase),
filename,
_countof(filename));
if(length == 0)
return false;
for(auto const & entry : s_regTable)
{
auto keyPath = std::wstring(L"Software\\Classes\\CLSID\\") + entry.Guid;
registry_handle hKey = RegistryCreateKey(keyPath.data(), hTransaction);
if(hKey == nullptr)
return false;
if(!RegistryCreateNameValue(hKey, nullptr, entry.Name))
return false;
registry_handle hKey2 =
RegistryCreateKey((keyPath + L"\\InProcServer32").data(), hTransaction);
if(hKey2 == nullptr)
return false;
if(!RegistryCreateNameValue(hKey2, nullptr, filename))
return false;
if(!RegistryCreateNameValue(hKey2, L"ThreadingModel", entry.ThreadingModel))
return false;
if(entry.TypelibGuid != nullptr)
{
registry_handle hKey3 =
RegistryCreateKey((keyPath + L"\\TypeLib").data(), hTransaction);
if(hKey3 == nullptr)
return false;
if(!RegistryCreateNameValue(hKey3, nullptr, entry.TypelibGuid))
return false;
}
if(entry.Version != nullptr)
{
registry_handle hKey3 =
RegistryCreateKey((keyPath + L"\\Version").data(), hTransaction);
if(hKey3 == nullptr)
return false;
if(!RegistryCreateNameValue(hKey3, nullptr, entry.Version))
return false;
}
}
return true;
}
HRESULT AppliancesModule::RegisterServer()
{
HANDLE hTransaction = ::CreateTransaction(
nullptr, // security attributes
nullptr, // reserved
TRANSACTION_DO_NOT_PROMOTE, // options
0, // isolation level (reserved)
0, // isolation flags (reserved)
INFINITE, // timeout
nullptr // transaction description
);
if(INVALID_HANDLE_VALUE == hTransaction)
{
auto lastError = ::GetLastError();
::CloseHandle(hTransaction);
return HRESULT_FROM_WIN32(lastError);
}
if(!Register(hTransaction))
{
auto lastError = ::GetLastError();
::CloseHandle(hTransaction);
return HRESULT_FROM_WIN32(lastError);
}
if(!::CommitTransaction(hTransaction))
{
auto lastError = ::GetLastError();
::CloseHandle(hTransaction);
return HRESULT_FROM_WIN32(lastError);
}
::CloseHandle(hTransaction);
return S_OK;
}
HRESULT AppliancesModule::UnregisterServer()
{
HANDLE hTransaction = ::CreateTransaction(
nullptr, // security attributes
nullptr, // reserved
TRANSACTION_DO_NOT_PROMOTE, // options
0, // isolation level (reserved)
0, // isolation flags (reserved)
INFINITE, // timeout
nullptr // transaction description
);
if(INVALID_HANDLE_VALUE == hTransaction)
{
auto lastError = ::GetLastError();
::CloseHandle(hTransaction);
return HRESULT_FROM_WIN32(lastError);
}
if(!Unregister(hTransaction))
{
auto lastError = ::GetLastError();
::CloseHandle(hTransaction);
return HRESULT_FROM_WIN32(lastError);
}
if(!::CommitTransaction(hTransaction))
{
auto lastError = ::GetLastError();
::CloseHandle(hTransaction);
return HRESULT_FROM_WIN32(lastError);
}
::CloseHandle(hTransaction);
return S_OK;
}
HRESULT AppliancesModule::GetClassObject(REFCLSID rclsid, REFIID riid, void** ppv)
{
return Module<InProc>::GetModule().GetClassObject(rclsid, riid, ppv);
}
HRESULT AppliancesModule::CanUnloadNow()
{
return Module<InProc>::GetModule().Terminate() ? S_OK : S_FALSE;
}
注册函数首先尝试通过调用注销来清理注册表,然后继续为静态数组中的每个条目添加(以及一些可选的)必需的键和值到注册表。注销操作执行一个简单的键树删除(它会删除所有子键及其值,但不会删除我们不关心的键本身)。然而,注册表条目的管理是以事务性的方式进行的,因此如果出现问题,注册表中不会留下任何残余。CreateTransaction
函数创建一个事务对象,该对象稍后可用于 RegCreateKeyTransacted
、RegOpenKeyTransacted
或 RegDeleteKeyTransacted
。在成功地向 Windows 注册表添加或删除条目后,通过调用 CommitTranscation
来提交事务。如果出现任何错误,则通过简单地调用 CloseHandle
(它会对未提交的事务句柄执行回滚)来回滚事务。
更新:registry_handle
是一个注册表键句柄的智能句柄,它在对象超出作用域时自动关闭其包装的句柄。
有了所有这些,服务器就准备好了。我们可以编译项目,并在以管理员身份运行的控制台中运行 regsvr32.exe
AppliancesServer.dll。
使用 WRL 使用 COM 对象
使用上面创建的电器对象,或者任何其他 COM 对象,通过 Windows Runtime C++ 模板库的另一个模板类 ComPtr<T>
来完成,这变得很容易。这是一个智能指针,它包装了 COM 接口的原始指针,并执行内部的簿记工作,如维护引用计数。
构造函数允许创建一个空对象或从指定的 COM 接口原始指针创建对象(还有复制、移动和转换构造函数)。可以通过调用 Detach
将 ComPtr
对象与它包装的 COM 接口分离,或者通过调用 Attach
将 ComPtr
对象附加到新的 COM 接口(如果对象已与 COM 接口关联,则先分离再附加到新的接口)。还可以通过调用 Swap
来交换包装的 COM 接口与另一个 ComPtr
的 COM 接口。要检索包装的 COM 接口的原始指针,请使用 Get
方法。要检索包含 COM 接口指针的成员的地址,请调用 GetAddressOf
。一个类似的方法,ReleaseAndGetAddressOf
,首先释放 COM 接口,然后返回包含 COM 接口(在这种情况下是 null
)指针的成员的地址。要复制当前或指定的由引用的 COM 对象实现的接口,请使用 CopyTo
、As
或 AsIID
。
为了说明这一点,我将创建一个 ComPtr<IAppliance>
来管理 IAppliance
接口的指针,并使用它来打开电器。然后,我将创建一个 ComPtr<ISmartAppliance>
来管理同一个对象的 ISmartAppliance
接口的指针,并使用它来远程关闭电器。但在此之前,向解决方案添加一个 Win32 控制台应用程序,并将其命名为 AppliancesClient
。
有两件事我们必须不要忘记包含:包含电器 COM 服务器的 COM 接口定义的 Appliances_h.h 头文件,以及用于 Windows Runtime C++ 模板库的 <wrl.h>
。
#include "..\AppliancesServer\Appliances_h.h"
#include <wrl.h>
using namespace Microsoft::WRL;
在初始化 COM 运行时之前,我们无法使用 COM 库。为了简化对 CoInitializeEx
和 CoUninitialize
的调用,我将创建一个名为 RuntimeContext
的包装类,该类在构造函数中初始化 COM 运行时,并在析构函数中注销它。在 main()
函数中,我将声明这个上下文类的一个对象来处理运行时初始化。
class RuntimeContext
{
HRESULT hr;
public:
explicit RuntimeContext(DWORD const flags)
{
hr = ::CoInitializeEx(nullptr, flags);
}
~RuntimeContext()
{
if(hr == S_OK)
::CoUninitialize();
}
operator HRESULT() const
{
return hr;
}
};
客户端应用程序的主函数相对简单:初始化运行时,创建一个空的 ComPtr<IAppliance>
对象,然后创建 TVSet
对象并将检索到的指向 IAppliance
接口的指针与之关联。使用智能指针,我们可以打开电器。然后,我们定义另一个空的智能指针,但用于 ISmartAppliance
接口,并将指向该接口的指针与现有的 TVSet
对象关联起来。使用第二个智能指针,我们可以远程关闭电器。
int main()
{
RuntimeContext runtime(COINIT_APARTMENTTHREADED);
if(S_OK != runtime)
return -1;
ComPtr<IAppliance> tvset;
auto hr = ::CoCreateInstance(
__uuidof(TVSet),
nullptr,
CLSCTX_INPROC_SERVER,
__uuidof(tvset),
reinterpret_cast<void**>(tvset.GetAddressOf()));
if(hr == S_OK)
{
tvset->TurnOn();
ComPtr<ISmartAppliance> smarttv;
hr = tvset.As(&smarttv);
smarttv->RemoteTurnOff();
}
return 0;
}
请注意这行代码:
hr = tvset.As(&smarttv);
使用 CopyTo
方法可以获得相同的结果。但我更喜欢前者,因为它输入更少,而且我觉得它更易读。
hr = tvset.CopyTo(smarttv.GetAddressOf());
如果您在没有附加调试器的情况下运行客户端应用程序,控制台中将不会打印任何消息,因为电器方法的实现使用 OutputDebugString
,它在调试器的输出窗口中打印一个 string
。如果您在附加调试器的情况下运行,您将在输出窗口中看到以下文本:
TV was turned on
TV was turned off with remote control
结论
本文的目的是展示 Windows Runtime C++ 模板库(简称 WRL)不仅可以用于创建和使用 Windows Runtime 组件,还可以用于创建和使用用于桌面应用程序的经典 COM 组件。该库提供了几种类(本文中展示了一些),使开发人员能够创建和使用 COM 对象(无论是 Windows Runtime 组件还是经典组件)。但请注意,该库实际上是为 Windows Runtime 构建的,不支持所有经典 COM 场景。像双接口(IDispatch
)、聚合、连接点或 ActiveX 控件等功能是不支持的。如果您的 COM 组件需要任何这些功能,您应该使用 ATL。
历史
- 2013 年 9 月 17 日:初始版本