Design

OLE2 and .INI Files

Source Code Accompanies This Article. Download It Now.

Object Linking and Embedding (OLE) is an architecture that allows applications to integrate data or objects into a compound document. Billy presents functions that let you use compound files to replace and enhance the initialization file functions provided with Windows.

Object Linking and Embedding (OLE) is an architecture that allows applications to integrate data or objects into a compound document. OLE2 provides a large set of interfaces that developers must understand to produce OLE2-compliant applications. Applications use these interfaces to provide features for linking and embedding objects, persistent storage, in-place editing, drag-and-drop, and more.

One basic feature of the OLE model is a sophisticated storage system called a "compound file." Compound files make hierarchies of objects persistent. This storage system's paradigm is that of a file system. A disk file system is composed of directories and files. Directory objects contain other directories and files. A file contains the user's data. Compound files provide two similar abstractions: storage objects, which are analogous to file-system directories, and stream objects, which are similar to files. A storage object can contain other storage objects and streams. A stream contains the equivalent of a typical file's data. The compound file manages these two logical abstractions and places the data into a single file in the file system.

Structured storage is a powerful feature provided with OLE2. Most of the OLE SDK information about compound files discusses the use of structured storage by OLE client and server applications to store linked and embedded objects in a compound document. It is important to point out, however, that applications can take advantage of this technology without being an OLE client or server application.

With the functions presented in this article, you can use compound files to replace and enhance the initialization file functions provided with Windows. Think of these functions as one example of using compound files for persistent storage. With a little more work, the sample code provided could be made generic enough to use for any type of data an application needs to save. Think of OLE's structured storage as a general-purpose data storage that allows you to name your data for later access. The storage system will manage the details of allocation and fragmentation within the file for you.

A Windows initialization (.INI) file is an ASCII file containing a hierarchy of information used to store application settings. The file is divided into sections noted by [section name]. Each section contains entry=value associations. A typical .INI file fragment is shown in Figure 1.

The Windows API provides several functions used to read and write information from an .INI file. My example provides clones for the standard GetPrivateProfileInt, GetPrivateProfileString, and WritePrivateProfileString routines. My functions store the data in a compound file instead of an .INI file. The advantages this provides are varied. For one, compound files are binary files. Storing application settings in a binary format makes it difficult for users to change the values without being in the application. Another advantage is more flexibility in the data that can be stored. Normal .INI files truncate trailing spaces in a value, and they don't allow line feeds in an entry's value. None of these limitations exist when using compound files. The binary nature of compound files also allows the development of extensions to initialization functions. For example, a function could be written that would serialize C++ objects into an entry's stream. If an application saved its last window position, it could use the extended function to save a rectangle object in the stream and then restore it at start-up. This saves converting a rectangle to a string, writing it to an .INI file, reading it in, parsing the string, and turning it into a rectangle object.

The complete implementation of the system presented here is provided electronically; see "Availability," page 3. These source listings provide three C functions to emulate the initialization functions provided by Windows. The functions CxGetPrivateProfileInt, CxGetPrivateProfileString, and CxWritePrivateProfileString are identical to the Windows routines, both in their parameter list and their operation; the only difference is that the data is stored in a compound file instead of an ASCII .INI file. This different persistent format is completely transparent to the application using the functions. I've provided a C interface to make it easy to use these functions with existing code.

I used Visual C++ 1.5 and MFC 2.5 for the implementation of the new functions. MFC provides some excellent classes for both OLE and standard abstract data types that made the work much easier than writing straight to OLE. Specifically, I used the MFC COleStreamFile class to write an entry's value to the compound file. I found that the MFC class COleDocument and its derived classes did not exactly provide the functionality needed to implement the profile functions. The COleDocument class is very much directed towards OLE client and server applications, and many of its member functions and variables are not applicable to this example.

There are two classes provided in this example for implementing the profile functions: CxOleDocFile and CxOleStorage; see Listing One. The CxOleStorage class provides an encapsulation of OLE's IStorage interface. This class was modeled after the MFC COleStreamFile class and its relationship to OLE's IStream interface. The CxOleDocFile class abstracts concepts applicable to OLE compound files and implements methods for creating and opening compound files. The term DocFile is the historical term for compound files and is used throughout the OLE APIs.

The profile functions use the compound-file, storage, and stream abstractions to provide the information hierarchy that is present in an .INI file. A compound file corresponds to the .INI file. Storages are created in the file to represent sections. A stream is used for an entry, and the contents of the stream represent the value for the entry. You can consider these classes provided as extensions to MFC. Your application must be an MFC application to use them. To include the functions, you can add the two source files provided to the application's project. Also, be sure to include a call to AfxInitOle in your application's InitInstance method.

To write a string, call CxWritePrivateProfileString with a section name, entry name, value, and filename as parameters (see Listing Two). This function instantiates a CxOleDocFile class and calls OpenDocFile to open the file. If the file does not exist, CreateDocFile is called to create the file. Both of these methods look in the Windows directory for the file if the filename does not contain a fully qualified path. If the filename is fully qualified, it will be created or opened from the specified location. Next, the WriteProfileString method is called to do the work of writing the new setting to the file.

WriteProfileString has a fair amount of work to do in order to honor the various ways the parameters can be specified. If the section name, entry name, and value are all valid strings, the data is written to the file. If the entry name pointer is NULL, the section and all of its entries are removed. If the value pointer is NULL, the entry is removed from the file. While this method does not throw an exception, it does use exception handling to improve its robustness.

