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.
All IPersistMedium interfaces derive from
the IPersist interface, which looks like this:
interface IPersist : IUnknown
{ HRESULT GetClassID([out] CLSID* pclsid); }
A client uses the GetClassID method
when it wants to save the state of an object. Typically, the client
queries for IPersistMedium, calls the GetClassID
method to obtain the CLSID for the object the client wants to save,
and then writes the CLSID to the persistence medium. The client
then requests the object to save its state into the medium.
Restoring the object is the inverse operation: The client reads the
CLSID from the medium, creates an instance of the class, queries
for the IPersistMedium interface on that object, and
requests the object to load its state from the medium.
A client might ask an object to save its state
in two basic forms: a self-describing set of named properties or an
opaque binary stream of bytes.
When an object provides its state as a
self-describing set of named properties, it provides each property
as a name-type-value tuple to its client. The client then stores
the properties in the form most convenient to the client, such as
text on HTML pages. The benefit of self-describing data, such as
<param> tags and XML, is that one entity can write
it and another can read it without tight coupling between the
two.
It is more efficient for an object to provide
its state as a binary stream of bytes because the object doesn't
need to provide a property name or translate each property into the
name-type-value tuple. In addition, the client doesn't need to
translate the property to and from text. However, opaque streams
contain machine dependencies, such as byte order and floating
point/character set representations, unless specifically addressed
by the object writing the stream.
ATL provides support for both forms of
persistence. Before we explore ATL's persistence implementation,
let's look at how you might implement COM persistence directly.
IPropertyBag and
IPersistPropertyBag
ActiveX control containers that implement a
"save as text" mechanism typically use IPropertyBag and
IPersistPropertyBag. A container implements
IPropertyBag, and a control implements
IPersistPropertyBag to indicate that it can persist its
state as a self-describing set of named properties:
interface IPropertyBag : public IUnknown {
HRESULT Read([in] LPCOLESTR pszPropName,
[in, out] VARIANT* pVar, [in] IErrorLog* pErrorLog);
HRESULT Write([in] LPCOLESTR pszPropName, [in] VARIANT* pVar);
};
interface IPersistPropertyBag : public IPersist {
HRESULT InitNew ();
HRESULT Load([in] IPropertyBag* pPropBag,
[in] IErrorLog* pErrorLog);
HRESULT Save([in] IPropertyBag* pPropBag,
[in] BOOL fClearDirty,
[in] BOOL fSaveAllProperties);
};
When a client (container) wants to have exact
control over how individually named properties of an object are
saved, it attempts to use an object's IPersistPropertyBag
interface as the persistence mechanism. The client supplies a
property bag to the object in the form of an IPropertyBag
interface.
When the object wants to read a property in
IPersistPropertyBag::Load, it calls
IPropertyBag::Read. When the object saves properties in
IPersistPropertyBag::Save, it calls
IPropertyBag::Write. Each property is described with a
name in pszPropName whose value is exchanged in a
VARIANT. For read operations, the property bag provides
the named property from the bag in the form specified by the input
VARIANT, unless the type is VT_EMPTY; in that
case, the property bag provides the property in any form that is
convenient to the bag.
The information that the
object provides for each property (name-type-value) during a save
operation allows a client to save the property values as text, for
instance. This is the primary reason a client might choose to
support IPersistPropertyBag. The client records errors
that occur during reading into the supplied error log.
IPropertyBag2 and
IPersistPropertyBag2
The IPropertyBag interface doesn't give
an object much information about the properties contained in the
bag. Therefore, the newer interface IPropertyBag2 gives an
object much greater access to information about the properties in a
bag. Objects that support persistence using IPropertyBag2
naturally implement the IPersistPropertyBag2
interface.
interface IPropertyBag2 : public IUnknown {
HRESULT Read([in] ULONG cProperties, [in] PROPBAG2* pPropBag,
[in] IErrorLog* pErrLog, [out] VARIANT* pvarValue,
[out] HRESULT* phrError);
HRESULT Write([in] ULONG cProperties, [in] PROPBAG2* pPropBag,
[in] VARIANT* pvarValue);
HRESULT CountProperties ([out] ULONG* pcProperties);
HRESULT GetPropertyInfo([in] ULONG iProperty,
[in] ULONG cProperties,
[out] PROPBAG2* pPropBag, [out] ULONG* pcProperties);
HRESULT LoadObject([in] LPCOLESTR pstrName, [in] DWORD dwHint,
[in] IUnknown* pUnkObject, [in] IErrorLog* pErrLog);
};
interface IPersistPropertyBag2 : public IPersist {
HRESULT InitNew ();
HRESULT Load ([in] IPropertyBag2* pPropBag,
[in] IErrorLog* pErrLog);
HRESULT Save ([in] IPropertyBag2* pPropBag,
[in] BOOL fClearDirty,
[in] BOOL fSaveAllProperties);
HRESULT IsDirty();
};
IPropertyBag2 is an enhancement of the
IPropertyBag interface. IPropertyBag2 allows the
object to obtain the number of properties in the bag and the type
information for each property through the CountProperties
and GetPropertyInfo methods. A property bag that
implements IPropertyBag2 must also support
IPropertyBag so that objects that support only
IPropertyBag can access their properties. Likewise, an object that
supports IPersistPropertyBag2 must also support
IPersistPropertyBag so that it can communicate with
property bags that support only IPropertyBag.
When the object wants to read a property in
IPersistPropertyBag2::Load, it calls
IPropertyBag2::Read. When the object saves properties in
IPersistPropertyBag2::Save, it calls
IPropertyBag2::Write. The client records errors that occur
during Read with the supplied IErrorLog
interface.
Implementing
IPersistPropertyBag
Clients ask an object to initialize itself only
once. When the client has no initial values to give the object, the
client calls the object's IPersistPropertyBag::InitNew
method. In this case, the object should initialize itself to
default values. When the client has initial values to give the
object, it loads the properties into a property bag and calls the
object's IPersistPropertyBag::Load method. When the client
wants to save the state of an object, it creates a property bag and
calls the object's IPersistPropertyBag::Save method.
This is pretty straightforward to implement in
an object. For example, the Demagogue object has three
properties: its name, speech, and volume. Here's an example of an
implementation to save and restore the following three properties
from a property bag:
class ATL_NO_VTABLE CDemagogue
: public IPersistPropertyBag, ... {
BEGIN_COM_MAP(CDemagogue)
...
COM_INTERFACE_ENTRY(IPersistPropertyBag)
COM_INTERFACE_ENTRY2(IPersist, IPersistPropertyBag)
END_COM_MAP()
...
CComBSTR m_name;
long m_volume;
CComBSTR m_speech;
STDMETHODIMP Load(IPropertyBag *pBag, IErrorLog *pLog) {
// Initialize the VARIANT to VT_BSTR
CComVariant v ((BSTR) NULL);
HRESULT hr = pBag->Read(OLESTR("Name"), &v, pLog);
if (FAILED(hr)) return hr;
m_name = v.bstrVal;
// Initialize the VARIANT to VT_I4
v = 0L;
hr = pBag->Read(OLESTR("Volume"), &v, pLog);
if (FAILED(hr)) return hr;
m_volume = v.lVal;
// Initialize the VARIANT to VT_BSTR
v = (BSTR) NULL;
hr = pBag->Read(OLESTR("Speech"), &v, pLog);
if (FAILED (hr)) return hr;
m_speech = v.bstrVal;
return S_OK;
}
STDMETHODIMP Save(IPropertyBag *pBag,
BOOL fClearDirty, BOOL /* fSaveAllProperties */) {
CComVariant v = m_name;
HRESULT hr = pBag->Write(OLESTR("Name"), &v);
if (FAILED(hr)) return hr;
v = m_volume;
hr = pBag->Write(OLESTR("Volume"), &v);
if (FAILED(hr)) return hr;
v = m_speech;
hr = pBag->Write(OLESTR("Speech"), &v);
if (FAILED(hr)) return hr;
if (fClearDirty) m_fDirty = FALSE;
return hr;
}
};
The IStream,
IpersistStreamInit, and IPersistStream Interfaces
COM
objects that want to save their state efficiently as a binary
stream of bytes typically implement IPersistStream or
IPersistStreamInit. An ActiveX
control that has persistent state must, at a minimum,
implement either IPersistStream or
IPersistStreamInit. The two interfaces are mutually
exclusive and generally shouldn't be implemented together. A
control implements IPersistStreamInit when it wants to
know when it is newly created, as opposed to when it has been
created and reinitialized from its existing persistent state. The
IPersistStream interface does not provide a means to
inform the control that it is newly created. The existence of
either interface indicates that the control can save and load its
persistent state into a streamthat is, an implementation of
IStream.
The IStream interface closely models
the Win32 file API, and you can easily implement the interface on
any byte-oriented media. COM provides two implementations of
IStream, one that maps to an OLE Structured Storage file
and another that maps to a memory buffer.
interface ISequentialStream : IUnknown {
HRESULT Read([out] void *pv, [in] ULONG cb,
[out] ULONG *pcbRead);
HRESULT Write( [in] void const *pv, [in] ULONG cb,
[out] ULONG *pcbWritten);
}
interface IStream : ISequentialStream {
HRESULT Seek([in] LARGE_INTEGER dlibMove,
[in] DWORD dwOrigin,
[out] ULARGE_INTEGER *plibNewPosition);
//...
}
interface IPersistStreamInit : public IPersist {
HRESULT IsDirty();
HRESULT Load([in] LPSTREAM pStm);
HRESULT Save([in] LPSTREAM pStm, [in] BOOL fClearDirty);
HRESULT GetSizeMax([out] ULARGE_INTEGER*pCbSize);
HRESULT InitNew();
};
interface IPersistStream : public IPersist {
HRESULT IsDirty();
HRESULT Load([in] LPSTREAM pStm);
HRESULT Save([in] LPSTREAM pStm, [in] BOOL fClearDirty);
HRESULT GetSizeMax([out] ULARGE_INTEGER*pCbSize);
};
When a client wants an object to save its state
as an opaque stream of bytes, it typically attempts to use the
object's IPersistStreamInit interface as the persistence
mechanism. The client supplies the stream into which the object
saves in the form of an IStream interface.
When the client calls
IPersistStreamInit::Load, the object reads its properties
from the stream by calling IStream::Read. When the client
calls IPersistStreamInit::Save, the object writes its
properties by calling IStream::Write. Note that unless the object goes to the extra effort of
handling the situation, the stream contains values in an
architecture-specific byte order.
Most recent clients prefer to use an object's
IPersistStreamInit interface; if it's not present, they
fall back and try to use IPersistStream. However, older
client code might attempt to use only an object's
IPersistStream implementation. To be compatible with both
types of clients, your object must implement
IPersistStream. However, other containers ask for only
IPersistStreamInit, so to be compatible with them, you
need to implement that interface.
However, you're not supposed to implement both
interfaces because then it's unclear to the container whether the
object needs to be informed when it's newly created. Pragmatically,
the best solution to this dilemma is to support both interfaces
when your object doesn't care to be notified when it's newly
created, even though this violates the specification for
controls.
Although IPersistStreamInit doesn't
derive from IPersistStream (it can't because of the mutual
exclusion aspect of the interfaces), they have identical v-tables
for all methods except the last: the InitNew method.
Because of COM's binary compatibility, when your object doesn't
need an InitNew call, your object can hand out an
IPersistStreamInit interface when asked for an
IPersistStream interface. So with a single implementation
of IPersistStreamInit and an extra COM interface map
entry, your object becomes compatible with a larger number of
clients.
class ATL_NO_VTABLE CDemagogue : public IPersistStreamInit, ... {
...
BEGIN_COM_MAP(CDemagogue)
...
COM_INTERFACE_ENTRY(IPersistStreamInit)
COM_INTERFACE_ENTRY_IID(IID_IPersistStream, IPersistStreamInit)
COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
END_COM_MAP()
...
};
Implementing
IPersistStreamInit
Clients ask an object to initialize itself only
once. When the client has no initial values to give the object, the
client calls the object's IPersistSteamInit::InitNew
method. In this case, the object should initialize itself to
default values. When the client has initial values to give the
object, the client opens the stream and calls the object's
IPersistStreamInit::Load method. When the client wants to
save the state of an object, it creates a stream and calls the
object's IPersistStreamInit::Save method.
As in the property bag implementation, this is
quite straightforward to implement in an object. Here's an example
of an implementation for the Demagogue object to save and
restore its three properties to and from a stream:
class CDemagogue : public IPersistStreamInit, ... {
CComBSTR m_name;
long m_volume;
CComBSTR m_speech;
BOOL m_fDirty;
STDMETHODIMP IsDirty() { return m_fDirty ? s_OK : S_FALSE; }
STDMETHODIMP Load(IStream* pStream) {
HRESULT hr = m_name.ReadFromStream(pStream);
if (FAILED (hr)) return hr;
ULONG cb;
hr = pStream->Read (&m_volume, sizeof (m_volume), &cb);
if (FAILED (hr)) return hr;
hr = m_speech.ReadFromStream(pStream);
if (FAILED (hr)) return hr;
m_fDirty = FALSE ;
return S_OK;
}
STDMETHODIMP Save (IStream* pStream) {
HRESULT hr = m_name.WriteToStream (pStream);
if (FAILED(hr)) return hr;
ULONG cb;
hr = pStream->Write(&m_volume, sizeof (m_volume), &cb);
if (FAILED(hr)) return hr;
hr = m_speech.WriteToStream (pStream);
return hr;
}
STDMETHODIMP GetSizeMax(ULARGE_INTEGER* pcbSize) {
if (NULL == pcbSize) return E_POINTER;
// Length of m_name
pcbSize->QuadPart = CComBSTR::GetStreamSize(m_name);
pcbSize->QuadPart += sizeof (m_volume);
// Length of m_speech
pcbSize->QuadPart += CComBSTR::GetStreamSize(m_speech);
return S_OK;
}
STDMETHODIMP InitNew() { return S_OK; }
};
IStorage and
IPersistStorage
An
embeddable objectan object that you can store in an OLE linking and
embedding container such as Microsoft Word and Microsoft Excelmust
implement IPersistStorage. The container provides the
object with an IStorage interface pointer. The
IStorage references a structured storage medium. The
storage object (the object implementing the IStorage
interface) acts much like a directory object in a traditional file
system. An object can use the IStorage interface to create
new and open existing substorages and streams within the storage
medium that the container provides.
interface IStorage : public IUnknown {
HRESULT CreateStream([string,in] const OLECHAR* pwcsName,
[in] DWORD grfMode, [in] DWORD reserved1,
[in] DWORD reserved2,
[out] IStream** ppstm);
HRESULT OpenStream([string,in] const OLECHAR* pwcsName,
[unique][in] void* reserved1, [in] DWORD grfMode,
[in] DWORD reserved2, [out] IStream** ppstm);
HRESULT CreateStorage([string,in] const OLECHAR* pwcsName,
[in] DWORD grfMode, [in] DWORD reserved1,
[in] DWORD reserved2,
[out] IStorage** ppstg);
HRESULT OpenStorage(
[string,unique,in] const OLECHAR* pwcsName,
[unique,in] IStorage* pstgPriority,
[in] DWORD grfMode, [unique,in] SNB snbExclude,
[in] DWORD reserved, [out] IStorage** ppstg);
// Following methods abbreviated for clarity...
HRESULT CopyTo( ... );
HRESULT MoveElementTo( ... )
HRESULT Commit( ... )
HRESULT Revert(void);
HRESULT EnumElements( ... );
HRESULT DestroyElement( . ., );
HRESULT RenameElement( ... );
HRESULT SetElementTimes( ... );
HRESULT SetClass( ... );
HRESULT SetStateBits( ... );
HRESULT Stat( ... );
};
interface IPersistStorage : public IPersist {
HRESULT IsDirty ();
HRESULT InitNew ([unique,in] IStorage* pStg);
HRESULT Load ([unique,in] IStorage* pStg);
HRESULT Save ([unique,in] IStorage* pStgSave,
[in] BOOL fSameAsLoad);
HRESULT SaveCompleted ([unique,in] IStorage* pStgNew);
HRESULT HandsOffStorage ();
};
The
IsDirty, InitNew, Load, and
Save methods work much as the similarly named methods in
the persistence interfaces you've seen previously. However, unlike
streams, when a container hands an object an IStorage
during the InitNew or Load calls, the object can
hold on to the interface pointer (after AddRef-ing it, of
course). This permits the object to read and write its state
incrementally instead of all at once, as do the other persistence
mechanisms. A container uses the HandsOffStorage and
SaveCompleted methods to instruct the object to release
the held interface and to give the object a new IStorage
interface, respectively.
Typically, a container of embedded objects
creates a storage to hold the objects. In this storage, the
container creates one or more streams to hold the container's own
state. In addition, for each embedded object, the container creates
a substorage in which the container asks the embedded object to
save its state.
This is a pretty heavy-weight persistence
technique for many objects. Simple objects, such as in the
Demagogue example used so far, don't really need this
flexibility. Often, such objects simply create a stream in their
given storage and save their state into the stream using an
implementation of IPersistStreamInit. This is exactly what
the ATL implementation of IPersistStorage does, so I defer
creating a custom example here; you'll see the ATL implementation
shortly.
|