尝试使用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库提供了CoInitializeEx5函数来进行COM初始化.

并且, 每个线程如果调用COM接口, 也都需要分别调用一次CoInitializeEx

CoInitializeEx函数如下:

HRESULT CoInitializeEx(
  LPVOID pvReserved,
  DWORD  dwCoInit
);

一些相关说明:

  • 第一个参数是预留参数, 必须为NULL
  • 第二个参数跟线程模式有关: 单元线程(apartment threaded)和多元线程(multithreaded)
FlagDesc
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:

FlagDesc
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);
	}
};

  1. [REF:Game Dev] ↩︎

  2. https://docs.microsoft.com/en-us/windows/win32/learnwin32/what-is-a-com-interface- ↩︎

  3. combaseapi.h ↩︎

  4. https://docs.microsoft.com/en-us/windows/win32/learnwin32/initializing-the-com-library ↩︎

  5. https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coinitializeex ↩︎

  6. https://docs.microsoft.com/zh-cn/cpp/atl/marshaling?view=vs-2019 ↩︎

  7. https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance ↩︎

  8. guiddef.h ↩︎

  9. https://docs.microsoft.com/zh-cn/windows/win32/api/wtypesbase/ne-wtypesbase-clsctx ↩︎

  10. https://docs.microsoft.com/en-us/windows/win32/learnwin32/creating-an-object-in-com ↩︎

  11. https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iunknown ↩︎

  12. https://docs.microsoft.com/en-us/windows/win32/com/aggregation ↩︎

  13. https://docs.microsoft.com/en-us/windows/win32/com/reusing-objects ↩︎

  14. http://www.dfwlt.com/history/data/journal/mycmq/19615.html ↩︎ ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值