WriteProfileString first checks the entry-name parameter. If this parameter is NULL, the section is deleted from the file by telling the root storage object to destroy the element specified by the section name. The CxOleStorage::DestroyElement method calls the IStorage::DestroyElement method to remove the storage from the compound file.

If an entry name was specified, the section name is used to open or create a storage in the file. Next, the function checks the value parameter. If this parameter is NULL, the DestroyElement method is called on the section storage to remove the entry's stream from the file. Otherwise, a COleStreamFile object is instantiated with the same name as the entry. The value string is then written to the stream, and the stream is closed. It is also important to close the section storage and flush the root storage so that everything is written to disk.

Reading values from the compound file works similar to writing data. The function CxReadPrivateProfileString is used to read a string value, and CxReadPrivateProfileInt is used to read a 16-bit integer value from the file. These functions create a CxOleDocFile object and invoke either the ReadProfileString or ReadProfileInt methods.

The ReadProfileString method first opens a storage with a name specified by the section name. After the storage is opened, the entry parameter is checked. If it is NULL, the behavior of this method is defined to enumerate all of the entries within this section and return them in the return buffer. Each entry name is null terminated, with the final string ending in two null-terminating characters. If the parameter is not NULL, then a stream is opened with the name specified by the entry parameter. The contents of the stream are read in and copied to the return buffer. If a stream does not exist with the specified name, the default value is copied to the return buffer.

The ReadProfileInt method works in a fashion similar to that of ReadProfileString. Instead of returning a null-terminated string, it reads in the contents of an entry's stream and treats it as a 16-bit unsigned integer. If the entry stream is not found, the specified default integer is returned.

I was not sure what to expect in terms of performance for compound files. To get a feel for the performance of these files, I wrote a sample application that uses the C functions and some of the methods on the classes; these functions are available electronically.

The first thing you can do with the sample program is convert an existing .INI file to a compound file. The sample copies the selected existing .INI file to your Windows temp directory. It then creates a compound file in the temp directory and calls the LoadFromIni method on the CxOleDocFile class. The amount of time the conversion takes is shown on the screen. After a test .INI file and a test compound file are created, you can perform several tests in parallel on the two files. The test will use the Windows API calls to access the .INI file and the new functions to access the compound file. The time each of these operations takes is shown on the screen. The sample provides the following tests: get all entries from both files, change each entry in each section to be one byte shorter, and change each entry to be one byte longer.

The easiest comparison was between the file sizes of an .INI file and a compound file. The smallest .INI file on my PC was 20 bytes. The equivalent compound file was 2560 bytes. My WIN.INI was the largest at around 30 Kbytes. The converted file was 150 Kbytes. If you need the smallest possible file, compound files are probably not the way to go.

The next tests were designed to compare read, write, and access times to the data stored in the files. When I first ran the test, the times on the compound files were horrible. To improve performance on storages, I changed from the direct mode to the transacted mode. This change improved performance, but the time to access all of the entries was still much slower than that for .INI files. I stepped through some code in the debugger to see what was taking so long. I found that the code was reading values quickly, but the disk light came on for long periods when the compound file was closed. More stepping showed that the call to the Commit method on a storage was the culprit. I was opening the storage in read/ write mode, and even though the code never wrote to the file, this function took a long time to execute. I changed the code to open the file in read mode when accessing entries, and the speed test improved dramatically.

The next big performance difference to tackle was the write times for updating entries. Write times were significantly slower than .INI files. Applications usually write to .INI files in short bursts. Because of this, Windows caches an .INI file. When a write occurs, the cache is updated in memory and written to disk. Because a compound file is much larger, I really did not want to cache the entire file. Instead, I decided to keep the compound file open between calls when writing information. If a call to write information is writing to the same file as the previous call, the compound file is already open, and the update happens very quickly. If you want to close the file, call CxWritePrivateProfileString with NULL for all parameters except the filename. You can also use these same parameters on the Windows call to make it refresh the cache for an .INI file. After adding the code to keep the file open, the performance for compound files was reasonably close to that of .INI files.

Compound files such as my .INI replacements are just one example of using OLE's structured storage. There are several other areas where this storage model can be useful. In a C program, compound files can be very useful if your application needs to store different types of structures. You can assign storage and stream names to access the data and let the storage system allocate and reclaim space in the file.

If you are using C++, modify the Cx-OleDocFile class to be a container of an abstract class CxOleDocFileItem. Derive new classes from the CxOleDocFileItem class to hold different types of data for your application. When you instantiate these derived classes, assign a storage name to them and add them to the compound-file class. Add a Checkpoint method to the compound-file class that goes through all of its contained items, then serialize them into a COleStreamFile if they have been modified. This is very similar to the way MFC uses compound files, but without a lot of extra overhead. This is appropriate if you just want to use structured storage.

Structured storage could also be made available to Visual Basic applications by putting the code into a DLL and providing the appropriate APIs. This would extend some very powerful functionality to your VB applications. There are more features with structured storage that I have not covered here. The model provides for nested transactions with complete program control over committing or reverting the transactions. There are also functions for moving, copying, and renaming storages and streams in the compound file. For any Windows application that has nontrivial data to store, OLE's structured storage should prove to be a very handy system to use.

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task.
However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

Video

This month's Dr. Dobb's Journal

This month,
Dr. Dobb's Journal is devoted to mobile programming. We introduce you to Apple's new Swift programming language, discuss the perils of being the third-most-popular mobile platform, revisit SQLite on Android
, and much more!