OWLNext 7.0
Borland's Object Windows Library for the modern age
|
To persist is to hold to a state or a goal.
In computer programs, an example of persistence is retaining information between application invocations–your application comes up in the same state you left it the day before. Borland provides classes that support persistence. A family of macros that makes adding persistence to your applications simpler is also provided.
This topic describes Borland's persistent streams class library.
This section describes Borland's object streaming support, then explains how to make your objects streamable.
Objects that you create when an application runs–windows, dialog boxes, collections, and so on–are temporary. They are constructed, used, and destroyed as the application proceeds. Objects can appear and disappear as they enter and leave their scope, or when the program terminates. By making your objects streamable you save these objects, either in memory or file streams, so that they persist beyond their normal lifespan.
There are many applications for persistent objects. When saved in shared memory they can provide interprocess communication. They can be transmitted via modems to other systems. And, most significantly, objects can be saved permanently on disk using file streams. They can then be read back and restored by the same application, by other instances of the same application, or by other applications. Efficient, consistent, and safe streamability is available to all objects.
Building your own streamable classes is straightforward and incurs little overhead. To make your class streamable you need to add specific data members, member functions, and operators. You also must derive your class, either directly or indirectly, from the TStreamableBase class. Any derived class is also streamable.
To simplify creating streamable objects, the persistent streams library contains macros that add all the routines necessary to make your classes streamable. The two most important are:
These macros add the boilerplate code necessary to make your objects streamable. In most cases you can make your objects streamable by adding these two macros at appropriate places in your code, as explained later.
Object streaming has been significantly changed from Borland's earlier implementation to make it easier to use and more powerful. These changes are compatible with existing code developed with Borland's ObjectWindows and Turbo Vision products.
The new streaming code is easier to use because it provides macros that relieve the programmer of the burden of remembering most of the details needed to create a streamable class. Its other new features include support for multiple inheritance, class versioning, and better system isolation. In addition, the streaming code has been reorganized to make it easier to write libraries that won't force streaming code to be linked in if it isn't used.
There have been several additions to the streaming capabilities. These changes are intended to be backward compatible, so if you compile a working application with the new streaming code, your application should be able to read streams that were written with the old code. There is no provision for writing the old stream format, however. We assume that you'll like the new features so much that you won't want to be without them.
The following sections describe the changes and new capabilities of streaming. Each of these changes is made for you when you use the DECLARE_STREAMABLE and IMPLEMENT_STREAMABLE macros.
Objects in streams now have a version number associated with them. An object version number is a 32-bit value that should not be 0. Whenever an object is written to a stream, its version number will also be written. With versioning you can recognize if there's an older version of the object you're reading in, so you can interpret the stream appropriately.
In your current code, you might be reading and writing base classes directly, as shown here:
This method will continue to work, but it won't write out any version numbers for the base class. To take full advantage of versioning, you should change these calls to use the new template functions that understand about versions:
Old streams wrote int and unsigned data types as 2-byte values. To move easily to 32-bit platforms, the new streams write int and unsigned values as 4-byte values. The new streams can read old streams, and will handle the 2-byte values correctly.
The old streams provide two member functions for reading and writing integer values:
These have been changed in the new streams:
Existing code that uses these functions will continue to work correctly if it is recompiled and relinked, although calls to readWord will generate warnings about a loss of precision when the return value is assigned to an int or unsigned in a 16-bit application. But in new code all of these functions should be avoided. In general, you probably know the true size of the data being written, so the streaming library now provides separate functions for each data size:
Use of these four functions is preferred.
The streaming code now provides four function templates that support virtual base classes and multiple inheritance. The following sections describe these functions.
Any class that has a direct virtual base should use the new ReadVirtualBase and WriteVirtualBase function templates:
A class derived from a class with virtual bases does not need to do anything special to deal with those virtual bases. Each class is responsible only for its direct bases.
Object streams now support multiple inheritance. To read and write multiple bases, use the new WriteBaseObject and ReadBaseObject function templates for each base:
The easiest way to make a class streamable is by using the macros supplied in the persistent streams library. The following steps will work for most classes:
The following sections provide details about defining and implementing streamable classes.
Defining streamable classes To define a streamable class you need to
Header file objstrm.h provides the classes, templates, and macros that are needed to define a streamable class.
Every streamable class must inherit, directly or indirectly, from the class TStreamableBase. In this example, the class Sample inherits directly from TStreamableBase. A class derived from Sample would not need to explicitly inherit from TStreamableBase because Sample already does. If you are using multiple inheritance, you should make TStreamableBase a virtual base instead of a nonvirtual base as shown here. This will make your classes slightly larger, but won't have any other adverse affect on them.
In most cases the DECLARE_STREAMABLE macro is all you need to use when you're defining a streamable class. This macro takes three parameters. The first parameter is used when compiling DLLs. This parameter takes a macro that is meant to expand to either __export, __import, or nothing, depending on how the class is to be used in the DLL. The second parameter is the name of the class that you're defining, and the third is the version number of that class. The streaming code doesn't pay any attention to the version number, so it can be anything that has some significance to you.
DECLARE_STREAMABLE adds a constructor to your class that takes a parameter of type StreamableInit. This is for use by the streaming code; you won't need to use it directly. DECLARE_STREAMABLE also creates two inserters and two extractors for your class so that you can write objects to and read them from persistent streams. For the class Sample (shown earlier in this section), these functions have the following prototypes:
The first inserter writes out objects of type Sample. The second inserter writes out objects pointed to by a pointer to Sample. This inserter gives you the full power of object streaming, because it understands about polymorphism. That is, it will correctly write objects of types derived from Sample, and when those objects are read back in using the pointer extractor (the last extractor) they will be read in as their actual types. The extractors are the inverse of the inserters.
Finally, DECLARE_STREAMABLE creates a nested class named Streamer, based on the TStreamer class, which defines the core of the streaming code.
Most of the members added to your class by the DECLARE_STREAMABLE macro are inline functions. There are a few, however, that aren't inline; these must be implemented outside of the class. Once again, there are macros to handle these definitions.
The IMPLEMENT_CASTABLE macro provided a rudimentary typesafe downcast mechanism. It is no longer needed, since all modern compilers support RTTI. There are only dummy declarations to allow legacy code that used it to compile.
IMPLEMENT_CASTABLE has several variants:
At some point in your source code you should invoke this macro with the name of your streamable class as its first parameter and the name of all its streamable base classes other than TStreamableBase as the succeeding parameters. For example,
The class Derived uses IMPLEMENT_CASTABLE2 because it has two streamable base classes.
In addition to the IMPLEMENT_CASTABLE macros, you should invoke the appropriate IMPLEMENT_STREAMABLE macro somewhere in your code. The IMPLEMENT_STREAMABLE macro looks like the IMPLEMENT_CASTABLE macros:
The IMPLEMENT_STREAMABLE macros have one important difference from the IMPLEMENT_CASTABLE macros: when using the IMPLEMENT_STREAMABLE macros you must list all the streamable base classes of your class in the parameter list, and you must list all virtual base classes that are streamable. This is because the IMPLEMENT_STREAMABLE macros define the special constructor that the object streaming code uses; that constructor must call the corresponding constructor for all of its direct base classes and all of its virtual bases. For example,
The nested class Streamer is the core of the streaming code for your objects. The DECLARE_STREAMABLE macro creates Streamer inside your class. It is a protected member, so classes derived from your class can access it. Streamer inherits from TNewStreamer, which is internal to the object streaming system. It inherits the following two pure virtual functions:
Streamer overrides these two functions, but does not provide definitions for them. You must write these two functions: Write should write any data that needs to be read back in to reconstruct the object, and Read should read that data. Streamer::GetObject returns a pointer to the object being streamed. For example,
It is usually easiest to implement the Read function before implementing the Write function. To implement Read you need to
Then implement Write to work in parallel with Read so that it sets up the data that Read will later read. The streaming classes provide several operators to make this easier. For example, opstream provides inserters for all the built-in types, just as ostream does. So all you need to do to write out any of the built-in types is to insert them into the stream.
You also need to write out base classes. In the old ObjectWindows and Turbo Vision streaming, this was done by calling the base's Read and Write functions directly. This doesn't work with code that uses the new streams, because of the way class versioning is handled.
The streaming library provides template functions to use when reading and writing base classes. ReadVirtualBase and WriteVirtualBase are used for virtual base classes, and ReadBaseObject and WriteBaseObject are used for nonvirtual bases. Just like IMPLEMENT_CASTABLE, you only need to deal with direct bases.
Virtual bases of your base classes will be handled by the base class, as shown in this example:
When you're writing out a base class, don't forget to cast the this pointer. Without the cast, the template function will think it's writing out your class and not the base class. The result will be that it calls your Write or Read function rather than the base's. This results in a lengthy series of recursive calls, which will eventually crash.
You can assign version numbers to different implementations of the same class as you change them in the course of maintenance. This doesn't mean that you can use different versions of the same class in the same program, but it lets you write your streaming code in such a way that a program using the newer version of a class can read a stream that contains the data for an older version of a class.
For example,
Suppose you've written out several objects of this type into a file and you discover that you need to change the class definition. You'd do it something like this:
Streams written with the old version of Sample will have a version number of 1 for all objects of type Sample. Streams written with the new version will have a version number of 2 for all objects of type Sample. The code in Read checks that version number to determine what data is present in the stream.
The streaming library used in the previous versions of ObjectWindows and Turbo Vision doesn't support object versioning. If you use the new library to read files created with that library, your Read function will be passed a version number of 0. Other than that, the version number has no significance to the streaming library, and you can use it however you want.