Writing and Reading COM objects using CArchive

Imagine you have to work with your custom COM objects in a MFC application. These COM objects are stored in a container (client), which is a GUI intensive MFC application with quite sophisticated class hierarchy. In many C++ classes in the client, I have a situation where a classic C++ class possesses one or more COM objects. Additionally, these classes implement standard Serialize() method to store their contents to the CArchive stream. Below is a pseudo code of typical client C++ class:

class Dummy
{
   void Serialize(CArchive& ar);

private:
   double m_Double;
   IPersistStreamInit* m_ipCom1;        // raw interface pointer
   CComPtr<IPersistStreamInit> m_spCom2;// smart interface pointer defined in AtlBase.h
   IPersistStreamInitPtr m_spCom3;      // smart interface pointer based on _com_ptr_t
                                        // created by #import directive
};


The question here is: Is there a way to serialize COM objects within the standard Serialize() method? To get the answer, I turned a few ATL/MFC books upside-down, do some Internet browsing and ... got zero hits. This forced me to find my own solution. There is a chance that a solution (better than presented below) exists, but I simply could not find it.

In order to store a COM class on the CArchive (using the solution below), the COM class MUST implement the IPersistStreamInit interface. This interface knows how to read/write via IStream interface, which is passed as an argument of the IPersistStreamInit::Save/Load methods.

If you refer to the documentation about IStream you will find the CreateStreamOnHGlobal() API function call that creates an IStream object on the top of HGLOBAL memory block. I can take advantage of this and store the initialized memory block into the CArchive. Below is a pseudo code that does just that:


SaveComToArchive(CArchive& ar, IPersistStreamInit* pObject)
{
   // Note: This is a pseudo code - it will not compile!
    
   // Create IStream
   IStream* pStream = CreateStreamOnHGlobal();
    
   // Write COM object's class UUID!
   pStream->WriteObjectClassUUID(pObject->GetClassUUID());
    
   // save the object
   pObject->Save(pStream);
    
   // Get the memory block maintained by the IStream
   HGLOBAL hMem;
   hMem = pStream->GetGlobalMemory();
    
   // Copy the block on the CArchive
   ar.Write(hMem,size_of_hMem_block);
    
   // Release the IStream
   pStream->Release;
}

The pseudo code for loading a COM object from the CArchive is very similar. It turns out that a code like this works just fine, and I believe, It should work for all COM objects that properly implement IPersistStreamInit interface. Since global memory is used as a buffer, it is obvious that we will use this solution for COM that stores data of small and medium size.

Below you will find a link to the implemented code. There you will find two functions and two operators, that have the following prototype:

HRESULT WriteComObjectToArchive(CArchive& ar, IPersistStreamInit* ipObject);
HRESULT ReadComObjectFromArchive(CArchive& ar, IPersistStreamInit** ippObject);
CArchive& operator << (CArchive& ar, IPersistStreamInit* ipObject);
CArchive& operator >> (CArchive& ar, IPersistStreamInit** ippObject);

Note: I deliberatelly put IPersistStreamInit* in each call rather than IUnknown*. Namely, I wanted to emphasize that COM object must implement the IPersistStreamInit interface.

Let me show how to use these functions. If you look at the implementation of the ReadComObjectFromArchive() (please, see the source code), you will see, that there are two options concerned with ippObject argument:

  • (a) If *ippObject==NULL, create a new COM object and load data for it,
  • (b) If *ippObject!=NULL load the data into existing COM object.
Both options are presented in sample call bellow. In most cases you will be interested in the (a) case only.

