使用 MFC 序列化 COM 对象






2.60/5 (4投票s)
2000年1月18日

57436

711
一种巧妙的方法,
引言
几个月前,我需要创建构成MFC MDI文档数据结构的对象。这个结构相当复杂,这意味着结构中的对象在结构中被多次引用。应用程序的插件将这些对象视为COM对象;插件仅通过文档的COM接口操作COM对象。这些对象也必须序列化到文档文件中。
COM实现
保留的解决方案是使用MFC实现它们(从`CCmdTarget`派生,然后使用MFC提供的宏),因为框架是用MFC完成的,并且此机制很好地集成在一起。有关如何使用MFC提供COM实现的更多信息,请参阅MSDN。
序列化
由于数据结构内部存在多个引用,因此对象的序列化不会像您预期的那样使用`IPersistStream`;这将导致开发一种不可取的序列化算法。事实上,我只是使用了MFC提供的序列化。为什么?好吧,数据结构涉及数据结构中对象的多个引用,而MFC提供了一种简洁的方法来序列化这种结构——而且更多,因为对象是用MFC实现的,所以序列化是免费提供的(因为对象继承自`CObject`)。
问题所在
好吧,我们有COM接口,我们有强大的序列化功能,那么问题在哪里呢?事实上,数据结构是使用COM规则访问的,因此主要依赖于正确的引用计数来管理对象的生存期。问题是,MFC序列化在加载时不会对`CCmdTarget`派生对象进行引用计数(`CCmdTarget`将引用计数初始化为1,这在存在多个引用的情况下是不正确的)。因此,如果对象被多次引用,您的代码将崩溃(这将在释放COM对象时出现)!
解决方案
显然,我们必须修改`CArchive`的加载算法,但是怎样呢?第一个想法是创建一个从`CArchive`继承的`CArchiveEx`类,然后重载必要的方法,但是由于没有虚方法,这是不可能的,而且无论如何,这将导致对MFC框架进行重要的修改(您将不得不修改`CDocument::OnOpenDocument()`才能使用新类)。
那么,我们卡住了吗?不!
事实上,如果您查看`DECLARE_SERIAL`和`IMPLEMENT_SERIAL`宏的定义,您会注意到这些宏定义了`operator >>`以便从`CArchive`流加载对象。就是这样!只需修改运算符>>并进行正确的引用计数即可。
因此,以下是`IMPLEMENT_SERIAL`和`DECLARE_SERIAL`的宏替换(粗体行表示与原始宏的不同之处)
#define DECLARE_SERIAL_COM(class_name) \ _DECLARE_DYNCREATE(class_name) \ AFX_API friend CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb); \ BOOL _m_bAlreadyLoaded; #define IMPLEMENT_SERIAL_COM(class_name, base_class_name, wSchema) \ CObject* PASCAL class_name::CreateObject() \ { class_name *pOb = new class_name; \ pOb->_m_bAlreadyLoaded = FALSE; \ return pOb; } \ _IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, class_name::CreateObject) \ AFX_CLASSINIT _init_##class_name(RUNTIME_CLASS(class_name)); \ CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb) \ { pOb = (class_name*) ar.ReadObject(RUNTIME_CLASS(class_name)); \ if (pOb != NULL) \ if (pOb->_m_bAlreadyLoaded) \ pOb->ExternalAddRef(); \ else \ pOb->_m_bAlreadyLoaded = TRUE; \ return ar; }新的`operator >>`负责在对象被多次加载时进行正确的引用计数。请注意,有一个布尔标志(`_m_bAlreadyLoaded`),它表示我们不应该在第一次加载时增加引用计数。您可能会说,我可以在分配新对象后立即将引用计数设置为零(0),然后在`operator >>`内部使用`ExternalAddRef()`,但这会改变`CreateObject()`的分配策略(它默认创建一个引用计数为一的对象),所以我认为这种方式在兼容性方面更好。
这些宏的开销是它向您的类添加了BOOL变量`_m_bAlreadyLoaded`。
如何使用此代码
只需分别用`DECLARE_SERIAL_COM`和`IMPLEMENT_SERIAL_COM`替换宏`DECLARE_SERIAL`和`IMPLEMENT_SERIAL`,它应该就可以工作了(您的类必须至少继承自`CCmdTarget`!)。提供的宏与`CArchive`序列化和参数方面完全兼容。
这些宏也与`CCmdTarget`的`CreateObject()`的创建策略兼容(我的意思是引用计数设置为预期值一(1))。
关于代码
给出的源代码只使用由实现COM接口的虚拟类提供的小型数据集来测试解决方案的正确性。测试只是在内存中创建一些数据并对其进行序列化;它还会转储数据结构的对象以显示引用计数是正确的。
有关测试方式的更多详细信息,请查看源代码注释。