Chapter 7. Persistence in ATL

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

_images/07atl01.jpg

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

1interface IPersist : IUnknown
2{ 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 eachproperty 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:

 1interface IPropertyBag : public IUnknown {
 2  HRESULT Read([in] LPCOLESTR pszPropName,
 3    [in, out] VARIANT* pVar, [in] IErrorLog* pErrorLog);
 4
 5  HRESULT Write([in] LPCOLESTR pszPropName, [in] VARIANT* pVar);
 6};
 7
 8interface IPersistPropertyBag : public IPersist {
 9  HRESULT InitNew ();
10  HRESULT Load([in] IPropertyBag* pPropBag,
11    [in] IErrorLog* pErrorLog);
12  HRESULT Save([in] IPropertyBag* pPropBag,
13    [in] BOOL fClearDirty,
14    [in] BOOL fSaveAllProperties);
15};

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.

 1interface IPropertyBag2 : public IUnknown {
 2  HRESULT Read([in] ULONG cProperties, [in] PROPBAG2* pPropBag,
 3    [in] IErrorLog*  pErrLog, [out] VARIANT* pvarValue,
 4    [out] HRESULT*   phrError);
 5  HRESULT Write([in] ULONG cProperties, [in] PROPBAG2* pPropBag,
 6    [in] VARIANT*    pvarValue);
 7  HRESULT CountProperties ([out] ULONG* pcProperties);
 8  HRESULT GetPropertyInfo([in] ULONG iProperty,
 9    [in] ULONG cProperties,
10    [out] PROPBAG2* pPropBag, [out] ULONG* pcProperties);
11  HRESULT LoadObject([in] LPCOLESTR pstrName, [in] DWORD dwHint,
12    [in] IUnknown* pUnkObject, [in] IErrorLog* pErrLog);
13};
14
15interface IPersistPropertyBag2 : public IPersist {
16  HRESULT InitNew ();
17  HRESULT Load ([in] IPropertyBag2* pPropBag,
18    [in] IErrorLog* pErrLog);
19  HRESULT Save ([in] IPropertyBag2* pPropBag,
20    [in] BOOL fClearDirty,
21    [in] BOOL fSaveAllProperties);
22  HRESULT IsDirty();
23};

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:

 1class ATL_NO_VTABLE CDemagogue
 2    : public IPersistPropertyBag, ... {
 3    BEGIN_COM_MAP(CDemagogue)
 4      ...
 5      COM_INTERFACE_ENTRY(IPersistPropertyBag)
 6      COM_INTERFACE_ENTRY2(IPersist, IPersistPropertyBag)
 7    END_COM_MAP()
 8    ...
 9    CComBSTR m_name;
10    long     m_volume;
11    CComBSTR m_speech;
12
13    STDMETHODIMP Load(IPropertyBag *pBag, IErrorLog *pLog) {
14      // Initialize the VARIANT to VT_BSTR
15      CComVariant v ((BSTR) NULL);
16      HRESULT hr = pBag->Read(OLESTR("Name"), &v, pLog);
17      if (FAILED(hr)) return hr;
18      m_name = v.bstrVal;
19
20      // Initialize the VARIANT to VT_I4
21      v = 0L;
22      hr = pBag->Read(OLESTR("Volume"), &v, pLog);
23      if (FAILED(hr)) return hr;
24      m_volume = v.lVal;
25
26      // Initialize the VARIANT to VT_BSTR
27      v = (BSTR) NULL;
28      hr = pBag->Read(OLESTR("Speech"), &v, pLog);
29      if (FAILED (hr)) return hr;
30      m_speech = v.bstrVal;
31
32      return S_OK;
33    }
34
35    STDMETHODIMP Save(IPropertyBag *pBag,
36      BOOL fClearDirty, BOOL /* fSaveAllProperties */) {
37      CComVariant v = m_name;
38      HRESULT hr = pBag->Write(OLESTR("Name"), &v);
39      if (FAILED(hr)) return hr;
40
41      v = m_volume;
42      hr = pBag->Write(OLESTR("Volume"), &v);
43      if (FAILED(hr)) return hr;
44
45      v = m_speech;
46      hr = pBag->Write(OLESTR("Speech"), &v);
47      if (FAILED(hr)) return hr;
48
49      if (fClearDirty) m_fDirty = FALSE;
50      return hr;
51    }
52};

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 stream – that 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.

 1interface ISequentialStream : IUnknown {
 2    HRESULT Read([out] void *pv, [in] ULONG cb,
 3        [out] ULONG *pcbRead);
 4
 5    HRESULT Write( [in] void const *pv, [in] ULONG cb,
 6        [out] ULONG *pcbWritten);
 7
 8}
 9
10interface IStream : ISequentialStream {
11  HRESULT Seek([in] LARGE_INTEGER dlibMove,
12               [in] DWORD dwOrigin,
13               [out] ULARGE_INTEGER *plibNewPosition);
14  //...
15}
16
17interface IPersistStreamInit : public IPersist {
18        HRESULT IsDirty();
19        HRESULT Load([in] LPSTREAM pStm);
20        HRESULT Save([in] LPSTREAM pStm, [in] BOOL fClearDirty);
21        HRESULT GetSizeMax([out] ULARGE_INTEGER*pCbSize);
22        HRESULT InitNew();
23};
24
25interface IPersistStream : public IPersist {
26        HRESULT IsDirty();
27        HRESULT Load([in] LPSTREAM pStm);
28        HRESULT Save([in] LPSTREAM pStm, [in] BOOL fClearDirty);
29        HRESULT GetSizeMax([out] ULARGE_INTEGER*pCbSize);
30};

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.

 1class ATL_NO_VTABLE CDemagogue : public IPersistStreamInit, ... {
 2...
 3BEGIN_COM_MAP(CDemagogue)
 4  ...
 5  COM_INTERFACE_ENTRY(IPersistStreamInit)
 6  COM_INTERFACE_ENTRY_IID(IID_IPersistStream, IPersistStreamInit)
 7  COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
 8END_COM_MAP()
 9...
10};

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:

 1class CDemagogue : public IPersistStreamInit, ... {
 2    CComBSTR m_name;
 3    long     m_volume;
 4    CComBSTR m_speech;
 5    BOOL     m_fDirty;
 6
 7    STDMETHODIMP IsDirty() { return m_fDirty ? s_OK : S_FALSE; }
 8
 9    STDMETHODIMP Load(IStream* pStream) {
10        HRESULT hr = m_name.ReadFromStream(pStream);
11        if (FAILED (hr)) return hr;
12
13        ULONG cb;
14        hr = pStream->Read (&m_volume, sizeof (m_volume), &cb);
15        if (FAILED (hr)) return hr;
16        hr = m_speech.ReadFromStream(pStream);
17        if (FAILED (hr)) return hr;
18        m_fDirty = FALSE ;
19        return S_OK;
20    }
21
22    STDMETHODIMP Save (IStream* pStream) {
23        HRESULT hr = m_name.WriteToStream (pStream);
24        if (FAILED(hr)) return hr;
25
26        ULONG cb;
27        hr = pStream->Write(&m_volume, sizeof (m_volume), &cb);
28        if (FAILED(hr)) return hr;
29
30        hr = m_speech.WriteToStream (pStream);
31        return hr;
32    }
33
34    STDMETHODIMP GetSizeMax(ULARGE_INTEGER* pcbSize) {
35        if (NULL == pcbSize) return E_POINTER;
36        // Length of m_name
37        pcbSize->QuadPart = CComBSTR::GetStreamSize(m_name);
38        pcbSize->QuadPart += sizeof (m_volume);
39        // Length of m_speech
40        pcbSize->QuadPart += CComBSTR::GetStreamSize(m_speech);
41        return S_OK;
42    }
43
44    STDMETHODIMP InitNew() { return S_OK; }
45};

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.

 1interface IStorage : public IUnknown {
 2  HRESULT CreateStream([string,in] const OLECHAR* pwcsName,
 3    [in] DWORD grfMode, [in] DWORD reserved1,
 4    [in] DWORD reserved2,
 5    [out] IStream** ppstm);
 6
 7  HRESULT OpenStream([string,in] const OLECHAR* pwcsName,
 8    [unique][in] void* reserved1, [in] DWORD grfMode,
 9    [in] DWORD reserved2, [out] IStream** ppstm);
10
11  HRESULT CreateStorage([string,in] const OLECHAR* pwcsName,
12    [in] DWORD grfMode, [in] DWORD reserved1,
13    [in] DWORD reserved2,
14    [out] IStorage** ppstg);
15
16  HRESULT OpenStorage(
17    [string,unique,in] const OLECHAR* pwcsName,
18    [unique,in] IStorage* pstgPriority,
19    [in] DWORD grfMode, [unique,in] SNB snbExclude,
20    [in] DWORD reserved, [out] IStorage** ppstg);
21
22  // Following methods abbreviated for clarity...
23  HRESULT CopyTo( ... );
24  HRESULT MoveElementTo( ... )
25  HRESULT Commit( ... )
26  HRESULT Revert(void);
27  HRESULT EnumElements( ... );
28  HRESULT DestroyElement( . ., );
29  HRESULT RenameElement( ... );
30  HRESULT SetElementTimes( ... );
31  HRESULT SetClass( ... );
32  HRESULT SetStateBits( ... );
33  HRESULT Stat( ... );
34};
35
36interface IPersistStorage : public IPersist {
37  HRESULT IsDirty ();
38  HRESULT InitNew ([unique,in] IStorage* pStg);
39  HRESULT Load ([unique,in] IStorage* pStg);
40  HRESULT Save ([unique,in] IStorage* pStgSave,
41    [in] BOOL fSameAsLoad);
42  HRESULT SaveCompleted ([unique,in] IStorage* pStgNew);
43  HRESULT HandsOffStorage ();
44};

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.

ATL Persistence Implementation Classes

ATL provides implementations of the IPersistPropertyBag. IPersistStreamInit and IPersistStorage interfaces called IPersistPropertyBagImpl, IPersistStreamInitImpl, and IPersistStorageImpl, respectively. Each template class takes one parameter, the name of the deriving class. You add support for these three persistence interfaces to your object like this:

 1class ATL_NO_VTABLE CDemagogue :
 2    public IPersistPropertyBagImpl<CDemagogue>,
 3    public IPersistStreamInitImpl<CDemagogue>,
 4    public IPersistStorageImpl<CDemagogue> {
 5 ...
 6 BEGIN_COM_MAP(CDemagogue)
 7     ...
 8     COM_INTERFACE_ENTRY2(IPersist, IPersistPropertyBag)
 9     COM_INTERFACE_ENTRY(IPersistPropertyBag)
10     COM_INTERFACE_ENTRY(IPersistStreamInit)
11     COM_INTERFACE_ENTRY(IPersistStream)
12     COM_INTERFACE_ENTRY(IPersistStorage)
13 END_COM_MAP()
14     ...
15};

Don’t forget to add the COM MAP enTRy for IPersist. All three persistence interfaces derive from IPersist, not IUnknown, so you need to respond affirmatively to queries for IPersist. Note also that you need to use the COM_INTERFACE_ENTRY2 (or COM_INTERFACE_ENTRY_IID) macro because multiple base classes derive from IPersist.

The Property Map

The ATL implementation of these three persistence interfaces requires that your object provide a table that describes all the properties that should be saved and loaded during a persistence operation. This table is called the property map. ATL uses the property map of a class for two independent purposes: persistence support and control property page support (discussed in Chapter 11, “ActiveX Controls”).

The various property map entries enable you to do the following:

  • Define the properties of the COM object that the ATL persistence-implementation classes save and restore during a persistence request

  • Define the member variables of the C++ class that the ATL persistence-implementation classes save and restore during a persistence request

  • Define the property pages that a class uses

  • Associate a property with its property page

The CDemagogue class’s property map looks like this:

1BEGIN_PROP_MAP(CDemagogue)
2  PROP_ENTRY_EX("Speech", DISPID_SPEECH,
3    CLSID_NULL, IID_ISpeaker)
4  PROP_ENTRY_EX("Volume", DISPID_VOLUME,
5    CLSID_NULL, IID_ISpeaker)
6  PROP_ENTRY_EX("Name", DISPID_NAME,
7    CLSID_NULL, IID_INamedObject)
8END_PROP_MAP()

The BEGIN_PROP_MAP and END_PROP_MAP macros define a class’s property map. You list the persistent properties of an object in the property map using the PROP_ENTRY and PROP_ENTRY_EX macros. The PROP_ENTRY macro describes a property that the persistence implementation can access via the default dispatch interfacein other words, the interface retrieved when you query for IID_IDispatch. You use the PROP_ENTRY_EX macro to describe a property that the persistence implementation must access using some other specified dispatch interface. Both macros require the name of the property, the property’s DISPID, and the CLSID of the property’s associated property page (discussed in Chapter 11). The PROP_ENTRY_EX macro also requires the IID of the dispatch interface that supports the specified property; the PROP_ENTRY macro uses IID_IDispatch.

1PROP_ENTRY (szDesc, dispid, clsid)
2PROP_ENTRY_EX (szDesc, dispid, clsid, iidDispatch)
3PROP_DATA_ENTRY (szDesc, member, vt)

You might also want to load and save member variables of your object that are not accessible via a dispatch interface. The PROP_DATA_ENTRY macro enables you to specify the name of a property, the member variable containing the value, and the VARIANT type of the variable, like so:

1BEGIN_PROP_MAP(CBullsEye)
2  PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
3  PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)
4  ...
5END_PROP_MAP()

Effectively, the PROP_DATA_ENTRY macro causes the persistence implementation to reach into your object, access the specified member variable for the length implied by the VARIANT type, place the data into a VARIANT, and write the VARIANT to the persistent medium. This is quite handy when you have a member variable that is a VARIANT-compatible type. However, it doesn’t work for noncompatible types such as indexed properties. Note that a PROP_PAGE macro also is used to associate aproperty to a property page; I discuss its use in Chapter 11, “ActiveX Controls.” The persistence implementations skip entries in the property map made with the PROP_PAGE macro.

One caution: Don’t add a PROP_ENTRY, PROP_ENTRY_EX, or PROP_DATA_ENTRY macro that has a property name with an embedded space character. Some relatively popular containers, such as Visual Basic 6, provide an implementation of IPropertyBag that cannot handle names with embedded spaces.

When you have a member variable that you want to load and save during a persistence operation, and that variable is not a VARIANT-compatible type (for example, an indexed or array variable), the property map mechanism doesn’t help. You have to override the appropriate member functions of the persistence implementation classes, and read and write the variable explicitly. To do this, you need to know the basic structure of the ATL persistence implementation.

The Persistence Implementations

Let’s look at how the persistence implementations work, using the property bag persistence implementation as the example. All persistence implementations are similar.

The Property Map

The property map macros basically add a static member function called GetPropertyMap to your class. GetPropertyMap returns a pointer to an array of ATL_PROPMAP_ENTRY structures. The structure looks like this:

1struct ATL_PROPMAP_ENTRY {
2    LPCOLESTR szDesc;
3    DISPID dispid;
4    const CLSID* pclsidPropPage;
5    const IID* piidDispatch;
6    DWORD dwOffsetData;
7    DWORD dwSizeData;
8    VARTYPE vt;
9};

For example, here’s a property map and the resulting macro expansion:

1BEGIN_PROP_MAP(CDemagogue)
2    PROP_ENTRY("Speech", DISPID_SPEECH, CLSID_NULL)
3    PROP_ENTRY_EX("Name", DISPID_NAME, CLSID_NULL,
4      IID_INamedObject)
5    PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
6END_PROP_MAP()

This property map expands to this:

 1__if_not_exists(__ATL_PROP_NOTIFY_EVENT_CLASS) {
 2    typedef ATL::_ATL_PROP_NOTIFY_EVENT_CLASS
 3        __ATL_PROP_NOTIFY_EVENT_CLASS;
 4}
 5static ATL::ATL_PROPMAP_ENTRY* GetPropertyMap() {
 6  static ATL::ATL_PROPMAP_ENTRY pPropMap[] = {
 7  {OLESTR("Speech"), DISPID_SPEECH, &CLSID_NULL,
 8      __uudiof(IID_IDispatch), 0, 0, 0},
 9  {OLESTR("Name"), DISPID_ NAME, &CLSID_NULL,
10      &IID_INamedObject, 0, 0, 0},
11  {OLESTR("_cx"), 0, &CLSID_NULL, NULL,
12      offsetof(_PropMapClass, m_sizeExtent.cx),
13      sizeof(((_PropMapClass*)0)-> m_sizeExtent.cx), VT_UI4},
14  {NULL, 0, NULL, &IID_NULL, 0, 0, 0}
15  };
16  return pPropMap;
17}

The szDesc field of the structure holds the name of the property. It’s used only by the property bag persistence implementation.

The dispid field contains the property’s dispatch identifier. All the persistence implementations need this so they can access the property via one of the object’s IDispatch implementations by calling the Invoke method.

The pclsidPropPage field contains a pointer to the CLSID for the property page associated with the object. It’s not used during persistence.

The piidDispatch field contains a pointer to the IID of the dispatch interface that supports this property. The specified dispid is unique to this interface.

Only PROP_DATA_ENTRY macros use the last three fields. The dwOffsetData field contains the offset of the specified member variable from the beginning of a class instance. The dwSizeData field contains the size of the variable in bytes, and the vt field contains the variable’s VARTYPE (VARIANT type enumeration code).

The various persistence implementations basically iterate over this map and load or save the properties listed. For properties listed using PROP_ENTRY and PROP_ENTRY_EX, the implementations call IDispatch::Invoke with the specified dispid to get or put the property.

Invoke transfers each property via a VARIANT. The stream persistence implementation simply wraps the variant in a CComVARIANT instance and uses its ReadFromStream and WriteToStream methods to do all the hard work. Therefore, stream persistence supports all VARIANT types that the CComVARIANT persistence implementation supports (discussed in Chapter 3, “ATL Smart Types”). The property bag implementation has it even easier because property bags deal directly in VARIANT s.

For properties listed using the PROP_DATA_ENTRY macro, things aren’t quite so simple. The IPersistStreamInit implementation directly accesses the object instance at the specified offset for the specified length. This reads or writes the specified number of bytes directly to or from the object.

However, the IPersistPropertyBag implementation must read and write properties held in a VARIANT. Therefore, this implementation copies the member variable of the object to a VARIANT before writing the property to the bag, and copies a VARIANT to the member variable after reading the property from the bag. The current implementation of IPersistPropertyBag persistence supports only a limited set of VARIANT types; worse, it silently fails to load and save properties with any VARTYPE s other than these:

  • VT_UI1, VT_I1: Read and write the variable as a BYTE.

  • VT_BOOL: Reads and writes the variable as a VARIANT_BOOL.

  • VT_UI2, VT_I2: Read and write the variable as a short.

  • VT_UI4, VT_I4, VT_INT, VT_UINT: Read and write the variable as a long.

  • VT_BSTR: Reads and writes the variable as a BSTR.

  • Any other VT_*: Silently fail.

IPersistPropertyBagImpl

The IPersistPropertyBagImpl<T> class implements the IPersistPropertyBag interface methods. IPersistPropertyBag, like all the persistence interfaces, derives from IPersist, which has one method: GetClassID.

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

All the persistence-implementation classes have the same implementation of GetClassID. They call a static member function named GetObjectCLSID method to retrieve the CLSID. This method must be in the deriving class (your object’s class) or one of its base classes.

 1template <class T>
 2class ATL_NO_VTABLE IPersistPropertyBagImpl
 3    : public IPersistPropertyBag {
 4public:
 5    ...
 6    STDMETHOD(GetClassID)(CLSID *pClassID) {
 7        ATLTRACE(atlTraceCOM, 2, _T("IPersistPropertyBagImpl::GetClassID\n"));
 8        if (pClassID == NULL)
 9            return E_POINTER;
10        *pClassID = T::GetObjectCLSID();
11        return S_OK;
12    }};

Normally, your class obtains its GetObjectCLSID static member function from CComCoClass:

1template <class T, const CLSID* pclsid = &CLSID_NULL>
2class CComCoClass {
3public:
4    ...
5    static const CLSID& WINAPI GetObjectCLSID() {return *pclsid;}
6};

This implies that a class must be createable for it to use the persistence classes. This is reasonable because it doesn’t do much good to save a class to some persistent medium and then be unable to create a new instance when loading the object from that medium.

The IPersistPropertyBagImpl<T> class also implements the remaining IPersistPropertyBag methods, including, for example, the Load method. IPersistPropertyBagImpl<T>::Load calls T::IPersistPropertyBag_Load to do most of the work. This allows your class to provide this method when it needs a custom implementation of Load. Normally, your object (class T) doesn’t provide an IPersistPropertyBag_Load method, so this call vectors to a default implementation provided by the base class IPersistPropertyBagImpl<T>::IPersistPropertyBag_Load method. The default implementation calls the global function AtlIPersistPropertyBag_Load. This global function iterates over the property map and, for each entry in the map, loads the property from the property bag:

 1template <class T>
 2class ATL_NO_VTABLE IPersistPropertyBagImpl
 3    : public IPersistPropertyBag {
 4public:
 5    ...
 6    // IPersistPropertyBag
 7    //
 8    STDMETHOD(Load)(LPPROPERTYBAG pPropBag,
 9        LPERRORLOG pErrorLog) {
10        ATLTRACE(atlTraceCOM, 2, _T("IPersistPropertyBagImpl::Load\n"));
11        T* pT = static_cast<T*>(this);
12        ATL_PROPMAP_ENTRY* pMap = T::GetPropertyMap();
13        ATLASSERT(pMap != NULL);
14        return pT->IPersistPropertyBag_Load(pPropBag,
15            pErrorLog, pMap);
16    }
17    HRESULT IPersistPropertyBag_Load(LPPROPERTYBAG pPropBag,
18        LPERRORLOG pErrorLog, ATL_PROPMAP_ENTRY* pMap) {
19        T* pT = static_cast<T*>(this);
20        HRESULT hr = AtlIPersistPropertyBag_Load(pPropBag,
21            pErrorLog, pMap, pT, pT->GetUnknown());
22        if (SUCCEEDED(hr))
23            pT->m_bRequiresSave = FALSE;
24        return hr;
25    }
26    ...
27};

This implementation structure provides three places where we can override methods and provide custom persistence support for a non-VARIANT-compatible property. We can override the Load method itself, in effect directly implementing the IPersistPropertyBag method. Alternatively, we can let ATL implement Load while our object implements IPersistPropertyBag_Load. Finally, we can let ATL implement Load and IPersistPropertyBag_Load while we provide a replacement global function called AtlIPersistPropertyBag_Load and play some linker tricks so that our object uses our global function instead of the ATL-provided one.

The most natural method is to implement Load. Normally, in this implementation, you call the base class Load method to read all properties described in the property map, and then read any custom, non-VARIANT-compatible properties:

 1HRESULT CMyObject::Load(LPPROPERTYBAG pPropBag,
 2    LPERRORLOG pErrorLog) {
 3    HRESULT hr =
 4IPersistPropertyBagImpl<CMyObject>::Load(pPropBag,pErrorLog);
 5    if (FAILED (hr)) return hr;
 6
 7    // Read an array of VT_I4
 8    // This requires us to create a "name" for each array element
 9    // Read each element as a VARIANT, then re-create the array
10    ...
11}

This approach has a few disadvantages. It’s a minor point, but the object now requires four methods for its persistence implementation: its Load, the base class Load, the base class IPersistPropertyBag_Load, and the AtlIPersistPropertyBag_Load. We could copy the base class Load implementation into the object’s Load method, but that makes the object more fragile because future versions of ATL might change its persistence-implementation technique.

Another slight disadvantage to this approach is that it is clear from the ATL implementation that the ATL designers intended for your object to override IPersistPropertyBag_Load. Note the following code fragment from the default implementation of Load:

1STDMETHOD(Load)(LPPROPERTYBAG pPropBag, LPERRORLOG pErrorLog) {
2  ...
3  T* pT = static_cast<T*>(this);
4  ...
5  return pT->IPersistPropertyBag_Load(pPropBag, pErrorLog, pMap);
6}

Instead of directly calling IPersistPropertyBag_Load, which is present in the same class as the Load method, the code calls the method using a pointer to the deriving classyour object’s class. This provides the same functionality as making the method virtual, without the overhead of a virtual function.

Generally, the best solution is to let the object provide its own implementation of the IPersistPropertyBag_Load method. In this implementation, the object can call the global function AtlIPersistProperyBag_Load and save any non-VARIANT-compatible properties it possessed. Here’s an example from the BullsEye control described in Chapter 11, “ActiveX Controls.” It contains a property that is an array of long integers. This can’t be described as a VARIANT-compatible type because it’s not a SAFEARRAY, so I can’t list the property in the property map.

 1HRESULT CBullsEye::IPersistPropertyBag_Load(
 2    LPPROPERTYBAG pPropBag, LPERRORLOG pErrorLog,
 3    ATL_PROPMAP_ENTRY* pMap) {
 4    if (NULL == pPropBag) return E_POINTER;
 5
 6    // Load the properties described in the PROP_MAP
 7    HRESULT hr = AtlIPersistPropertyBag_Load(pPropBag, pErrorLog,
 8       pMap, this, GetUnknown());
 9    if (SUCCEEDED(hr)) m_bRequiresSave = FALSE;
10
11    if (FAILED (hr)) return hr;
12
13    // Load the indexed property - RingValues
14    // Get the number of rings
15    short sRingCount;
16    get_RingCount (&sRingCount);
17    // For each ring, read its value
18    for (short nIndex = 1; nIndex <= sRingCount; nIndex++) {
19
20        // Create the base property name
21        CComBSTR bstrName = OLESTR("RingValue");
22
23        // Create ring number as a string
24        CComVariant vRingNumber = nIndex;
25        hr = vRingNumber.ChangeType (VT_BSTR);
26        ATLASSERT (SUCCEEDED (hr));
27
28        // Concatenate the two strings to form property name
29        bstrName += vRingNumber.bstrVal;
30
31        // Read ring value from the property bag
32        CComVariant vValue = 0L;
33        hr = pPropBag->Read(bstrName, &vValue, pErrorLog);
34        ATLASSERT (SUCCEEDED (hr));
35        ATLASSERT (VT_I4 == vValue.vt);
36
37        if (FAILED (hr)) {
38            hr = E_UNEXPECTED;
39            break;
40        }
41
42        // Set the ring value
43        put_RingValue (nIndex, vValue.lVal);
44     }
45
46     if (SUCCEEDED(hr)) m_bRequiresSave = FALSE;
47     return hr;
48 }

The Save method works symmetrically. The IPersistPropertyBagImpl<T> class implements the Save method. IPersistPropertyBagImpl<T>::Save calls T::IPersistPropertyBag_Save to do the work. Again, your object (class T) doesn’t normally provide an IPersistPropertyBag_Save method, so this call vectors to a default implementation that the base class IPersistPropertyBagImpl<T>::IPersistPropertyBag_Save method provides. The default implementation calls the global function AtlIPersistPropertyBag_Save. This global function iterates over the property map and, for each entry in the map, saves the property to the property bag.

 1template <class T>
 2class ATL_NO_VTABLE IPersistPropertyBagImpl
 3    : public IPersistPropertyBag {
 4public:
 5    ...
 6    // IPersistPropertyBag
 7    //
 8    STDMETHOD(Save)(LPPROPERTYBAG pPropBag, BOOL fClearDirty,
 9        BOOL fSaveAllProperties) {
10        ATLTRACE(atlTraceCOM, 2, _T("IPersistPropertyBagImpl::Save\n"));
11        T* pT = static_cast<T*>(this);
12        ATL_PROPMAP_ENTRY* pMap = T::GetPropertyMap();
13        ATLASSERT(pMap != NULL);
14        return pT->IPersistPropertyBag_Save(pPropBag,
15            fClearDirty, fSaveAllProperties, pMap);
16    }
17
18    HRESULT IPersistPropertyBag_Save(LPPROPERTYBAG pPropBag,
19    BOOL fClearDirty, BOOL fSaveAllProperties,
20        ATL_PROPMAP_ENTRY* pMap) {
21        T* pT = static_cast<T*>(this);
22        HRESULT hr;
23        hr = AtlIPersistPropertyBag_Save(pPropBag, fClearDirty,
24            fSaveAllProperties, pMap, pT, pT->GetUnknown());
25        if (fClearDirty && SUCCEEDED(hr)) {
26            pT->m_bRequiresSave=FALSE;
27        }
28        return hr;
29    }
30};

Finally, IPersistPropertyBagImpl implements the InitNew method this way:

1STDMETHOD(InitNew)() {
2    ATLTRACE(atlTraceCOM, 2,
3        _T("IPersistPropertyBagImpl::InitNew\n"));
4    T* pT = static_cast<T*>(this);
5    pT->m_bRequiresSave = TRUE;
6    return S_OK;
7}

Therefore, you need to override InitNew directly when you have any initialization to perform when there are no properties to load.

IPersistStreamInitImpl

The implementation contained in IPersistStreamInitImpl is quite similar to the one just described. The Load and Save methods call the IPersistStreamInit_Load and IPersistStreamInit_Save methods, which are potentially provided by the deriving object but typically provided by the default implementation in IPersistStreamInitImpl. These implementations call the global helper functions AtlIPersistStreamInit_Load and AtlIPersistStreamInit_Save.

 1template <class T>
 2class ATL_NO_VTABLE IPersistStreamInitImpl
 3  : public IPersistStreamInit {
 4public:
 5  ...
 6  // IPersistStream
 7  STDMETHOD(Load)(LPSTREAM pStm) {
 8    ATLTRACE(atlTraceCOM, 2,
 9      _T("IPersistStreamInitImpl::Load\n"));
10    T* pT = static_cast<T*>(this);
11    return pT->IPersistStreamInit_Load(pStm,
12      T::GetPropertyMap());
13  }
14
15  HRESULT IPersistStreamInit_Load(LPSTREAM pStm,
16    ATL_PROPMAP_ENTRY* pMap) {
17    T* pT = static_cast<T*>(this);
18    HRESULT hr =
19      AtlIPersistStreamInit_Load(pStm, pMap, pT,
20        pT->GetUnknown());
21    if (SUCCEEDED(hr)) pT->m_bRequiresSave = FALSE;
22    return hr;
23  }
24
25  STDMETHOD(Save)(LPSTREAM pStm, BOOL fClearDirty) {
26    T* pT = static_cast<T*>(this);
27    ATLTRACE(atlTraceCOM, 2,
28      _T("IPersistStreamInitImpl::Save\n"));
29    return pT->IPersistStreamInit_Save(pStm, fClearDirty,
30      T::GetPropertyMap());
31  }
32
33  HRESULT IPersistStreamInit_Save(LPSTREAM pStm,
34    BOOL fClearDirty, ATL_PROPMAP_ENTRY* pMap) {
35    T* pT = static_cast<T*>(this);
36    return AtlIPersistStreamInit_Save(pStm, fClearDirty,
37      pMap, pT, pT->GetUnknown());
38  }
39};

IPersistStreamInitImpl also implements the InitNew method this way:

1STDMETHOD(InitNew)() {
2    ATLTRACE(atlTraceCOM, 2,
3      _T("IPersistStreamInitImpl::InitNew\n"));
4    T* pT = static_cast<T*>(this);
5    pT->m_bRequiresSave = TRUE;
6  return S_OK;
7}

Therefore, as with property bags, you need to override InitNew directly when you have any initialization to perform when there are no properties to load.

The implementation of the IsDirty method assumes the presence of a member variable named m_bRequiresSave somewhere in your class hierarchy.

1STDMETHOD(IsDirty)() {
2    ATLTRACE(atlTraceCOM, 2,
3      _T("IPersistStreamInitImpl::IsDirty\n"));
4    T* pT = static_cast<T*>(this);
5    return (pT->m_bRequiresSave) ? S_OK : S_FALSE;
6}

The persistence implementations originally assumed that only ActiveX controls would use them, as if controls were the only objects that needed a persistence implementation. Although ATL has greatly reduced the coupling between the control classes and the persistence implementation, the CComControlBase class normally provides the m_bRequiresSave variable and the SetDirty and Getdirty helper functions usually used to access the variable.

1class ATL_NO_VTABLE CComControlBase {
2public:
3    void SetDirty(BOOL bDirty) { m_bRequiresSave = bDirty; }
4
5    // Obtain the dirty state for the control
6    BOOL GetDirty() { return m_bRequiresSave; }
7    ...
8    unsigned m_bRequiresSave:1;
9};

To use the persistence-implementation classes in an object that doesn’t derive from CComControlBase, you need to define the m_bRequiresSave variable in your class hierarchy somewhere. Typically, for convenience, you also define the SetDirty and Getdirty helper methods. Noncontrols can use this class to provide this persistence support:

1class ATL_NO_VTABLE CSupportDirtyBit {
2public:
3  CSupportDirtyBit() : m_bRequiresSave(FALSE) {}
4  void SetDirty(BOOL bDirty) {
5    m_bRequiresSave = bDirty ? TRUE : FALSE;
6  }
7  BOOL GetDirty() { return m_bRequiresSave ? TRUE : FALSE; }
8  BOOL m_bRequiresSave;
9};

Finally, the IPersistStreamInitImpl class provides the following implementation of GetSizeMax:

 1STDMETHOD(GetSizeMax)(ULARGE_INTEGER* pcbSize) {
 2  HRESULT hr = S_OK;
 3  T* pT = static_cast<T*>(this);
 4
 5  if (pcbSize == NULL)
 6    return E_POINTER;
 7
 8  ATL_PROPMAP_ENTRY* pMap = T::GetPropertyMap();
 9  ATLENSURE(pMap != NULL);
10
11  // Start the size with the size of the ATL version
12  // we write out.
13  ULARGE_INTEGER nSize;
14  nSize.HighPart = 0;
15  nSize.LowPart = sizeof(DWORD);
16
17  CComPtr<IDispatch> pDispatch;
18  const IID* piidOld = NULL;
19  for (int i = 0; pMap[i].pclsidPropPage != NULL; i++) {
20    if (pMap[i].szDesc == NULL)
21      continue;
22
23    // check if raw data entry
24    if (pMap[i].dwSizeData != 0) {
25      ULONG ulSize=0;
26      //Calculate stream size for BSTRs special case
27      if (pMap[i].vt == VT_BSTR) {
28        void* pData = (void*)(pMap[i].dwOffsetData +
29          (DWORD_PTR)pT);
30        ATLENSURE(
31          pData >= (void*)(DWORD_PTR)pMap[i].dwOffsetData
32          && pData >= (void*)(DWORD_PTR)pT );
33        BSTR bstr=*reinterpret_cast<BSTR*>(pData);
34        ulSize=CComBSTR::GetStreamSize(bstr);
35      } else {
36        ulSize = pMap[i].dwSizeData;
37      }
38      nSize.QuadPart += ulSize;
39      continue;
40    }
41
42    CComVariant var;
43    if (pMap[i].piidDispatch != piidOld) {
44      pDispatch.Release();
45      if (FAILED(pT->GetUnknown()->
46        QueryInterface(*pMap[i].piidDispatch,
47        (void**)&pDispatch))) {
48        ATLTRACE(atlTraceCOM, 0,
49          _T("Failed to get a dispatch pointer for "
50             "property #%i\n"), i);
51        hr = E_FAIL;
52        break;
53      }
54      piidOld = pMap[i].piidDispatch;
55    }
56
57    if (FAILED(pDispatch.GetProperty(pMap[i].dispid, &var))) {
58      ATLTRACE(atlTraceCOM, 0,
59        _T("Invoked failed on DISPID %x\n"),
60        pMap[i].dispid);
61      hr = E_FAIL;
62      break;
63    }
64    nSize.QuadPart += var.GetSize();
65  }
66  *pcbSize = nSize;
67  return hr;
68}

Previous versions of ATL simply returned E_NOTIMPL from the GetSizeMax function. As of this writing, the MSDN documentation claims that ATL still does not implement this function. In any event, the implementation that ATL 8 actually provides is fairly straightforward. GetSizeMax loops through all the entries in the property map and accumulates the total size each entry requires in the nSize variable. If it finds a “raw”PROP_DATA_ENTRY, it simply increments the total nSize by the size ofthe member specified in the PROP_DATA_ENTRY macro. Alternatively, if it finds a PROP_ENTRY or PROP_ENTRY_EX in the map, it queries the object for the specified IDispatch interface and wraps the resulting interface pointer in a CComPtr. Recall from Chapter 3, “ATL Smart Types,” that the CComPtr smart pointer template class provides a convenient specialization for IDispatch that exposes property “getters” and “setters.”GetSizeMax uses CComPtr<IDispatch>::GetProperty to retrieve a VARIANT value for the property specified in the property map entry. The function wraps the returned VARIANT in a CComVariant and uses that class’s GetSize function to increment the nSize total for the object.

IPersistStorageImpl

The ATL implementation of IPersistStorage is very simplistic. The Save method creates a stream called "Contents" within the provided storage and depends on an IPersistStreamInit implementation to write the contents of the stream.

 1template <class T>
 2class ATL_NO_VTABLE IPersistStorageImpl
 3    : public IPersistStorage {
 4public:
 5    STDMETHOD(Save)(IStorage* pStorage, BOOL fSameAsLoad) {
 6        ATLTRACE(atlTraceCOM, 2,
 7          _T("IPersistStorageImpl::Save\n"));
 8        CComPtr<IPersistStreamInit> p;
 9        p.p = IPSI_GetIPersistStreamInit();
10        HRESULT hr = E_FAIL;
11        if (p != NULL) {
12            CComPtr<IStream> spStream;
13            static LPCOLESTR vszContents = OLESTR("Contents");
14            hr = pStorage->CreateStream(vszContents,
15              STGM_READWRITE | STGM_SHARE_EXCLUSIVE | STGM_CREATE,
16              0, 0, &spStream);
17            if (SUCCEEDED(hr)) hr = p->Save(spStream, fSameAsLoad);
18        }
19        return hr;
20    }
21    ...
22};

Similarly, the Load method opens the "Contents" stream and uses the IPersistStreamInit implementation to read the contents of the stream.

 1STDMETHOD(Load)(IStorage* pStorage) {
 2    ATLTRACE(atlTraceCOM, 2, _T("IPersistStorageImpl::Load\n"));
 3    CComPtr<IPersistStreamInit> p;
 4    p.p = IPSI_GetIPersistStreamInit();
 5    HRESULT hr = E_FAIL;
 6    if (p != NULL) {
 7        CComPtr<IStream> spStream;
 8        hr = pStorage->OpenStream(OLESTR("Contents"), NULL,
 9          STGM_DIRECT | STGM_SHARE_EXCLUSIVE, 0, &spStream);
10        if (SUCCEEDED(hr)) hr = p->Load(spStream);
11    }
12    return hr;
13}

The InitNew and IsDirty implementations retrieve the object’s IPersistStreamInit interface pointer (using a helper function to get the interface) and delegate to the same named method in that interface:

 1STDMETHOD(IsDirty)(void) {
 2    ATLTRACE(atlTraceCOM, 2,
 3      _T("IPersistStorageImpl::IsDirty\n"));
 4    CComPtr<IPersistStreamInit> p;
 5    p.p = IPSI_GetIPersistStreamInit();
 6    return (p != NULL) ? p->IsDirty() : E_FAIL;
 7}
 8
 9STDMETHOD(InitNew)(IStorage*) {
10    ATLTRACE(atlTraceCOM, 2,
11      _T("IPersistStorageImpl::InitNew\n"));
12    CComPtr<IPersistStreamInit> p;
13    p.p = IPSI_GetIPersistStreamInit();
14    return (p != NULL) ? p->InitNew() : E_FAIL;
15}

One of the main reasons an object supports IPersistStorage is so the object can incrementally read and write its state. Unfortunately, the ATL implementation doesn’t support this. The implementation does not cache the provided IStorage interface provided during the Load and Save calls, so it’s not available for later incremental reads and writes. Not caching the IStorage interface makes implementing the last two methods trivial, however:

 1STDMETHOD(SaveCompleted)(IStorage* /* pStorage */) {
 2  ATLTRACE(atlTraceCOM, 2,
 3    _T("IPersistStorageImpl::SaveCompleted\n"));
 4  return S_OK;
 5}
 6
 7STDMETHOD(HandsOffStorage)(void) {
 8  ATLTRACE(atlTraceCOM, 2, _T("IPersistStorageImpl::HandsOffStorage\n"));
 9  return S_OK;
10}

Generally, most objects that need the functionality IPersistStorage provides can’t use the implementation ATL provides. They must derive directly from IPersistStorage and implement all the methods explicitly.

Additional Persistence Implementations

Given what you’ve already seen, you might find it interesting to demonstrate an additional persistence implementation built using functionality we’ve already covered.

IPersistMemory

Let’s look at implementing the IPersistMemory interface:

1interface IPersistMemory : IPersist {
2    HRESULT IsDirty();
3    HRESULT Load([in, size_is(cbSize)] LPVOID pvMem,
4        [in] ULONG cbSize);
5    HRESULT Save([out, size_is(cbSize)] LPVOID pvMem,
6        [in] BOOL fClearDirty, [in] ULONG cbSize);
7    HRESULT GetSizeMax([out] ULONG* pCbSize);
8    HRESULT InitNew();
9};