void Dummy::Serialize(CArchive& ar)
{
   HRESULT hr;
   if(ar.IsStoring()) {
      ar << m_Double;
      hr = WriteComObjectToArchive(ar,m_ipCom1); // raw pointer
      // if(FAILED(hr)) ... check hr here to catch errors
      hr = WriteComObjectToArchive(ar,m_spCom2); // CComPtr<> pointer
      hr = WriteComObjectToArchive(ar,m_ipCom3); // _com_ptr_t<> pointer
   }
   // loading
   else {
      ar >> m_Double;
      
      // case a) - create new COM objects during the read
      
      m_ipCom1 = NULL; // it must be NULL in order to create a new object!
      hr = ReadComObjectFromArchive(ar,&m_ipCom1);
      // if the call is successful, the m_ipCom1 points to a new COM object,
      // with reference count set to 1!
      
      // The &operator of CComPtr<> will reset the pointer to NULL for us and
      // release any previous object.
      hr = ReadComObjectFromArchive(ar,&m_spCom2);
      // The &operator of _com_ptr_t<> will reset the pointer to NULL for us and
      // release any previous object.
      hr = ReadComObjectFromArchive(ar,&m_spCom3);
      // if the calls were successful, the m_spCom2 and m_spCom3 point to 
      // a new COM objects, both with reference count set to 1!
     
      // case b) - use existing COM objects and simply load the data!
      
      // m_ipCom1 must point to IPersistStreamInit of a valid COM object.
      hr = ReadComObjectFromArchive(ar,&m_ipCom1);
      // if the call is successful, the m_ipCom1 was loaded with data from stream
      // and the reference count remained the same. 
      
      IPersistStreamInit* pPSI;
      // The &operator of CComPtr<> will reset the pointer to NULL for us, so
      // we can't use it in the case b).
      pPSI = m_spCom2;  // The pointer inside the m_spCom2 must be non NULL and valid.
      hr = ReadComObjectFromArchive(ar,&pPSI);
      
      pPSI = m_spCom3;  // The pointer inside the m_spCom3 must be non NULL and valid.
      hr = ReadComObjectFromArchive(ar,&pPSI);
      // if the calls were successful, the m_spCom2 and m_spCom3 now hold new data
   }
}

Now, I show you the same function as above, but using the operators and omitting most comments. In this version you do not get the HRESULT. Operators will throw _com_error exception in the case of error.

void Dummy::Serialize(CArchive& ar)
{
   if(ar.IsStoring()) {
      ar << m_Double;
      ar << m_ipCom1; // raw pointer
      ar << m_spCom2; // CComPtr<> pointer
      ar << m_spCom3; // _com_ptr_t<> pointer
   }
   // loading
   else {
      ar >> m_Double;
      
      // case a) - create new COM objects during the read
      
      m_ipCom1 = NULL; // it must be NULL in order to create a new object!
      ar >> &m_ipCom1;
      ar >> &m_spCom2;
      ar >> &m_spCom3;
     
      // case b) - use existing COM objects and simply load the data!
      
      ar >> &m_ipCom1;
      
      IPersistStreamInit* pPSI;
      pPSI = m_spCom2; 
      ar >> &pPSI;
      
      pPSI = m_spCom3; 
      ar >> &pPSI;
   }
}

Download source - 4 KB The source code includes two short files ArchiveCom.h(.cpp). Add these files to your MFC project and add the #include "ArchiveCom.h" line in appropriate source implementation (CPP) files.

Download demo project - 29 KB The demo project creates a simple COM named Dummy as an EXE server. This COM implements IPersistentStreamInit interface and a few properties. The demo also creates a client named Test, which has a simple CTestDummy C++ class that uses the several Dummy COM objects and implements Serialize() method. In the main() of the demo a CArchive is created, the CTestDummy class object is initialized and written to the archive. A second (empty) CTestDummy object is created is then loaded from the CArchive stream.

To see the implementation of the CTestDummy class, open the Test.cpp file.



