第九部分 且看看COMToys的更多特性以及与MFC的比较
COMToys的更多特性
除了主要的功能以外,COMToys还有一些其它特性。
智能指针 ATL的智能指针是我不得不赞扬的一个东西。如果你到目前为止还没用使用过智能指针的话,那么就赶快终结这个时代吧!我的意思是,终止对AddRef 和 Release的一切操心。不管你的函数有多少退出路径,智能指针都能保证所有的引用计数正确无误。免得你殚精竭虑,费尽心思和功夫来查找引用计数的bugs。有过这种经历的兄弟都知道,那是一个可怕的噩梦:
图十九 Aaaaaaaah
对于那些一见到模板就往后退的家伙来说,COMToys有一个宏隐藏了那些尖括弧:
#define DECLARE_SMARTPTR(ifacename) \
typedef CComQIPtr SP##ifacename;
现在你可以写:
DECLARE_SMARTPTR(IPersist);
为了定义新类型SPIPersist。以便在任何出现IPersist的地方都能使用这个新类型。SPIUnknown是个专门派生的,因为它需要CComPtr,而不是CComQIPtr。(没有必要为IUnknown调用QueryInterface——每个接口都具备)。下面的代码段是一些典型的使用和没有使用智能指针的COM代码,通过比较,相信不再需要更多的解释:
///////////////////////////////////////////////////////////////////////
// 简化后的智能指针代码:
// 典型的没有用智能指针实现的 COM 函数,当有多个退出路径时,这种做法很糟糕。
//
//
STDMETHODIMP CExplorerBar::SetSite(IUnknown* punkSite)
{
// If a site is being held, release it.
if(m_pSite) {
m_pSite->Release();
m_pSite = NULL;
}
// If punkSite is not NULL, a new site is being set.
if (punkSite) {
......
// Get and keep the IInputObjectSite pointer.
if (SUCCEEDED(punkSite->QueryInterface(IID_IInputObjectSite,
(LPVOID*)&m_pSite))) {
return S_OK;
}
return E_FAIL;
}
return S_OK;
}
/////////////////////////////////////////////////////////////////////////////////////
// 用智能指针实现的相同的 COM 函数,从 12 行代码减少到 2 行。
// 因为 m_pSite 被声明为 CComQIPtr,没有必要调用 QueryInterface(IID_IInputObjectSite),
// 也没有任何必要调用 AddRef 累加新的指针或调用Release释放旧的指针。智能指针全都搞掂
//
STDMETHODIMP CBandObj::SetSite(IUnknown* punkSite)
{
if (m_pSite = punkSite) {
......
}
return punkSite && !m_pSite ? E_FAIL : S_OK;
}
有关ATL智能指针更深层次的探讨请参考另外一篇专题文章:“
用ATL建立轻量级的COM对象”。这篇文章还描述了智能指针可能导致麻烦的罕见情形。 多线程 COMToys并不能实现所有的COM对象,它的应用仅限于外壳扩展。其线程模型是单线程公寓(STA),这种情况下,类成员的线程安全是自动的,并且只有全局量需要保护。有静态成员(全局量)的COMToys类同时也有临界部分,g_mydata,在存取它之前必须锁定。COMToys的一个小类CTLockData可以很轻松完成这种锁定,由构造函数锁定临界部分,而由析构函数进行解锁,所以你要做的只是用下面的代码:
{ // protected block of code
CTLockData lock(g_mydata);
// do your worst
……
}
由于有了智能指针,CTLockData的好处是不必记住解锁,即便函数或代码块有多个出口。C++保证析构函数会被调用并且在控制离开时解锁。CTFactory在任何需要存取其全局类工厂列表时使用CTLockData。
调试诊断 理解任何COM对象运作行为的最佳方法是只要接口方法被调用,就输出其诊断信息。没有这样的诊断,便不可能弄清楚Band对象是如何工作的。COMToys有一个内建的跟踪机制。IMPLEMENT_ IWhatsIt宏在每一个函数的开始处(顶部)产生TRACE语句,所以只要打开跟踪开关(COMToys::bTRACE = TRUE),就可以看到对象正在做什么。COMToys使用CTraceFn类。
COMToys能产生人们可读的接口名。这个诊断系统使用一个叫DbgName的重载函数来获得各种不同对象的"调试名"。DbgName(WM_DESTROY)返回WM_DESTROY;DbgName(SCODE)返回S_OK 或 E_OUTOFMEMORY之类的值;而DbgName(CWnd*)返回窗口名字。
正如所期望的那样,DbgName(REFIID)返回人可读的GUID,如:{EB0FE172-1A3A-11D0-89B3-00A0C90A90AC},但如何知道这一长串东西代表的就是IDeskBand呢?COMToys有一个办法:
DEBUG_BEGIN_INTERFACE_NAMES()
DEBUG_INTERFACE_NAME(IDeskBand)
DEBUG_END_INTERFACE_NAMES()
现在DbgName(IID_IDeskBand)返回的是IDeskBand,而不是那让人费解的十六进制。这个宏产生一个局部表,它被链接到一个DbgName可以搜索的全局表中。详细内容请参见源代码中的debug.h 和 debug.cpp文件。
COMToys 动态链接库DLL 下载COMToys的源代码以后可以用六种方式编译:与MFC静态链接的静态库(static library),与MFC动态链接的静态库(static library),或者与MFC动态链接(扩展DLL)的DLL,用Debug和Release模式编译它们从而形成六个版本。
存在的问题 哪个系统会没有问题呢?包括COMToys在内还有很多地方需要完善。我只写了Band对象所需要的那一部分。因此,像CTPersistStream 和 CTPersistFile并未做任何实质性的事情;它们只有一个修改标志。像微软在bug文档中惯用的伎俩一样:当你不能解决某个bug的时候,就把它当做一个特性。把它说成:“这是设计行为云云......”
COMToys的另一个令人讨厌的是宏调试问题——有时你无法在调试器中跟踪进去。幸好我到哪都不喜欢使用调试器,而是用自己的跟踪方法。
COMToys和MFC
在进行总结之前,关于COMToys和MFC之间的关系,我想再多罗嗦几句。COMToys似乎很依赖MFC。我对此不以为然,因为我喜欢MFC并总是乐于使用它。但有些人觉得MFC是负担和累赘。我觉得不要陷入MFC和ATL之间的争论,但我必须指出,从本质上讲,COMToys将接口融于实体类的方法是独立于MFC的,也就是说在这一点上它不依赖MFC。
当我编写COMToys的时候,我仔细地注意了在哪里需要MFC,哪里不需要它。因此,CTOleWindow,CTDockingWindow和CTInputObject使用了句柄(HWND,HACCEL)来代替CWnd*之类的MFC对象,因为它们不需要MFC来打包任何东西。另一方面,CTMfcContextMenu需要CMenu和CCmdTarget,因为它完成所有的命令例程。同样,CTMfcModule调用MFC来实现标准的DLL入口。通常在设计与MFC有关的类时,我在名字中都包含有“Mfc”。
除了实现像CTMfcContextMenu之类的类以外,COMToys只有三个地方确实需要MFC支持。IMPLEMENT_IUnknownCT假设某个CCmdTarget派生主类实现IUnknown。CTFactory没有像它应该做的那样实现IClassFactory,所以你需要一个COleObjectFactory派生类工厂来创建对象。CTModule不实现模块锁定或者DllGetClassObject,所以你需要CTMfcModule来获得这些重要的细节。为了完全将COMToys完全和MFC隔离,你只要填补这些缝隙就可以了。
至于IUnknown,必须由一个基本的CTUnknown来实现AddRef,Release,和 QueryInterface。AddRef/Release可以很简单地对数据成员m_dwRef进行++/--操作,而QueryInterface调用理论上等价的GetInterfaceHook,GetInterfaceHook必须由外部类提供。你可以派生另一个实现(把它叫做CTUnknownMTA),这个实现用InterlockedIncrement 和 InterlockedDecrement代替++/--。有了这些实现,你的COM类就可以用IMPLEMENT_ IUnknown(CMyComClass, CTUnknown)来代替当前设计用于CCmdTarget 的IMPLEMENT_IUnknownCT宏。这样做就有点像ATL了——但不涉及模板。
至于类工厂,必须为CTFactory实现IClassFactory。这个实现可以调用某个名字类似OnCreateObject的虚函数,每一个COM类都不必须通过返回“new CMyComClass”来提供这个虚函数。(COleObjectFactory用MFC运行时系统创建对象。)
为了钩挂类工厂,必须实现CTModule::OnDllGetClassObject。它将搜索类工厂列表来查找谁的CLSID与请求的匹配,然后调用它的CreateInstance方法。
最后还有两件事情要做:一是要在CTModule中编写一些代码来进行模块锁定(CTModule::OnDllCanUnloadNow);二是当获得DLL_PROCESS_ATTACH/DETACH.时,在DllMain中初始化或终止这个模块。
做完以上的事情,你就可以使用COMToys来编写COM对象了,用它编写倾向于ATL的COM对象,或者比ATL还要ATL。COMToys模型有两层:基础层独立于MFC,较高层的类使用MFC。我进行这种分层不是没有道理,它确实必要。如果有时间我会为此再写一篇专题文章。
来自黑洞恐怖的奇闻轶事
没有哪一个项目是顺顺利利完成而不经历波折与磨难,COMToys也一样。它发生在我测试注册代码的时候,我注意到在调用ResourceUnregister时,IRegistrar没有完全删除该删除的东西。我猜测它是出于安全起见才这么做的。于是我决定加几行代码在注销类的同时删除HKCR\CLSID\clsid。我查到MFC中有一个现成的函数来做这件事情:AfxOleUnregisterClass。这个函数有两个参数,一个是某个类的ID,另一个是ProgID。于是我加上了这一行代码:
AfxOleUnregisterClass(m_clsid, GetProgID());
为了测试我的代码,我运行regsvr32.exe /u,然后运行REGEDIT去看注册表,按F5刷新,显示结果如图二十。看到没有!HKEY_CLASSES_ROOT已经全部展开。我的整个HKEY_CLASSES_ROOT不见了,只剩下空空如也的CLSID键。
图二十 啊!我的注册表键值到哪去了?
怎么会这样啊;一定是REGEDIT搞错了。我退出REGEDIT并从"开始"菜单中重新运行REGEDIT,结果给我显示出一个小对话框。"这个程序引用了一个不存在的lnk文件……",这些信息看似无所谓,但实际上我面临的形势严峻。我桌面上的东西不翼而飞。资源管理器失踪了?IE也没了?每个图标被替换成莫名其妙的图标。我甚至不能启动MS-DOS窗口。我气得恼羞成怒,差点把显示器砸了。只好重启机器。但很不幸,我被欺骗了。MFC把整个注册表删掉了。我沮丧地望着屏幕无可奈何,想着最后一次备份是在什么时候......
幸运的是我用一个买来的程序每天都自动备份注册表。但lnk文件都没了怎么运行呢?终于我还能从"开始"菜单中到"运行"窗口,然后敲入command.com/cmd.exe打开外壳MS-DOS窗口。进到ConfigSafe目录并运行它将注册表恢复到前一天的配置。到这我才松了口气。
这件事情导致系统临近崩溃的边缘。后来Band对象不能插入,它们不需要ProgID。MyBands的注册表串没有ProgID,所以CTFactory::GetProgID自然返回空CString。注意是空empty,不是NULL。我将这个空串传到AfxOleUnregisterClass函数,它包含如下的代码行:
if (pszProgID != NULL)
_AfxRecursiveRegDeleteKey(HKEY_CLASSES_ROOT,
(LPTSTR)pszProgID);
我想不需要再做进一步解释了,函数名中的单词“recursive”说明了一切。MFC象傻子一样欣然地删除了HKEY_CLASSES_ROOT下的所有东西。微软的人会修复这个问题吗! 这次的经验证明,在操作注册表的时候,以下几点是一定要注意的:
1、 必须要小心谨慎,这一点怎么强调都不过分。
2、 如果程序中要处理注册表键,要针对NULL值和空串增加一些附加的安全检查代码。
3、 如果没有用某个程序每天备份注册表,那么现在就赶快备份吧,亡羊补牢,为时不晚。
4、 不要相信MFC。不要相信任何人。并且如果你有心脏病,那么就不要做程序员(开玩笑)。
总结
无论你是否决定在自己的程序中使用COMToys,我都希望你至少认识到使用ATL、MFC或任何其它从ATL/MFC继承的系统并不是绝对的。重要的是不要陷入任何一个系统的怪圈中,而要按照自己方式来使用它们。我就乐意将COMToys看成是带多继承的MFC,或者无模板的ATL。
我还希望你能明白,花点时间建立一点儿自己的编程平台或者说基础结构(COMToys包括执行类只有2400行左右的代码,)这样你可以极大地方便自己的C++ COM编程。宏,智能指针,跟踪——这些简单的工具对编程也大有帮助,使编程更加容易。最终的COMToys库源代码不可能在此全部列出,需要的话可以全部下载。总之,用COMToys来编写BandObj对象很简单,只需将一些预先做好的东西粘合在一起并加入你要的新特性。它的宗旨就是可重用性。不仅仅是在BandObj中使用,它也可用于其它的应用。今后还可以进一步实现IPersistStream接口以扩充更大的功能。 COMToys并不是十全十美,也没有哪个系统能做到。但COMToys在实践中运行良好,而且我用它建立了Band对象和一个快速的浏览文件的浏览器。今后我会不断完善COMToys,所以请关注最新开发动向和源代码。
最后祝大家编程愉快!
(全文完)