很多年前,微软提供了COM组件对象模型。
随后,提供了COM SDK。
后来,又在MFC中,增加了COM开发的支持。
再后来,又提供了ATL类库,对COM开发提供了更强大的支持。
本文从使用MFC和ATL开发COM组件的角度,来分析比较两者的差异和优劣。
首先,需要继承一个基类,并声明成动态创建类(RuntimeClass)。
//.h
class CComMFCDemo : public CCmdTarget
{
DECLARE_DYNCREATE(CComMFCDemo)
};
//.cpp
IMPLEMENT_DYNCREATE(CComMFCDemo, CCmdTarget)
然后,声明并实现COM功能函数。
//.h
class CComMFCDemo : public CCmdTarget
{
BSTR Greeting(BSTR name);
long Add(long val1, long val2);
long Sub(long val1, long val2);
};
//.cpp
BSTR CComMFCDemo::Greeting(BSTR name)
{
CComBSTR tmp("Welcome, ");
tmp.Append(name);
return tmp;
}
long CComMFCDemo::Add(long val1, long val2)
{
return val1 + val2;
}
long CComMFCDemo::Sub(long val1, long val2)
{
return val1 - val2;
}
然后,声明并实现接口。
这里,为了通用性,使用了双重接口定义。并且,声明了两个不同的接口类型。即IWelcome和IMath。
//.h
class CComMFCDemo : public CCmdTarget
{
BEGIN_DUAL_INTERFACE_PART(Welcome, IWelcome)
STDMETHOD(Greeting)(BSTR name, BSTR *message);
END_INTERFACE_PART(Welcome)
BEGIN_DUAL_INTERFACE_PART(Math, IMath)
STDMETHOD(Add)(LONG val1, LONG val2, LONG* result);
STDMETHOD(Sub)(LONG val1, LONG val2, LONG* result);
END_INTERFACE_PART(Math)
};
//.cpp
DELEGATE_DUAL_INTERFACE(CComMFCDemo, Welcome)
STDMETHODIMP CComMFCDemo::DUAL_INTERFACE(Welcome)::Greeting(BSTR name, BSTR *message)
{
METHOD_PROLOGUE(CComMFCDemo, Welcome)
*message = pThis->Greeting(name);
return S_OK;
}
DELEGATE_DUAL_INTERFACE(CComMFCDemo, Math)
STDMETHODIMP CComMFCDemo::DUAL_INTERFACE(Math)::Add(LONG val1, LONG val2, LONG* result)
{
METHOD_PROLOGUE(CComMFCDemo, Math)
*result = pThis->Add(val1, val2);
return S_OK;
}
STDMETHODIMP CComMFCDemo::DUAL_INTERFACE(Math)::Sub(LONG val1, LONG val2, LONG* result)
{
METHOD_PROLOGUE(CComMFCDemo, Math)
*result = pThis->Sub(val1, val2);
return S_OK;
}
(有关的宏定义,可以参考这里)
#define BEGIN_DUAL_INTERFACE_PART(localClass, baseClass) \
BEGIN_INTERFACE_PART(localClass, baseClass) \
STDMETHOD(GetTypeInfoCount)(UINT FAR* pctinfo); \
STDMETHOD(GetTypeInfo)(UINT itinfo, LCID lcid, ITypeInfo FAR* FAR* pptinfo); \
STDMETHOD(GetIDsOfNames)(REFIID riid, OLECHAR FAR* FAR* rgszNames, UINT cNames, LCID lcid, DISPID FAR* rgdispid); \
STDMETHOD(Invoke)(DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS FAR* pdispparams, VARIANT FAR* pvarResult, EXCEPINFO FAR* pexcepinfo, UINT FAR* puArgErr); \
#define END_DUAL_INTERFACE_PART(localClass) \
END_INTERFACE_PART(localClass) \
#define DUAL_INTERFACE(dualClass) X##dualClass
#define DELEGATE_DUAL_INTERFACE(objectClass, dualClass) \
STDMETHODIMP_(ULONG) objectClass::DUAL_INTERFACE(dualClass)::AddRef() \
{ \
METHOD_PROLOGUE(objectClass, dualClass) \
return pThis->ExternalAddRef(); \
} \
STDMETHODIMP_(ULONG) objectClass::DUAL_INTERFACE(dualClass)::Release() \
{ \
METHOD_PROLOGUE(objectClass, dualClass) \
return pThis->ExternalRelease(); \
} \
STDMETHODIMP objectClass::DUAL_INTERFACE(dualClass)::QueryInterface( \
REFIID iid, LPVOID* ppvObj) \
{ \
METHOD_PROLOGUE(objectClass, dualClass) \
return pThis->ExternalQueryInterface(&iid, ppvObj); \
} \
STDMETHODIMP objectClass::DUAL_INTERFACE(dualClass)::GetTypeInfoCount( \
UINT FAR* pctinfo) \
{ \
METHOD_PROLOGUE(objectClass, dualClass) \
LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE); \
ASSERT(lpDispatch != NULL); \
return lpDispatch->GetTypeInfoCount(pctinfo); \
} \
STDMETHODIMP objectClass::DUAL_INTERFACE(dualClass)::GetTypeInfo( \
UINT itinfo, LCID lcid, ITypeInfo FAR* FAR* pptinfo) \
{ \
METHOD_PROLOGUE(objectClass, dualClass) \
LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE); \
ASSERT(lpDispatch != NULL); \
return lpDispatch->GetTypeInfo(itinfo, lcid, pptinfo); \
} \
STDMETHODIMP objectClass::DUAL_INTERFACE(dualClass)::GetIDsOfNames( \
REFIID riid, OLECHAR FAR* FAR* rgszNames, UINT cNames, \
LCID lcid, DISPID FAR* rgdispid) \
{ \
METHOD_PROLOGUE(objectClass, dualClass) \
LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE); \
ASSERT(lpDispatch != NULL); \
return lpDispatch->GetIDsOfNames(riid, rgszNames, cNames, \
lcid, rgdispid); \
} \
STDMETHODIMP objectClass::DUAL_INTERFACE(dualClass)::Invoke( \
DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, \
DISPPARAMS FAR* pdispparams, VARIANT FAR* pvarResult, \
EXCEPINFO FAR* pexcepinfo, UINT FAR* puArgErr) \
{ \
METHOD_PROLOGUE(objectClass, dualClass) \
LPDISPATCH lpDispatch = pThis->GetIDispatch(FALSE); \
ASSERT(lpDispatch != NULL); \
return lpDispatch->Invoke(dispidMember, riid, lcid, \
wFlags, pdispparams, pvarResult, \
pexcepinfo, puArgErr); \
} \
然后,声明并实现接口映射表。
//.h
class CComMFCDemo : public CCmdTarget
{
DECLARE_INTERFACE_MAP()
};
//.cpp
BEGIN_INTERFACE_MAP(CComMFCDemo, CCmdTarget)
INTERFACE_PART(CComMFCDemo, IID_IWelcome, Welcome)
INTERFACE_PART(CComMFCDemo, IID_IMath, Math)
END_INTERFACE_MAP()
完成了上面这些,即可以满足例如C++,C#这些静态语言使用COM接口的需求;但还不能满足例如Python,VB这些动态语言使用COM接口的需求。
于是,我们需要继续声明并实现接口调度表。
(需要注意的是,调度表的ID必须与.idl文件里的ID对应。并且,不同接口方法的ID,在此调度表里必须为唯一值)
//.h
class CComMFCDemo : public CCmdTarget
{
DECLARE_DISPATCH_MAP()
};
//.cpp
BEGIN_DISPATCH_MAP(CComMFCDemo, CCmdTarget)
DISP_FUNCTION_ID(CComMFCDemo, "Greeting", 1, Greeting, VT_BSTR, VTS_BSTR)
DISP_FUNCTION_ID(CComMFCDemo, "Add", 2, Add, VT_I4, VTS_I4 VTS_I4)
DISP_FUNCTION_ID(CComMFCDemo, "Sub", 3, Sub, VT_I4, VTS_I4 VTS_I4)
END_DISPATCH_MAP()
最后,需要声明并实现工厂类,以及注册工厂类。
//.h
class CComMFCDemo : public CCmdTarget
{
DECLARE_OLECREATE(CComMFCDemo)
};
//.cpp
IMPLEMENT_OLECREATE_FLAGS(CComMFCDemo, "COMServerMFC.ComMFCDemo", afxRegApartmentThreading, 0xe825a1ea, 0x2672, 0x4dd3, 0x8d, 0xa2, 0x57, 0x86, 0x4f, 0xd7, 0x7f, 0xbb)
至此,MFC中的COM类,已经基本完成了。
在C#中使用COM的例子程序为:
// interface invoke
var o = new COMDemo();
IWelcome welcome = o;
string str = welcome.Greeting("Interface Caller");
Console.WriteLine(str);
IMath math = (IMath)o;
int n = math.Add(3,4);
Console.WriteLine(n);
// dispatch invoke
Type t = Type.GetTypeFromProgID("COMServerMFC.ComMFCDemo");
dynamic o = Activator.CreateInstance(t);
string str = o.Greeting("Dispatch Caller");
Console.WriteLine(str);
int n = o.Add(4,5);
Console.WriteLine(n);
首先,需要继承多个模板基类(其中包括了两个接口模板类)。
//.h
class ATL_NO_VTABLE CCOMDemo :
public CComObjectRootEx,
public CComCoClass,
public IDispatchImpl1, /*wMinor =*/ 0>,
public IDispatchImpl,
{
};
然后,声明并实现COM功能函数(按照接口方法声明)。
//.h
class ATL_NO_VTABLE CCOMDemo :
//...
{
STDMETHOD(Greeting)(BSTR name, BSTR* message);
STDMETHOD(Add)(long val1, long val2, long* result);
STDMETHOD(Sub)(long val1, long val2, long* result);
}
//.cpp
STDMETHODIMP CCOMDemo::Greeting(BSTR name, BSTR* message)
{
CComBSTR tmp("Welcome, ");
tmp.Append(name);
*message = tmp;
return S_OK;
}
STDMETHODIMP CCOMDemo::Add(LONG val1, LONG val2, LONG* result)
{
*result = val1 + val2;
return S_OK;
}
STDMETHODIMP CCOMDemo::Sub(LONG val1, LONG val2, LONG* result)
{
*result = val1 - val2;
return S_OK;
}
然后,声明并实现接口映射表。
//.h
class ATL_NO_VTABLE CCOMDemo :
//...
{
BEGIN_COM_MAP(CCOMDemo)
COM_INTERFACE_ENTRY(IWelcome)
COM_INTERFACE_ENTRY(IMath)
COM_INTERFACE_ENTRY2(IDispatch, IWelcome)
END_COM_MAP()
}
最后,需要声明并实现工厂类,以及注册工厂类。
//.h
OBJECT_ENTRY_AUTO(__uuidof(COMDemo), CCOMDemo)
至此,ATL中的COM类,已经基本完成了。
在C#中使用COM的例子程序为:
// interface invoke
var o = new COMDemo();
IWelcome welcome = o;
string str = welcome.Greeting("Interface Caller");
Console.WriteLine(str);
IMath math = (IMath)o;
int n = math.Add(3, 4);
Console.WriteLine(n);
// dispatch invoke
Type t = Type.GetTypeFromProgID("COMServer.COMDemo");
dynamic o = Activator.CreateInstance(t);
string str = o.Greeting("Dispatch Caller");
Console.WriteLine(str);
int n = ((IMath)o).Add(4, 5); // o.Add() is failed
Console.WriteLine(n);
/ | MFC | ATL |
---|---|---|
实现方式 | 宏定义 | 多重继承 |
创建方式 | 动态创建类 | 静态创建类 |
自定义接口 | 增加成员类(接口类) | 继承模板基类(接口类) |
调度接口 | 集中于派生类 | 分散于基类 |
接口映射表 | 按成员类偏移值索引 | 按基类偏移值索引 |
工厂模式 | 单例 | 多例 |
ATL中,使用继承的方式,可以很方便的集成、重用接口定义。很多基础功能都可以在基类中完成。要实现多个接口定义,只需要多重继承于多个基类(接口类)。
MFC中,要实现多个接口定义,需要添加多个成员类(接口类)。
某些情况下,需要将多个接口类组合后,并实现部分接口方法,从而产生新的接口类(例如CPreviewHandlerImpl)。这样的接口组合,在MFC中,显得非常吃力。而在ATL中,则显得非常轻松。
两种方法最终都用了new ClassName()的方式建立COM实例。
MFC中,需要编码来实现接口方法定义,使其与功能函数对应匹配。
ATL中,不需要编码来实现接口方法定义,可以直接按照接口方法声明,实现功能函数定义。并且,ATL消除了MFC中对接口方法声明顺序的依赖。这意味着,可以在任意派生类中,以任何顺序实现功能函数定义。
这个特性非常有用,可以有效避免很多不必要的人为失误,将对代码质量的依赖,转变为对编译器的依赖。体现了“最少编码”原则。
(在我经历的项目实践中,出现过因为没有严格按照接口方法声明顺序,来实现接口方法定义,最终导致程序崩溃的案例)
ATL中,由于不需要实现接口调度表了,以后增加接口方法时,也不需要去修改维护接口调度表的代码。基类(接口类)完全接管了这些功能。
ATL中,不同接口之间的调度方法的ID,不必为唯一值。各个接口可以为自己的调度方法分配独立ID值。由于不同的接口类独立处理调度请求,所以不同接口之间的(相同的)调度方法ID值,不会发生冲突。
与ATL中实现自定义接口方法一样,这个特性非常有用。ATL将部分多余的代码都移除了,使开发人员的代码维护工作量大大减少。
/ | MFC | ATL |
---|---|---|
编码复杂性 | 复杂 | 简单 |
功能复杂性 | 一般 | 强 |
依赖性 | 高 | 低 |
目标文件容量 | 大 | 小 |
ATL类库大大减少了COM组件开发的编码工作量,并提高了代码的质量,降低了后期维护的难度。
ATL类库通过引入更多强大的类,来增强COM组件的功能,包括后来的COM+服务。而MFC对COM的支持就基本停止更新了。
通过ATL类库开发的COM组件,不依赖于MFC运行库,所以程序体积小,容易制作成ActiveX在网络上传输。
MFC由于将调度接口表集中在COM类中,虽然要求ID值唯一,但是也因此提供了全局调度的能力。
在C#的例子程序中,可以看到这个代码o.Add(4,5)
,在MFC生成的COM组件中,可以正确的调用;而在ATL生成的COM组件中,则调用失败。代码必须改成((IMath)o).Add(4, 5)
,才能调用成功。这大大限制了ATL生成的COM组件在C#语言中的泛化能力。
综合前面的分析,我们可以发现,在COM组件的实际开发中,无论是从编码的角度,还是从使用的角度,ATL都比MFC要更方便和强大一些。
MFC因为历史原因,很多遗留项目依然在使用这个架构。对于这些项目,要顺利支持COM接口,必须使用与MFC配套的方法。
而对于单独开发的项目,则直接采用ATL类库开发,即是最有效的方式。
ATL类库,将所有关于COM组件技术的细节都隐藏到了底层,并消除了代码相关性,使开发人员只做最小而必要的改动。
使用了ATL类库的代码非常精炼和简洁,这使得开发人员可以专注于功能和业务逻辑,而不必陷入复杂而琐碎的技术细节当中。