Comments

  • Yet another way ...

    Posted by Legacy on 10/25/1999 12:00am

    Originally posted by: doublej

    You can take advantage of compound document for this task.

    1. Derive your document from COleDocument
    2. In constructor call EnableCompoundFile( TRUE )
    3. In Serialize use member m_lpRootStg to create substorages and streams for any tree structure of your com objects.

    Example (no error checking):

    .
    .
    COleStreamFile file;
    file.CreateStream( m_lpRootStg, _T("Name"), mode );
    CComPtr<IPersistStream>* pPS;
    pYourObject->QueryInterface( IID_IPersistStream, (LPVOID*)&pPS );
    OleSaveToStream( pPS, file.GetStream() );
    file.Close();
    .
    .


    !!! Headache !!!:
    All records made by CArchive are in compound files written to stream 'CONTENTS'. If this stream is empty then the compound file is deleted by MFC internal code (after serialize is finished), so you must write at least one byte using CArchive!!!

    This method has many advantages. You can structure your data in file, load only necessary parts, leave old data in file (for undo or history), take advantage of transactions, etc.

    Reply
  • There's a simpler way already built into MFC

    Posted by Legacy on 05/26/1999 12:00am

    Originally posted by: Douglas Peterson

    This works for COM objects that implement IPersistStream or IPersistStreamInit.
    
    

    CMyDoc::Serialize(CArchive& ar)
    {
    COleVariant varTemp;

    if (ar.IsStoring())
    {
    varTemp.vt = VT_UNKNOWN;
    varTemp.punkVal = m_ipCom1;
    ar << varTemp;

    varTemp.punkVal = m_spCom2;
    ar << varTemp;

    varTemp.punkVal = m_spCom3;
    ar << varTemp;

    // Don't allow Release when varTemp is destroyed
    varTemp.punkVal = NULL;
    }
    else
    {
    varTemp.vt = VT_UNKNOWN;
    varTemp.punkVal = NULL;
    ar >> varTemp;
    m_ipCom1 = (ICom2Int*)varTemp.punkVal;
    m_ipCom1->AddRef();

    ar >> varTemp;
    m_spCom2 = varTemp.punkVal;

    ar >> varTemp;
    m_spCom3 = varTemp.punkVal;
    }
    }

    Note that this is a bit hackish because COleVariant does not have a constructor and operator= for IUnknown*. Why it has serialization support but not ctor support is beyond me.

    You could use this class to do it:

    class COleVariantEx : public COleVariant
    {
    public:
    operator = (LPUNKNOWN punk)
    {
    Clear();
    vt = VT_UNKNOWN;
    punkVal = punk;
    if (punk)
    punkVal->AddRef();
    }
    };

    CMyDoc::Serialize(CArchive& ar)
    {
    COleVariantEx varTemp;

    if (ar.IsStoring())
    {
    varTemp = m_ipCom1;
    ar << varTemp;

    varTemp = m_spCom2;
    ar << varTemp;

    varTemp = m_spCom3;
    ar << varTemp;
    }
    else
    {
    varTemp = (IUnknown*)NULL;

    ar >> varTemp;
    m_ipCom1 = (ICom1Int*)varTemp.punkVal;
    m_ipCom1->AddRef();

    ar >> varTemp;
    m_spCom2 = varTemp.punkVal;

    ar >> varTemp;
    m_spCom3 = varTemp.punkVal;
    }
    }

    Also note that operator<</>>(CArchive, COleVariant) uses an undocumented MFC class called CArchiveStream. This takes a CArchive reference and gives you an object that you can pass to IPersistXXX as IStream*. It's quite handy and you can use it yourself if you like. Check out the code for operator<</>>(CArchive, COleVariant) in "Src\OLEVAR.CPP". CArchiveStream is in "Include\AFXPRIV2.H" and "Src\ARCSTRM.CPP".

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • With JRebel, developers get to see their code changes immediately, fine-tune their code with incremental changes, debug, explore and deploy their code with ease (both locally and remotely), and ultimately spend more time coding instead of waiting for the dreaded application redeploy to finish. Every time a developer tests a code change it takes minutes to build and deploy the application. JRebel keeps the app server running at all times, so testing is instantaneous and interactive.

  • Ever-increasing workloads and the challenge of containing costs leave companies conflicted by the need for increased processing capacity while limiting physical expansion. Migration to HP's new generation of increased-density rack-and-blade servers can address growing demands for compute capacity while reducing costly sprawl. Sponsored by: HP and Intel® Xeon® processors Intel, the Intel logo, and Xeon Inside are trademarks of Intel Corporation in the U.S. and/or other countries. HP is the sponsor …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds