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

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 aBYTE
.VT_BOOL
: Reads and writes the variable as aVARIANT_BOOL
.VT_UI2
,VT_I2
: Read and write the variable as ashort
.VT_UI4
,VT_I4
,VT_INT
,VT_UINT
: Read and write the variable as along
.VT_BSTR
: Reads and writes the variable as aBSTR
.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.