previous page
next page

A Review of COM Persistence

Objects that have a persistent state should implement at least one persistence interfaceand, preferably multiple interfacesto provide the container with the most flexible choice of how it wants to save the object's state. Persistent state refers to data (typically properties and instance variables) that an object needs to have preserved before a container destroys the object. The container provides the saved state to the object after it re-creates the object so that the object can reinitialize itself to its previous state.

COM itself doesn't require an object to support persistence, nor does COM use such support if it's present in an object. COM simply documents a protocol by which clients can use any persistence support that an object provides. Often we refer to this persistence model as client-managed persistence because, in this model, the client determines where to save the persistent data (the medium) and when the save occurs.

COM defines some interfaces that model a persistence medium and, for some media, an implementation of the interfaces. Such interfaces typically use the naming convention IMedium, where the medium is Stream, Storage, PropertyBag, and so on. The medium interface has methods such as Read and Write that an object uses when loading and saving its state.

COM also documents interfaces that an object implements when it wants to support persistence into various media. Such interfaces typically use the naming convention IPersistMedium. The persistence interfaces have methods such as Load and Save that the client calls to request the object to restore or save its state. The client provides the appropriate medium interface to the object as an argument to the Load or Save requests. Figure 7.1 illustrates this model.

Figure 7.1. The client-managed persistence model


All IPersistMedium interfaces derive from the IPersist interface, which looks like this:

interface IPersist : IUnknown               
{ HRESULT GetClassID([out] CLSID* pclsid); }

A client uses the GetClassID method when it wants to save the state of an object. Typically, the client queries for IPersistMedium, calls the GetClassID method to obtain the CLSID for the object the client wants to save, and then writes the CLSID to the persistence medium. The client then requests the object to save its state into the medium. Restoring the object is the inverse operation: The client reads the CLSID from the medium, creates an instance of the class, queries for the IPersistMedium interface on that object, and requests the object to load its state from the medium.

A client might ask an object to save its state in two basic forms: a self-describing set of named properties or an opaque binary stream of bytes.

When an object provides its state as a self-describing set of named properties, it provides each property as a name-type-value tuple to its client. The client then stores the properties in the form most convenient to the client, such as text on HTML pages. The benefit of self-describing data, such as <param> tags and XML, is that one entity can write it and another can read it without tight coupling between the two.

It is more efficient for an object to provide its state as a binary stream of bytes because the object doesn't need to provide a property name or translate each property into the name-type-value tuple. In addition, the client doesn't need to translate the property to and from text. However, opaque streams contain machine dependencies, such as byte order and floating point/character set representations, unless specifically addressed by the object writing the stream.

ATL provides support for both forms of persistence. Before we explore ATL's persistence implementation, let's look at how you might implement COM persistence directly.

IPropertyBag and IPersistPropertyBag

ActiveX control containers that implement a "save as text" mechanism typically use IPropertyBag and IPersistPropertyBag. A container implements IPropertyBag, and a control implements IPersistPropertyBag to indicate that it can persist its state as a self-describing set of named properties:

interface IPropertyBag : public IUnknown {                      
  HRESULT Read([in] LPCOLESTR pszPropName,                      
    [in, out] VARIANT* pVar, [in] IErrorLog* pErrorLog);        
                                                                
  HRESULT Write([in] LPCOLESTR pszPropName, [in] VARIANT* pVar);
};                                                              
                                                                
interface IPersistPropertyBag : public IPersist {               
  HRESULT InitNew ();                                           
  HRESULT Load([in] IPropertyBag* pPropBag,                     
    [in] IErrorLog* pErrorLog);                                 
  HRESULT Save([in] IPropertyBag* pPropBag,                     
    [in] BOOL fClearDirty,                                      
    [in] BOOL fSaveAllProperties);                              
};                                                              

When a client (container) wants to have exact control over how individually named properties of an object are saved, it attempts to use an object's IPersistPropertyBag interface as the persistence mechanism. The client supplies a property bag to the object in the form of an IPropertyBag interface.

When the object wants to read a property in IPersistPropertyBag::Load, it calls IPropertyBag::Read. When the object saves properties in IPersistPropertyBag::Save, it calls IPropertyBag::Write. Each property is described with a name in pszPropName whose value is exchanged in a VARIANT. For read operations, the property bag provides the named property from the bag in the form specified by the input VARIANT, unless the type is VT_EMPTY; in that case, the property bag provides the property in any form that is convenient to the bag.

The information that the object provides for each property (name-type-value) during a save operation allows a client to save the property values as text, for instance. This is the primary reason a client might choose to support IPersistPropertyBag. The client records errors that occur during reading into the supplied error log.

IPropertyBag2 and IPersistPropertyBag2

The IPropertyBag interface doesn't give an object much information about the properties contained in the bag. Therefore, the newer interface IPropertyBag2 gives an object much greater access to information about the properties in a bag. Objects that support persistence using IPropertyBag2 naturally implement the IPersistPropertyBag2 interface.

interface IPropertyBag2 : public IUnknown {                     
  HRESULT Read([in] ULONG cProperties, [in] PROPBAG2* pPropBag, 
    [in] IErrorLog*  pErrLog, [out] VARIANT* pvarValue,          
    [out] HRESULT*   phrError);                                   
  HRESULT Write([in] ULONG cProperties, [in] PROPBAG2* pPropBag,
    [in] VARIANT*    pvarValue);                                   
  HRESULT CountProperties ([out] ULONG* pcProperties);          
  HRESULT GetPropertyInfo([in] ULONG iProperty,                 
    [in] ULONG cProperties,                                     
    [out] PROPBAG2* pPropBag, [out] ULONG* pcProperties);       
  HRESULT LoadObject([in] LPCOLESTR pstrName, [in] DWORD dwHint,
    [in] IUnknown* pUnkObject, [in] IErrorLog* pErrLog);        
};                                                              
                                                                
interface IPersistPropertyBag2 : public IPersist {              
  HRESULT InitNew ();                                           
  HRESULT Load ([in] IPropertyBag2* pPropBag,                   
    [in] IErrorLog* pErrLog);                                   
  HRESULT Save ([in] IPropertyBag2* pPropBag,                   
    [in] BOOL fClearDirty,                                      
    [in] BOOL fSaveAllProperties);                              
  HRESULT IsDirty();                                            
};                                                              

IPropertyBag2 is an enhancement of the IPropertyBag interface. IPropertyBag2 allows the object to obtain the number of properties in the bag and the type information for each property through the CountProperties and GetPropertyInfo methods. A property bag that implements IPropertyBag2 must also support IPropertyBag so that objects that support only IPropertyBag can access their properties. Likewise, an object that supports IPersistPropertyBag2 must also support IPersistPropertyBag so that it can communicate with property bags that support only IPropertyBag.

When the object wants to read a property in IPersistPropertyBag2::Load, it calls IPropertyBag2::Read. When the object saves properties in IPersistPropertyBag2::Save, it calls IPropertyBag2::Write. The client records errors that occur during Read with the supplied IErrorLog interface.

Implementing IPersistPropertyBag

Clients ask an object to initialize itself only once. When the client has no initial values to give the object, the client calls the object's IPersistPropertyBag::InitNew method. In this case, the object should initialize itself to default values. When the client has initial values to give the object, it loads the properties into a property bag and calls the object's IPersistPropertyBag::Load method. When the client wants to save the state of an object, it creates a property bag and calls the object's IPersistPropertyBag::Save method.

This is pretty straightforward to implement in an object. For example, the Demagogue object has three properties: its name, speech, and volume. Here's an example of an implementation to save and restore the following three properties from a property bag:

