MFC与ATL在COM组件开发中的差异和优劣

我与影子孤独终老i 提交于 2019-12-07 04:38:00

很多年前,微软提供了COM组件对象模型。
随后,提供了COM SDK。
后来,又在MFC中,增加了COM开发的支持。
再后来,又提供了ATL类库,对COM开发提供了更强大的支持。

本文从使用MFC和ATL开发COM组件的角度,来分析比较两者的差异和优劣。


MFC代码部分


首先,需要继承一个基类,并声明成动态创建类(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);

ATL代码部分


首先,需要继承多个模板基类(其中包括了两个接口模板类)。

//.h
class ATL_NO_VTABLE CCOMDemo :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CCOMDemo, &CLSID_COMDemo>,
    public IDispatchImpl<IWelcome, &IID_IWelcome, &LIBID_COMServerLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IDispatchImpl<IMath, &IID_IMath, &LIBID_COMServerLib, 1, 0>,
{
};

然后,声明并实现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代码的差异分析


/ MFC ATL
实现方式 宏定义 多重继承
创建方式 动态创建类 静态创建类
自定义接口 增加成员类(接口类) 继承模板基类(接口类)
调度接口 集中于派生类 分散于基类
接口映射表 按成员类偏移值索引 按基类偏移值索引
工厂模式 单例 多例

  • MFC中,实现COM类主要有两种方式:
    1) 新建COM类,主要用于对Automation的支持。
    2) 修改已有类,使其变成为COM类。一般是通过修改CDocument的派生类,并调用COleTemplateServer::ConnectTemplate()方法来连接COM。主要用于对OLE的支持(例如Word,Excel等)。
    由于上述情况,所以MFC使用宏定义的方式来支持COM开发。
  • ATL中,没有CCmdTarget和CDocument的基础类,而重新设计了CComCoClass和IDispatchImpl等模板类。所以ATL使用模板类库和多重继承的方式来支持COM开发。

ATL中,使用继承的方式,可以很方便的集成、重用接口定义。很多基础功能都可以在基类中完成。要实现多个接口定义,只需要多重继承于多个基类(接口类)。
MFC中,要实现多个接口定义,需要添加多个成员类(接口类)。
某些情况下,需要将多个接口类组合后,并实现部分接口方法,从而产生新的接口类(例如CPreviewHandlerImpl)。这样的接口组合,在MFC中,显得非常吃力。而在ATL中,则显得非常轻松。


  • MFC中,工厂类使用动态创建类中的CreateObject()方法来建立COM实例。所以COM类必须声明并实现RuntimeClass。
  • ATL中,工厂类使用模板基类中的CreateInstance()方法来建立COM实例。建立实例的过程中,使用了_ATL_NEW宏定义。ATL通过Template Class技术,替代了Runtime Class技术。

两种方法最终都用了new ClassName()的方式建立COM实例。


  • MFC中,实现自定义接口方法,需要在COM类中添加成员类(接口类),并按照接口方法声明的顺序,依次(在成员类中)声明接口方法函数,并实现接口方法函数到COM类功能函数的调用。接口方法函数中,使用METHOD_PROLOGUE宏定义,进行从接口对象指针到COM对象指针的转换。
  • ATL中,实现自定义接口方法,需要将COM类继承于模板基类(接口类),并按照接口方法声明,(在COM类中)声明并实现功能函数。功能函数声明的顺序,不需要依赖于接口方法声明的顺序。通过模板基类的vtable机制,基类(接口类)的接口方法声明与派生类(COM类)的功能函数声明自动对应匹配了。

MFC中,需要编码来实现接口方法定义,使其与功能函数对应匹配。
ATL中,不需要编码来实现接口方法定义,可以直接按照接口方法声明,实现功能函数定义。并且,ATL消除了MFC中对接口方法声明顺序的依赖。这意味着,可以在任意派生类中,以任何顺序实现功能函数定义。
这个特性非常有用,可以有效避免很多不必要的人为失误,将对代码质量的依赖,转变为对编译器的依赖。体现了“最少编码”原则。
(在我经历的项目实践中,出现过因为没有严格按照接口方法声明顺序,来实现接口方法定义,最终导致程序崩溃的案例)


  • MFC中,实现调度接口方法,需要在COM类中添加接口调度表,并按照接口方法声明,实现接口调度表。并且,该COM类中的所有调度方法的ID,必须为唯一值
  • ATL中,实现调度接口方法,需要将COM类继承于模板基类(接口类),不需要额外实现接口调度表。通过CComTypeInfoHolder类的处理,基类(接口类)的接口方法声明与派生类(COM类)的功能函数声明自动对应匹配了。

ATL中,由于不需要实现接口调度表了,以后增加接口方法时,也不需要去修改维护接口调度表的代码。基类(接口类)完全接管了这些功能。
ATL中,不同接口之间的调度方法的ID,不必为唯一值。各个接口可以为自己的调度方法分配独立ID值。由于不同的接口类独立处理调度请求,所以不同接口之间的(相同的)调度方法ID值,不会发生冲突。
与ATL中实现自定义接口方法一样,这个特性非常有用。ATL将部分多余的代码都移除了,使开发人员的代码维护工作量大大减少。


  • MFC中,实现接口映射表,需要在COM类中添加接口映射表。由于接口类是成员类,所以按照成员类偏移值(offsetof宏),建立接口映射表索引。
  • ATL中,实现接口映射表,需要在COM类中添加接口映射表。由于接口类是基类,所以按照基类偏移值(offsetofclass宏),建立接口映射表索引。

MFC与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类库的代码非常精炼和简洁,这使得开发人员可以专注于功能和业务逻辑,而不必陷入复杂而琐碎的技术细节当中。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!