The IPersistMemory interface allows a client to request that the object save its state to a fixed-size memory block (identified with a void*). The interface is very similar to IPersistStreamInit, except that it uses a memory block instead of an expandable IStream. The cbSize argument to the Load and Save methods indicates the amount of memory accessible through pvMem. The IsDirty, GetSizeMax, and InitNew methods are semantically identical to those in IPersistStreamInit.

Implementing the IPersistMemory interface

You’ve seen that ATL provides the IPersistStreamInitImpl class that saves and restores the state of an object to a stream. The COM API CreateStreamOnHGlobal returns an IStream implementation that reads and writes to a global memory block. We can use the two together and easily implement IPersistMemory using the functionality IPersistStreamInitImpl provides.

With the exception of the Load and Save methods, all methods in our IPersistMemory implementation simply delegate to the same named method in ATL’s IPersistStreamInit implementation.

  1template <class T, class S = IPersistStreamInit>
  2class ATL_NO_VTABLE IPersistMemoryImpl : public IPersistMemory {
  3public:
  4    // IPersist
  5    STDMETHODIMP GetClassID(CLSID *pClassID) {
  6        ATLTRACE(atlTraceCOM, 2, _T("IPersistMemoryImpl::GetClassID\n"));
  7        T* pT = static_cast<T*>(this);
  8        S* psi = static_cast <S*> (pT);
  9        return psi->GetClassID(pClassID);
 10    }
 11
 12    // IPersistMemory
 13    STDMETHODIMP IsDirty() {
 14        ATLTRACE(atlTraceCOM, 2,
 15            _T("IPersistMemoryImpl::IsDirty\n"));
 16        T* pT = static_cast<T*>(this);
 17        S* psi = static_cast <S*> (pT);
 18        return psi->IsDirty();
 19    }
 20
 21    STDMETHODIMP Load(void* pvMem, ULONG cbSize) {
 22        ATLTRACE(atlTraceCOM, 2,
 23            _T("IPersistMemoryImpl::Load\n"));
 24        T* pT = static_cast<T*>(this);
 25
 26        // Get memory handle. We need an actual HGLOBAL
 27        // here because it's required for CreateStreamOnHGlobal
 28        HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, cbSize);
 29        if (h == NULL) return E_OUTOFMEMORY;
 30        LPVOID pv = GlobalLock(h);
 31        if (!pv) return E_OUTOFMEMORY;
 32
 33        // Copy to memory block
 34        CopyMemory (pv, pvMem, cbSize);
 35        CComPtr<IStream> spStrm;
 36        // Create stream on memory
 37        HRESULT hr = CreateStreamOnHGlobal (h, TRUE, &spStrm);
 38        if (FAILED (hr)) {
 39            GlobalUnlock (h);
 40            GlobalFree (h);
 41            return hr;
 42        }
 43        // Stream now owns the memory
 44
 45        // Load from stream
 46        S* psi = static_cast <S*> (pT);
 47        hr = psi->Load (spStrm);
 48
 49        GlobalUnlock (h);
 50        return hr;
 51    }
 52
 53    STDMETHODIMP Save(void* pvMem, BOOL fClearDirty,
 54        ULONG cbSize) {
 55        ATLTRACE(atlTraceCOM, 2,
 56            _T("IPersistMemoryImpl::Save\n"));
 57        T* pT = static_cast<T*>(this);
 58
 59        // Get memory handle
 60        HGLOBAL h = GlobalAlloc (GMEM_MOVEABLE, cbSize);
 61        if (NULL == h) return E_OUTOFMEMORY;
 62
 63        // Create stream on memory
 64        CComPtr<IStream> spStrm;
 65        HRESULT hr = CreateStreamOnHGlobal (h, TRUE, &spStrm);
 66        if (FAILED (hr)) {
 67            GlobalFree (h);
 68            return hr;
 69        }
 70        // Stream now owns the memory
 71
 72        // Set logical size of stream to physical size of memory
 73        // (Global memory block allocation rounding causes
 74        // differences)
 75        ULARGE_INTEGER uli;
 76        uli.QuadPart = cbSize ;
 77        spStrm->SetSize (uli);
 78
 79        S* psi = static_cast <S*> (pT);
 80        hr = psi->Save (spStrm, fClearDirty);
 81        if (FAILED (hr)) return hr;
 82
 83        LPVOID pv = GlobalLock (h);
 84        if (!pv) return E_OUTOFMEMORY;
 85        // Copy to memory block
 86        CopyMemory (pvMem, pv, cbSize);
 87
 88        return hr;
 89    }
 90
 91    STDMETHODIMP GetSizeMax(ULONG* pcbSize) {
 92        if (pcbSize == NULL) return E_POINTER;
 93        *pcbSize = 0;
 94
 95        T* pT = static_cast<T*>(this);
 96        S* psi = static_cast <S*> (pT);
 97        ULARGE_INTEGER uli ;
 98        uli.QuadPart = 0;
 99        HRESULT hr = psi->GetSizeMax (&uli);
100        if (SUCCEEDED (hr)) *pcbSize = uli.LowPart;
101
102        return hr;
103    }
104
105    STDMETHODIMP InitNew() {
106        ATLTRACE(atlTraceCOM, 2,
107            _T("IPersistMemoryImpl::InitNew\n"));
108        T* pT = static_cast<T*>(this);
109        S* psi = static_cast <S*> (pT);
110        return psi->InitNew();
111    }
112};

Notice the use of static_cast to downcast the this pointer to the deriving class and then up-cast the resulting pointer to an IPersistStreamInit*. We do this so that we get a compile-time error when the class deriving from IPersistMemoryImpl doesn’t also derive from IPersistStreamInit. This approach does require the deriving class to not implement IPersistStreamInit in an “unusual” way, such as on a tear-off interface or via aggregation.

Alternatively, we could have retrieved the IPersistStreamInit interface using QueryInterface:

1T* pT = static_cast<T*>(this);
2CComQIPtr<S> psi = pT->GetUnknown() ;

However, then we might find out at runtime that no IPersistStreamInit implementation is available, which means the object then ends up saying that it implements IPersistMemory without the capability to do so. I prefer compile-time errors whenever possible, so I chose the former approach accepting its limitations.

Using the IPersistMemoryImpl Template Class