class ATL_NO_VTABLE CDemagogue
    : public IPersistPropertyBag, ... {
    BEGIN_COM_MAP(CDemagogue)
      ...
      COM_INTERFACE_ENTRY(IPersistPropertyBag)
      COM_INTERFACE_ENTRY2(IPersist, IPersistPropertyBag)
    END_COM_MAP()
    ...
    CComBSTR m_name;
    long     m_volume;
    CComBSTR m_speech;

    STDMETHODIMP Load(IPropertyBag *pBag, IErrorLog *pLog) {
      // Initialize the VARIANT to VT_BSTR
      CComVariant v ((BSTR) NULL);
      HRESULT hr = pBag->Read(OLESTR("Name"), &v, pLog);
      if (FAILED(hr)) return hr;
      m_name = v.bstrVal;

      // Initialize the VARIANT to VT_I4
      v = 0L;
      hr = pBag->Read(OLESTR("Volume"), &v, pLog);
      if (FAILED(hr)) return hr;
      m_volume = v.lVal;

      // Initialize the VARIANT to VT_BSTR
      v = (BSTR) NULL;
      hr = pBag->Read(OLESTR("Speech"), &v, pLog);
      if (FAILED (hr)) return hr;
      m_speech = v.bstrVal;

      return S_OK;
    }

    STDMETHODIMP Save(IPropertyBag *pBag,
      BOOL fClearDirty, BOOL /* fSaveAllProperties */) {
      CComVariant v = m_name;
      HRESULT hr = pBag->Write(OLESTR("Name"), &v);
      if (FAILED(hr)) return hr;

      v = m_volume;
      hr = pBag->Write(OLESTR("Volume"), &v);
      if (FAILED(hr)) return hr;

      v = m_speech;
      hr = pBag->Write(OLESTR("Speech"), &v);
      if (FAILED(hr)) return hr;

      if (fClearDirty) m_fDirty = FALSE;
      return hr;
    }
};

The IStream, IpersistStreamInit, and IPersistStream Interfaces

COM objects that want to save their state efficiently as a binary stream of bytes typically implement IPersistStream or IPersistStreamInit. An ActiveX control that has persistent state must, at a minimum, implement either IPersistStream or IPersistStreamInit. The two interfaces are mutually exclusive and generally shouldn't be implemented together. A control implements IPersistStreamInit when it wants to know when it is newly created, as opposed to when it has been created and reinitialized from its existing persistent state. The IPersistStream interface does not provide a means to inform the control that it is newly created. The existence of either interface indicates that the control can save and load its persistent state into a streamthat is, an implementation of IStream.

The IStream interface closely models the Win32 file API, and you can easily implement the interface on any byte-oriented media. COM provides two implementations of IStream, one that maps to an OLE Structured Storage file and another that maps to a memory buffer.

interface ISequentialStream : IUnknown {                        
    HRESULT Read([out] void *pv, [in] ULONG cb,                 
        [out] ULONG *pcbRead);                                  
                                                                
    HRESULT Write( [in] void const *pv, [in] ULONG cb,          
        [out] ULONG *pcbWritten);                               
                                                                
}                                                               
                                                                
interface IStream : ISequentialStream {                         
  HRESULT Seek([in] LARGE_INTEGER dlibMove,                     
               [in] DWORD dwOrigin,                             
               [out] ULARGE_INTEGER *plibNewPosition);          
  //...                                                         
}                                                               
                                                                
interface IPersistStreamInit : public IPersist {                
        HRESULT IsDirty();                                      
        HRESULT Load([in] LPSTREAM pStm);                       
        HRESULT Save([in] LPSTREAM pStm, [in] BOOL fClearDirty);
        HRESULT GetSizeMax([out] ULARGE_INTEGER*pCbSize);       
        HRESULT InitNew();                                      
};                                                              
                                                                
interface IPersistStream : public IPersist {                    
        HRESULT IsDirty();                                      
        HRESULT Load([in] LPSTREAM pStm);                       
        HRESULT Save([in] LPSTREAM pStm, [in] BOOL fClearDirty);
        HRESULT GetSizeMax([out] ULARGE_INTEGER*pCbSize);       
};                                                              

