IPersistStorage: A Heavy Dose of Protocol

The persistence model of IPersistFile introduces a few of the complications that arise when an object is allowed incremental access to its persistent data—that is, allowed to scribble in its storage. The persistence model of IPersistStorage is even more complicated because it allows an object to read and write its information to a storage hierarchy that begins with an arbitrary storage element (IStorage) as well as to handle low-memory save situations. As with IPersistFile, a client of an object that implements IPersistStorage has to be able to tell that object when to release its pointers to open storage and stream elements so that the client can manipulate the storage, change an underlying file location, and so on.

So the IPersistStorage interface describes not only load and save semantics but also includes initialization semantics such as IPersistStreamInit and the operations necessary to determine when the object can scribble to its storage:


interface IPersistStorage : IPersist
{
HRESULT IsDirty(void);
HRESULT InitNew(IStorage *pstg);
HRESULT Load(IStorage *pstg);
HRESULT Save(IStorage *pstg, BOOL fSameAsLoad);
HRESULT SaveCompleted(IStorage *pstg);
HRESULT HandsOffStorage(void);
};

These functions behave as follows:

Member
Function

Description

IsDirty

Same semantics as in IPersistStream[Init] and IPersistFile.

InitNew

Instructs the object to fully initialize a new storage identified by pstg. The object should create and open every storage and stream element into which it would like to scribble, as well as any element that it will require in a low-memory save situation. It should also preallocate stream space (IStream::SetSize) that it will need in a save operation in case the storage medium is full. It may also hold a reference on pstg itself.

Load

Instructs the object to load its persistent data from the storage element identified by pstg. The object should hold open any element it may want (for scribbling) or require (for low-memory saves) as described for InitNew. Load is always called in lieu of InitNew to initialize the object.

Save

Instructs the object to save its persistent data in either its current storage or a different storage, depending on the fSameAsLoad flag (pstg will always be non-NULL). If fSameAsLoad is TRUE, the object can write changes incrementally; otherwise, the object must completely rewrite all of its data. In either case, the object continues to hold its present pointers to open elements, al-though it cannot scribble until SaveCompleted is called. The object clears its dirty flag on Save.

Save is not allowed to fail as the result of an out-of-memory condition, which means that the object must open and hold pointers to any element it might need in order to complete a save during InitNew and Load. Creating and opening elements requires memory that might not be available.

The object should never call IStorage::Commit on the pstg passed to this function nor write a CLSID with WriteClassStg; the client owns those operations.

SaveCompleted

Informs the object that the calling client has completed its overall save procedure. If pstg is NULL, the object can once again scribble to its open elements. If pstg is non-NULL, the object must release its pointers and reopen its elements underneath pstg.

HandsOffStorage

Instructs the object to release all of its pointers to any and all elements, including the pointer passed to InitNew or Load. This call can follow Save and precede SaveCompleted to allow the client to manipulate the storage without possibility of access violations (see the following discussion).


As with IPersistStreamInit, both the Load and InitNew functions can initialize the object as well as its storage. InitNew is called only if the object as yet has no persistent storage; otherwise, Load is always called. The two will never be used on the same instance of an object. InitNew should, however, preallocate stream space with IStream::SetSize. It's silly to go to all the trouble to save pointers for a low-memory save just to run out of disk space at the same time!5

The relationship between Save, SaveCompleted, and HandsOffStorage is a little more complex than with IPersistFile. These functions provide an object with incremental access to its storage (including access needed for low-memory saves) but allow the client to temporarily suspend that right in order to rename the storage element, save a file and reopen it, move a file, and so on. The nature of IPersistStorage allows an object to hold an IStorage pointer for its own use, which it may access incrementally throughout its lifetime. When that client wants to perform a complete save, or when that client wants to save everything into a new file, it has to have some way of telling the persistent object to release whatever pointers it holds to open storage and stream elements.

To illustrate the process, let's assume that we have a client in control of a root storage, which in turn contains a substorage. The client instantiates some object to which it hands the IStorage pointer to that substorage, allowing the object to create whatever hierarchy it wants within that substorage. At this point the object is uninitialized. If the storage is brand-new, the client initializes the object (and its storage) through IPersistFile::InitNew; if the storage is opened within an existing file, initialization occurs with IPersistStorage::Load:

In either case, the client hands an IStorage pointer to the object, which is now in the scribble state, in which it can open and hold any pointers to any elements in the storage (it must call AddRef on the pstg passed to Load or InitNew to hold that pointer):

At some later time, the client tells the object to save its data through IPersistStorage::Save, to perform either a full save or an incremental save. In both cases, the object is passed the IStorage in which to save, using fSameAsLoad to distinguish the operations to perform. Again, IPersistStorage::Save is contractually obligated to save its data without failing as a result of out-of-memory conditions. This last statement has some heavy implications. It means that to fulfill this requirement, the object must not attempt to create new streams or substorages from within Save because creating them requires memory. Again, this means that in its implementation of both Load and InitNew, the object must not only hold onto the IStorage pointer, but it must also create and hold open any element it might need in a subsequent incremental save. This ensures that a user's data can be saved with zero available memory, which is all that matters in such conditions. Of course, if you are not interested in robustness of this kind, feel free to ignore the rules…and then train your product support teams in the fine art of soothing horribly irate customers! Best to follow the rules.

Again, after InitNew or Load is called, the object is in its scribble state, in which it reads and writes in the storage as necessary. In scribble mode, additional calls to InitNew and Load are illegal (client error) and should return E_UNEXPECTED.

Now one of two things might happen to the object: the client can call either Save or HandsOffStorage. As mentioned already, Save instructs the object to perform either an incremental or a full save. After Save is called, the object enters into a zombielike (or no-scribble) state. A zombified object cannot perform any incremental writes to the storage, although it can still read from the storage without problem. When the client wants to allow scribbling again, it calls SaveCompleted, freeing the object from the curse of being undead:

In some situations, a client requires the object to release any of its open storage or stream pointers, for example when the client is going to rename the underlying file or when it is reverting to a previously saved state. This is the purpose of HandsOffStorage, which tells the object to get its grubby little mitts off any elements within the storage. If this call occurs before Save, the object must shrug its shoulders, heave a heavy sigh, and blindly call Release on all its pointers. However, the client later makes the same bits available through SaveCompleted, which the object was looking at when HandsOffStorage was called. In other words, the object doesn't lose any data and doesn't have to reinitialize its internal state—it can just reopen its needed elements.

When the client calls HandsOffStorage, the object enters the hands-off state. In this state, it cannot read from or write to a storage—it has no pointers! When the client has finished partying on the storage, it must then call SaveCompleted, passing the IStorage pointer from which the object can reopen its elements. SaveCompleted means a return to the scribble state.

Obviously, the client must always pass an IStorage pointer to SaveCompleted when the object is in the hands-off state. The storage must always contain the object's expected hierarchy because the object will attempt to reopen its elements. Granted, the storage may or may not be the same as the one passed to InitNew, Load, or even Save. But the same data will be in whichever storage the object receives. Keep in mind as well that a sequence of calls to HandsOffStorage and SaveCompleted should clear the object's dirty flag.

We'll see all of this protocol reduced to code a little later in this chapter.

5 Do not depend on the stream having a 512-byte allocation granularity as with compound files because it is not necessarily true that the stream is part of a compound file. It could be implemented on global memory or a database field with a different granularity.