An object uses this IPersistMemory implementation this way:

 1class ATL_NO_VTABLE CDemagogue :
 2    ...
 3    public IPersistStreamInitImpl<CDemagogue>,
 4    public IPersistMemoryImpl<CDemagogue>,
 5    public CSupportDirtyBit {
 6    ...
 7BEGIN_COM_MAP(CDemagogue)
 8    ...
 9    COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
10    COM_INTERFACE_ENTRY(IPersistStreamInit)
11    COM_INTERFACE_ENTRY(IPersistMemory)
12END_COM_MAP()

Adding Marshal-by-Value Semantics Using Persistence

When you pass an interface pointer as a parameter to a remote (out-of-apartment) method call, the default in COM is to pass by reference. In other words, the object stays where it is, and only a reference to the object is given to the recipient of the call. This typically means that references to the object involve round-trips back to the object, which can be quite expensive. An object can override this pass-by-reference default by implementing the IMarshal interface.

The primary reason most developers implement IMarshal on an object is to give it pass-by-value semantics. In other words, when you pass an interface pointer to a remote method call, you prefer that COM pass a copy of the object to the method. All references to the object are then local and do not involve round-trips back to the “original” object. When an object implements IMarshal in such a way that it has pass-by-value semantics, we typically say that the object marshals by value.

 1interface IMarshal : public IUnknown {
 2  STDMETHOD GetUnmarshalClass([in] REFIID riid,
 3    [unique,in] void* pv,
 4    [in] DWORD dwDestContext,
 5    [unique,in] void* pvDestContext,
 6    [in] DWORD mshlflags, [out] CLSID* pCid);
 7
 8  STDMETHOD GetMarshalSizeMax([in] REFIID riid,
 9    [unique,in] void* pv,
10    [in] DWORD dwDestContext,
11    [unique,in] void* pvDestContext,
12    [in] DWORD mshlflags,
13    [out] DWORD* pSize) ;
14
15  STDMETHOD MarshalInterface([unique,in] IStream* pStm,
16    [in] REFIID riid, [unique][in] void* pv,
17    [in] DWORD dwDestContext,
18    [unique,in] void* pvDestContext,
19    [in] DWORD mshlflags);
20
21  STDMETHOD UnmarshalInterface([unique,in] IStream* pStm,
22    [in] REFIID riid, [out] void** ppv);
23
24  STDMETHOD ReleaseMarshalData([unique,in] IStream* pStm);
25
26  STDMETHOD DisconnectObject([in] DWORD dwReserved);
27};

Given a complete implementation of IPersistStream or IPersistStreamInit, it’s quite easy to build a marshal-by-value implementation of IMarshal.

A class typically implements marshal-by-value by returning its own CLSID as the result of the GetUnmarshalClass method. The IPersistStream::GetClassID method produces the needed CLSID.

The GetMarshalSizeMax method must return the number of bytes needed to save the persistent state of the object into a stream. The IPersistStream::GetSizeMax method produces the needed size.

The MarshalInterface and UnmarshalInterface methods need to write and read, respectively, the persistent state of the object into the provided stream. Therefore, we can use the Save and Load methods of IPersistStream for this functionality.

ReleaseMarshalData and DisconnectObject method can simply return S_OK.

Here’s a template class that uses an object’s IPersistStreamInit interface to provide a marshal-by-value implementation. [1] Once again, I decided to down-cast and up-cast using static_cast to obtain the IPersistStreamInit interface, so I receive an error at compile time when the deriving class doesn’t implement IPersistStreamInit.

 1template <class T>
 2class ATL_NO_VTABLE IMarshalByValueImpl : public IMarshal {
 3
 4  STDMETHODIMP GetUnmarshalClass(REFIID /* riid */,
 5    void* /* pv */,
 6    DWORD /* dwDestContext */,
 7    void* /* pvDestContext */,
 8    DWORD /* mshlflags */, CLSID *pCid) {
 9    T* pT = static_cast<T*>(this);
10    IPersistStreamInit* psi =
11      static_cast<IPersistStreamInit*>(pT);
12    return psi->GetClassID (pCid);
13  }
14
15  STDMETHODIMP GetMarshalSizeMax(REFIID /* riid */,
16    void* /* pv */,
17    DWORD /* dwDestContext */,
18    void* /* pvDestContext */,
19    DWORD /* mshlflags */, DWORD* pSize) {
20    T* pT = static_cast<T*>(this);
21    IPersistStreamInit* psi =
22      static_cast <IPersistStreamInit*> (pT);
23
24    ULARGE_INTEGER uli = { 0 };
25
26    HRESULT hr = psi->GetSizeMax(&uli);
27    if (SUCCEEDED (hr)) *pSize = uli.LowPart;
28    return hr;
29  }
30
31  STDMETHODIMP MarshalInterface(IStream *pStm, REFIID /* riid */,
32    void* /* pv */, DWORD /* dwDestContext */,
33    void* /* pvDestCtx */, DWORD /* mshlflags */) {
34    T* pT = static_cast<T*>(this);
35    IPersistStreamInit* psi = static_cast <IPersistStreamInit*> (pT);
36    return psi->Save(pStm, FALSE);
37  }
38
39  STDMETHODIMP UnmarshalInterface(IStream *pStm, REFIID riid,
40    void **ppv) {
41    T* pT = static_cast<T*>(this);
42    IPersistStreamInit* psi =
43      static_cast <IPersistStreamInit*> (pT);
44    HRESULT hr = psi->Load(pStm);
45    if (SUCCEEDED (hr)) hr = pT->QueryInterface (riid, ppv);
46    return hr;
47  }
48  STDMETHODIMP ReleaseMarshalData(IStream* /* pStm */) {
49    return S_OK;
50  }
51
52  STDMETHODIMP DisconnectObject(DWORD /* dwReserved */) {
53    return S_OK;
54  }
55};

You can use this template class to provide a marshal-by-value implementation for your object. You need to derive your class from the previous IMarshalByValueImpl class (to get the IMarshal method implementations) and the IPersistStreamInitImpl class. You must also add a COM_INTERFACE_ENTRY for IMarshal to the class’s interface map. Here’s an example:

 1class ATL_NO_VTABLE CDemagogue :
 2    ...
 3    public IPersistStreamInitImpl<CDemagogue>,
 4    public CSupportDirtyBit,
 5    public IMarshalByValueImpl<CDemagogue> {
 6    ...
 7  BEGIN_COM_MAP(CDemagogue)
 8      COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
 9      COM_INTERFACE_ENTRY(IPersistStreamInit)
10      COM_INTERFACE_ENTRY(IMarshal)
11  END_COM_MAP()
12  ...
13};

Note that adding marshal-by-value support to your class this way means that all instances of the class use pass-by-value semantics. It is not possible to pass one object instance by reference and another instance by value (assuming that both instances have the same marshaling context: in-proc, local, or different machine).

Summary

Many objects need some support for persistence, and ATL provides an easily extensible, table-driven implementation of the IPersistStream[Init] and the IPersistPropertyBag interfaces. These implementations save and load the properties described by the class’s property map entries. By overriding the appropriate methods, you can extend this persistence support for data types that property map entries do not support. The ATL implementation of IPersistStorage allows the object to be embedded into an OLE container but doesn’t take advantage of many of the capabilities of the IStorage medium.

Using and extending the stream persistence support that ATL provides allows an object to offer additional functionality to its clients. For example, you’ve seen how to implement IPersistMemory support to your object (which MFC-based containers prefer). In addition, you can easily add marshal-by-value semantics to your class by reusing the stream persistence functionality.