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.