When a client wants an object to save its state as an opaque stream of bytes, it typically attempts to use the object's IPersistStreamInit interface as the persistence mechanism. The client supplies the stream into which the object saves in the form of an IStream interface.

When the client calls IPersistStreamInit::Load, the object reads its properties from the stream by calling IStream::Read. When the client calls IPersistStreamInit::Save, the object writes its properties by calling IStream::Write. Note that unless the object goes to the extra effort of handling the situation, the stream contains values in an architecture-specific byte order.

Most recent clients prefer to use an object's IPersistStreamInit interface; if it's not present, they fall back and try to use IPersistStream. However, older client code might attempt to use only an object's IPersistStream implementation. To be compatible with both types of clients, your object must implement IPersistStream. However, other containers ask for only IPersistStreamInit, so to be compatible with them, you need to implement that interface.

However, you're not supposed to implement both interfaces because then it's unclear to the container whether the object needs to be informed when it's newly created. Pragmatically, the best solution to this dilemma is to support both interfaces when your object doesn't care to be notified when it's newly created, even though this violates the specification for controls.

Although IPersistStreamInit doesn't derive from IPersistStream (it can't because of the mutual exclusion aspect of the interfaces), they have identical v-tables for all methods except the last: the InitNew method. Because of COM's binary compatibility, when your object doesn't need an InitNew call, your object can hand out an IPersistStreamInit interface when asked for an IPersistStream interface. So with a single implementation of IPersistStreamInit and an extra COM interface map entry, your object becomes compatible with a larger number of clients.

class ATL_NO_VTABLE CDemagogue : public IPersistStreamInit, ... {
...
BEGIN_COM_MAP(CDemagogue)
  ...
  COM_INTERFACE_ENTRY(IPersistStreamInit)
  COM_INTERFACE_ENTRY_IID(IID_IPersistStream, IPersistStreamInit)
  COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
END_COM_MAP()
...
};

Implementing IPersistStreamInit

Clients ask an object to initialize itself only once. When the client has no initial values to give the object, the client calls the object's IPersistSteamInit::InitNew method. In this case, the object should initialize itself to default values. When the client has initial values to give the object, the client opens the stream and calls the object's IPersistStreamInit::Load method. When the client wants to save the state of an object, it creates a stream and calls the object's IPersistStreamInit::Save method.

As in the property bag implementation, this is quite straightforward to implement in an object. Here's an example of an implementation for the Demagogue object to save and restore its three properties to and from a stream:

class CDemagogue : public IPersistStreamInit, ... {
    CComBSTR m_name;
    long     m_volume;
    CComBSTR m_speech;
    BOOL     m_fDirty;

    STDMETHODIMP IsDirty() { return m_fDirty ? s_OK : S_FALSE; }

    STDMETHODIMP Load(IStream* pStream) {
        HRESULT hr = m_name.ReadFromStream(pStream);
        if (FAILED (hr)) return hr;

        ULONG cb;
        hr = pStream->Read (&m_volume, sizeof (m_volume), &cb);
        if (FAILED (hr)) return hr;
        hr = m_speech.ReadFromStream(pStream);
        if (FAILED (hr)) return hr;
        m_fDirty = FALSE ;
        return S_OK;
    }

    STDMETHODIMP Save (IStream* pStream) {
        HRESULT hr = m_name.WriteToStream (pStream);
        if (FAILED(hr)) return hr;

        ULONG cb;
        hr = pStream->Write(&m_volume, sizeof (m_volume), &cb);
        if (FAILED(hr)) return hr;

        hr = m_speech.WriteToStream (pStream);
        return hr;
    }

    STDMETHODIMP GetSizeMax(ULARGE_INTEGER* pcbSize) {
        if (NULL == pcbSize) return E_POINTER;
        // Length of m_name
        pcbSize->QuadPart = CComBSTR::GetStreamSize(m_name);
        pcbSize->QuadPart += sizeof (m_volume);
        // Length of m_speech
        pcbSize->QuadPart += CComBSTR::GetStreamSize(m_speech);
        return S_OK;
    }

