尝试使用COM
COM技术
Component Object Model(COM), 即组件对象模型, 是一种软件组件的二进制接口标准, 换句话说, COM是不受编程语言限制的标准.
设计模式中也有一种组件设计模式, 这种设计能提高代码重用性, 也能通过不同的功能组件来进行解耦.1
DirectX就是基于COM的, 虽然在刚接触的时候会感到编写基于COM的代码有些复杂, 但其实创建COM对象实例的代码是比较固定的.
理解COM接口
COM interface2, 即COM接口, 和Java中的interface
概念有些相似.
接口只需定义需要的函数, 而不用具体实现
Java中的接口:
interface ISome{
void do();
}
C++中相似的就是纯虚函数:
class SomeInterface{
public:
virtual void DoSomething() = 0;
};
如果使用struct
, 可以省略访问修饰符public
:
struct SomeInterface{
virtual void DoSomething() = 0;
};
也可以通过宏定义一个interface结构:
#define interface struct
interface IInterface{
virtual void Do() = 0;
};
combaseapi.h3文件中提供了COM的基本定义, 在该文件中通过宏定义了一个interface
:
#define __STRUCT__ struct
#define interface __STRUCT__
COM对象
COM对象是COM接口的具体实现, 一个COM对象可以实现不同的COM接口. 在what-is-a-com-interface中有简单举例:
class IDrawable
{
public:
virtual void Draw() = 0;
};
// An interface for serialization.
class ISerializable
{
public:
virtual void Load(PCWSTR filename) = 0; // Load from file.
virtual void Save(PCWSTR filename) = 0; // Save to file.
};
// Declarations of drawable object types.
class Shape : public IDrawable
{
...
};
class Bitmap : public IDrawable, public ISerializable
{
...
};
(以上代码仅用于理解COM概念, 并非实际代码!) ---- What Is a COM Interface?
这样, 使用COM对象的时候可以通过接口的指针调用函数, 而不用使用具体的实现对象指针:
IDrawable *pDrawable = CreateTriangleShape();
if (pDrawable)
{
pDrawable->Draw();
}
(代码来自what-is-a-com-interface)
使用COM
在使用COM之前和之后有一些必要的操作: 初始化COM库和卸载COM库.
初始化COM库
任何程序使用COM接口前都需要对COM库进行初始化4, COM库提供了CoInitializeEx
5函数来进行COM初始化.
并且, 每个线程如果调用COM接口, 也都需要分别调用一次CoInitializeEx
CoInitializeEx
函数如下:
HRESULT CoInitializeEx(
LPVOID pvReserved,
DWORD dwCoInit
);
一些相关说明:
- 第一个参数是预留参数, 必须为
NULL
- 第二个参数跟线程模式有关: 单元线程(apartment threaded)和多元线程(multithreaded)
Flag | Desc |
---|---|
COINIT_APARTMENTTHREADED | 单元线程 |
COINIT_MULTITHREADED | 多元线程 |
如果使用单元线程, 需要线程满足一些条件:
- 只能从单个线程访问COM对象
- 线程中有消息循环
- 不会在线程间共用COM接口指针(实际上这一点没有太严格, 可以通过COM 封送技术6来共享)
调用CoInitializeEx
:
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
另外Initialize COM Library中建议多设置一个Flag: COINIT_DISABLE_OLE1DDE
这个Flag设置之后, 减少了关于OLE1.0技术的相关开销, OLE1.0是个过时的技术.
所以也可以写为:
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
卸载COM库
每次CoInitializeEx调用成功后, 都需要在之后调用CoUninitialize
函数.
CoUninitialize这个函数没有形参也没有返回值.
换句话说, 每个调用了一次CoInitializeEx
的线程都需要调用一次CoUninitialize
函数, 这两个函数是成对调用的.
//......
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
if(SUCCESS(hr)){
//......
CoUninitialize();
}
//......
创建COM对象实例的方式
创建COM对象实例有两种方式:
- COM提供的通用的函数
CoCreateInstance
- 基于COM实现的库中提供的一些函数, 用来创建库中COM对象实例.
CoCreateInstance
CoCreateInstance7函数如下:
HRESULT CoCreateInstance(
REFCLSID rclsid,
LPUNKNOWN pUnkOuter,
DWORD dwClsContext,
REFIID riid,
LPVOID *ppv
);
COM的实现中每个COM对象和COM接口都各自有一个标识数来唯一标识它们自己,
这个标识数------即ID, 在COM中是全局唯一的, 也叫全局 / 通用唯一标识符(GUID / UUID),
全称:globally / universally unique identifier
一些说明:
参数ppv
接收一个地址(指针), 对应riid中请求的接口的指针变量的地址。失败时对应的地址为NULL.
IWICImagingFactory *spWICFactory;
if(FAILED(CoCreateInstance(
CLSID_WICImagingFactory,
NULL,
CLSCTX_INPROC_SERVER,
IID_IWICImagingFactory,
reinterpret_cast<void **>(&spWICFactory)
)){
//spWICFactory is NULL here.
}
REFCLSID
参考guiddef.h8, REFCLSID其实是GUID
的引用类型, guiddef.h中对GUID
的定义如下:
typedef struct _GUID {
unsigned long Data1;
unsigned short Data2;
unsigned short Data3;
unsigned char Data4[ 8 ];
} GUID;
可以看到GUID是一个128-bit的数值数据对象(结构体), 而REFCLSID定义如下:
typedef GUID IID;
//......
#ifndef _REFCLSID_DEFINED
#define _REFCLSID_DEFINED
#ifdef __cplusplus
#define REFCLSID const IID &
#else
#define REFCLSID const IID * __MIDL_CONST
#endif
#endif
//......
所以CLSID
也是代码上看也是GUID
. CLSID
即Class ID, 用来标识一个COM对象.
CoCreateInstance
第一个参数传入一个实现了COM接口的COM对象的Class ID.
REFIID
REFIID
即对IID的引用, 也是一个GUID的引用类型:
#ifndef _REFIID_DEFINED
#define _REFIID_DEFINED
#ifdef __cplusplus
#define REFIID const IID &
#else
#define REFIID const IID * __MIDL_CONST
#endif
#endif
不过IID
, 指Interface ID, 而CLSID
指的是Class ID.
CoCreateInstance
第四个参数传入一个COM接口的Interface ID.
参数dwClsContext
这个参数标明了创建函数的执行的上下文(即执行环境), 需要传入CLSCTX9一个枚举类型对象:
typedef enum tagCLSCTX {
CLSCTX_INPROC_SERVER,
CLSCTX_INPROC_HANDLER,
CLSCTX_LOCAL_SERVER,
CLSCTX_INPROC_SERVER16,
CLSCTX_REMOTE_SERVER,
CLSCTX_INPROC_HANDLER16,
CLSCTX_RESERVED1,
CLSCTX_RESERVED2,
CLSCTX_RESERVED3,
CLSCTX_RESERVED4,
CLSCTX_NO_CODE_DOWNLOAD,
CLSCTX_RESERVED5,
CLSCTX_NO_CUSTOM_MARSHAL,
CLSCTX_ENABLE_CODE_DOWNLOAD,
CLSCTX_NO_FAILURE_LOG,
CLSCTX_DISABLE_AAA,
CLSCTX_ENABLE_AAA,
CLSCTX_FROM_DEFAULT_CONTEXT,
CLSCTX_ACTIVATE_X86_SERVER,
CLSCTX_ACTIVATE_32_BIT_SERVER,
CLSCTX_ACTIVATE_64_BIT_SERVER,
CLSCTX_ENABLE_CLOAKING,
CLSCTX_APPCONTAINER,
CLSCTX_ACTIVATE_AAA_AS_IU,
CLSCTX_RESERVED6,
CLSCTX_ACTIVATE_ARM32_SERVER,
CLSCTX_PS_DLL
} CLSCTX;
这里取Creating-an-object-in-com中枚举出的四个Flag10:
Flag | Desc |
---|---|
CLSCTX_INPROC_SERVER | 与应用程序在同一个计算机, 同一个进程中 |
CLSCTX_LOCAL_SERVER | 与应用程序在同一个计算机, 不同的进程中 |
CLSCTX_REMOTE_SERVER | 与应用程序在不同的计算机中 |
CLSCTX_ALL | 由函数选择, 使用最有效率的选项 |
各Flag效率排序(高->低) : 同进程, 不同进程, 不同计算机
最简单的情况下, 不需要考虑跨进程和跨计算机, 使用CLSCTX_INPROC_SERVER
是没问题的.
pUnkOuter
这个参数可以为NULL或者传入一个聚合对象的IUnknown
接口指针.
如果调用CoCreateInstance
创建的COM对象实例是一个聚合对象的一部分的话,
比如在外部组件中通过CoCreateInstance创建内部组件
就需要传入聚合对象相应的IUnknown
接口指针. 否则就传入NULL值.
聚合–Aggregation
IUnKnown接口
即使是最简单的组件, 实际上也还需要直接或者间接继承一个IUnKnown
接口11, IUnknown
接口是由COM提供的基础接口, 可以用来获取聚合对象(Aggregation)12的其他接口, 为了实现该功能, IUnknown
接口提供了几个函数.
其中一个函数是QueryInterface
:
HRESULT QueryInterface(
REFIID riid,
void **ppvObject
);
可以简单理解它的作用, 类似于如下代码:
STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject){
if(riid == IID_INNER_1)(CInner1 *)*ppvObject = get_pCInner1();
else if(riid == IID_INNER_2)(CInner2 *)*ppvObject = get_pCInner2();
//else if(......) ......
if(*ppvobject == NULL) return ResultFromScode(E_NOINTERFACE);
return NOERROR;
}
聚合
如果一个(外部)组件他想重用(内部)组件, 有几种方式:
- 使用继承
- 一种方式是在(外部)组件添加函数以支持(内部)组件功能,
- 另一种方式是直接返回内部组件的指针
(继承可能是比较常见的方式, 不过它也不是完美的解决方案13)
😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃😃
这最后一种方式就是聚合. 聚合依赖于IUnKnown
接口的实现, 通过IUnKnown
接口可以返回内部组件的指针. 但是如果直接如以下代码一样直接返回(内部)组件指针, 会存在问题:
STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject){
if(riid == IID_INNER_1)(CInner1 *)*ppvObject = pCInner1;
else if(riid == IID_INNER_2)(CInner2 *)*ppvObject = pCInner2;
//else if(......) ......
if(*ppvobject == NULL) return ResultFromScode(E_NOINTERFACE);
return NOERROR;
}
内部组件的指针会暴露出来, 如果此时用对内部组件调用QueryInterface
会导致获取到的其实是内部组件的组件, 而不是外部组件的组件14.
内部组件和外部组件都是组件
为了解决这个问题, 需要在组件中通过对外部组件的IUnKnown
接口指针调用14, 类似于:
class Component : public IUnKnown{
//......
IUnKnown* pUnOuter;
STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject){
return pUnOuter->QueryInterface(riid, ppvobject);
}
};
[REF:Game Dev] ↩︎
https://docs.microsoft.com/en-us/windows/win32/learnwin32/what-is-a-com-interface- ↩︎
combaseapi.h ↩︎
https://docs.microsoft.com/en-us/windows/win32/learnwin32/initializing-the-com-library ↩︎
https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coinitializeex ↩︎
https://docs.microsoft.com/zh-cn/cpp/atl/marshaling?view=vs-2019 ↩︎
https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance ↩︎
guiddef.h ↩︎
https://docs.microsoft.com/zh-cn/windows/win32/api/wtypesbase/ne-wtypesbase-clsctx ↩︎
https://docs.microsoft.com/en-us/windows/win32/learnwin32/creating-an-object-in-com ↩︎
https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iunknown ↩︎
https://docs.microsoft.com/en-us/windows/win32/com/aggregation ↩︎
https://docs.microsoft.com/en-us/windows/win32/com/reusing-objects ↩︎
http://www.dfwlt.com/history/data/journal/mycmq/19615.html ↩︎ ↩︎