    STDMETHODIMP InitNew() { return S_OK; }
};

IStorage and IPersistStorage

An embeddable objectan object that you can store in an OLE linking and embedding container such as Microsoft Word and Microsoft Excelmust implement IPersistStorage. The container provides the object with an IStorage interface pointer. The IStorage references a structured storage medium. The storage object (the object implementing the IStorage interface) acts much like a directory object in a traditional file system. An object can use the IStorage interface to create new and open existing substorages and streams within the storage medium that the container provides.

interface IStorage : public IUnknown {                      
  HRESULT CreateStream([string,in] const OLECHAR* pwcsName, 
    [in] DWORD grfMode, [in] DWORD reserved1,               
    [in] DWORD reserved2,                                   
    [out] IStream** ppstm);                                 
                                                            
  HRESULT OpenStream([string,in] const OLECHAR* pwcsName,   
    [unique][in] void* reserved1, [in] DWORD grfMode,       
    [in] DWORD reserved2, [out] IStream** ppstm);           
                                                            
  HRESULT CreateStorage([string,in] const OLECHAR* pwcsName,
    [in] DWORD grfMode, [in] DWORD reserved1,               
    [in] DWORD reserved2,                                   
    [out] IStorage** ppstg);                                
                                                            
  HRESULT OpenStorage(                                      
    [string,unique,in] const OLECHAR* pwcsName,             
    [unique,in] IStorage* pstgPriority,                     
    [in] DWORD grfMode, [unique,in] SNB snbExclude,         
    [in] DWORD reserved, [out] IStorage** ppstg);           
                                                            
  // Following methods abbreviated for clarity...           
  HRESULT CopyTo( ... );                                    
  HRESULT MoveElementTo( ... )                              
  HRESULT Commit( ... )                                     
  HRESULT Revert(void);                                     
  HRESULT EnumElements( ... );                              
  HRESULT DestroyElement( . ., );    
  HRESULT RenameElement( ... );                             
  HRESULT SetElementTimes( ... );                           
  HRESULT SetClass( ... );                                  
  HRESULT SetStateBits( ... );                              
  HRESULT Stat( ... );                                      
};                                                          
                                                            
interface IPersistStorage : public IPersist {               
  HRESULT IsDirty ();                                       
  HRESULT InitNew ([unique,in] IStorage* pStg);             
  HRESULT Load ([unique,in] IStorage* pStg);                
  HRESULT Save ([unique,in] IStorage* pStgSave,             
    [in] BOOL fSameAsLoad);                                 
  HRESULT SaveCompleted ([unique,in] IStorage* pStgNew);    
  HRESULT HandsOffStorage ();                               
};                                                          

The IsDirty, InitNew, Load, and Save methods work much as the similarly named methods in the persistence interfaces you've seen previously. However, unlike streams, when a container hands an object an IStorage during the InitNew or Load calls, the object can hold on to the interface pointer (after AddRef-ing it, of course). This permits the object to read and write its state incrementally instead of all at once, as do the other persistence mechanisms. A container uses the HandsOffStorage and SaveCompleted methods to instruct the object to release the held interface and to give the object a new IStorage interface, respectively.

Typically, a container of embedded objects creates a storage to hold the objects. In this storage, the container creates one or more streams to hold the container's own state. In addition, for each embedded object, the container creates a substorage in which the container asks the embedded object to save its state.

This is a pretty heavy-weight persistence technique for many objects. Simple objects, such as in the Demagogue example used so far, don't really need this flexibility. Often, such objects simply create a stream in their given storage and save their state into the stream using an implementation of IPersistStreamInit. This is exactly what the ATL implementation of IPersistStorage does, so I defer creating a custom example here; you'll see the ATL implementation shortly.


previous page
next page
Converted from CHM to HTML with chm2web Pro 2.75 (unicode)