The standard library bundled with Delphi, Borland's visual Pascal programming environment for Windows, includes a handy little class
named TIniFile. TIniFile provides a simple but useful interface for accessing and manipulating ini files, a text-based file format
for storing structured data. Using TIniFile, one can read, write, load, and store things like user preferences or even your
program's main data structures in a user-editable text-based format with relative ease.
Without something like TIniFile, accomplishing such a feat can be a chore.
Oftentimes this results in the use of a proprietary, binary format that is uneditable by end users.
I missed having TIniFile around when I started programming in C++, so I decided to write my own.
Borland's TIniFile class supported the reading and writing of integers, floating point numbers, boolean values, and character strings.
My IniFile class supports all four of those data structures, and also allows the reading and writing of arbitrary binary data.
This data is encoded into an unintelligible mess of ASCII characters, so storing data this way eliminates the likelihood that anyone
will be able to manually edit it with any sort of ease. This usually isn't desirable, but being able storing arbitrary chunks binary
data can be quite handy at times.
My BIniFile class, a subclass of IniFile, also supports the reading and writing of BMessages to and from ini files.
Such BMessages are simply flattened and written out as ASCII encoded binary data.
Thus, they too are effectively manually uneditable.
How it works
(If you're curious, you can click here
to view the public interfaces of IniFile and BIniFile before going into more detail).
IniFile stores the ini file data structure directly in memory.
An ini file is composed of a number of named sections, each of which contains a number of named keys.
With each key is associated a unique value. Thus, any value in the ini file may be uniquely identified by its section and key.
Load()
and Store()
are used to load and store an entire ini file from or to a given file.
A filename can also be passed into the constructor if you wish to load a file when you create the object.
If a file error occurs while attempting to load the given file, the constructor will throw an IniFile::EFileError
exception.
By default, Load()
and Store()
simply return false when such an error occurs, but if specified they will also throw
an IniFile::EFileError
. Also, all IniFile and BIniFile functions will throw an IniFile::EInsufficientMemory
exception if a dynamic memory allocation fails.
Assignment and copy construction are implemented to make complete copies.
Clear()
restores the object to an empty state.
For each data type IniFile supports, there is a corresponding pair of WriteTYPE()
and ReadTYPE()
functions
(some types have two or three ReadTYPE()
functions to support different dynamic memory allocation methods).
The first argument is always the name of the section that owns the key/value pair you wish to read to or write from.
The second argument is always the name of the key whose value you wish to read or write.
The remaining arguments are type dependent.
Dynamic memory allocation failures aside, all IniFile::WriteTYPE()
functions are guaranteed to succeed.
BIniFile::WriteMessage()
will only fail if unable to flatten the given message.
Reading is a different matter. All data is stored internally as 8-bit character strings, so for most data types,
some sort of conversion is necessary on a call to ReadTYPE()
.
Different measures are taken for different data types to account for times when this conversion fails
(or when the requested Section/Key/Value tuple does not exist):
ReadInt(), ReadFloat(), ReadBool(), ReadString(), ReadMessage():
All of these functions accept a default value that is returned in the event of a conversion error or missing value.
In the case of ReadString()
and ReadMessage()
, a copy of the given default value is returned
(unless the given default value is NULL, in which case NULL is returned).
ReadData():
These functions return the number of bytes that were copied into the result.
If 0 is returned, the result may also be NULL.
ReadTYPE() pairs and triplets:
As you may have noticed, there are two versions of ReadString()
and ReadData()
,
and three versions of ReadMessage()
.
For ReadString()
and ReadData()
, the first version of each allows you pass in a pre-allocated string or
data buffer into which the function reads as much of the requested data as possible.
The second version returns a newly allocated string or data buffer (thanks to malloc()) for which you are now responsible.
When you're through using it, it's your job to "free(result)" the pointer returned to you.
The upside of the second type of function is that the IniFile object knows how much data there is to read,
and thus can allocate a data structure of sufficient size.
For the first two versions of ReadMessage()
, the Message parameter is a reference to a pre-allocated BMessage to
Unflatten() into. The reference returned by these functions is always just a reference to the Message parameter (provided as a convenience).
The four argument version of ReadMessage()
requires a reference to a default value, whereas the three argument version simply
calls the four argument version with Message as both the Message parameter *and* the Default parameter.
If the function is unable to Unflatten() into Message the data read from the given section and key, then Message is assigned the value
of Default.
The third version of ReadMessage()
, i.e. the one that returns a pointer to a BMessage, always returns a newly allocated BMessage
(or NULL). Thus, even when the default value is returned, it's a new copy of the BMessage pointed to by Default, not Default itself.
Therefore it's always safe (and necessary) to "delete result" whatever the third version of ReadMessage()
returns once you're
through using it.
How is it used?
A normal use of IniFile or BIniFile might be something like the following:
When an application is started, it loads a specific ini file used to store user preferences into an IniFile object.
To account for the first time the program is run (or any time the ini file may be improperly edited or deleted by the user),
the program supplies some sort of default value to be returned in the event of a read failure for each key it attempts to read from
the IniFile object (or for ReadData()
calls, the number of bytes read is monitored, and a default chunk of data substituted
if the wrong amount of data is read, or if the data is improperly formatted).
The user preferences of the program are set based on the values read in, and the program runs as normal.
When the user requests that the application close itself, the application creates a new ini file object,
writes all of its current settings into it, and saves it to disk. Thus, the next time the user runs the program,
his/her settings from the last session will be restored.
What does an ini file look like?
For those who aren't familiar with ini files, here's a short example of what an ini file might look like:
[ This is the first section ]
Where's the data? = Here's the data ; This is a comment
This line will fail = ; because it has no value to match the key
= This line also fails because it has no key before the first =
This one's okay, though = Properly Formatted == Properly Loaded
[Section2]
Key=Value
Key2=12345
3=3.0
[ This Is An Odd Name For A Section [I think]]
___ = ___
This would be parsed into the following IniFile structure:
"This is the first section"
"Where's the data?" >> "Here's the data"
"This one's okay, though" >> "Properly Formatted == Properly Loaded"
"Section2"
"Key" >> "Value"
"Key2" >> "12345"
"3" >> "3.0"
"This Is An Odd Name For A Section [I think"
"___" >> "___"
Note that embedded whitespace is allowed, but leading and trailing whitespace is removed.
What file format does IniFile recognize?
Should you ever feel like manually editing an ini file to be loaded by the IniFile class, it's also useful to know just what
the class is expecting. Here's a quick rundown:
- Ini files are read from the top down.
- All lines before the first section line are ignored.
- A section line is a line where the first non-whitespace character is a left square bracket
"["
.
- The text in between the first left square bracket and the first right square bracket
"]"
designates the section's name, which may contain left square brackets.
- Whitespace may be embedded in section names, but leading and trailing whitespace is removed.
- A section owns all the data lines that follow it up until a new section line is encountered.
- A given section may appear multiple times in a given file.
- A data line designates a key/value pair.
- The key is all text to the left of the first equal sign "=" in the line.
- Whitespace may be embedded in the key, but leading and trailing whitespace is removed.
- The value is all text to the right of the first equal sign in the line, and may contain equal signs.
- Whitespace may be embedded in the value, but leading and trailing whitespace is removed.
- Comments begin with a semicolon ";" and extend to the end of the line.
- All text following a comment is ignored up until the end of the line.
- A backslash preceding a semicolon has no effect; the semicolon still starts a comment.
- Any line that does not match as a section line or a data line is ignored.
Limitations
There are a few limitations in the current implementation (there may also be many more I'm not thinking of right at this moment ;-):
- Only 8-bit character sets are supported. To the best of my knowledge, it should handle UTF-8 just fine, because the only chacters with any special meaning are all part of the ASCII character set. Basically, any 8-bit character not in the set
{ \n \r \t \s ; [ = }
of ASCII characters is a valid section, name, or value character. "[" and "=" are also valid in certain places, as described previously.
- These classes are not inherently thread safe. It's up to you to handle any synchronization necessary if you wish to use them concurrently in a multi-threaded manner.
- Strings written with
WriteString()
may not contain newlines, carriage returns, or semi-colons. Or rather, nothing is stopping you from including said characters in a string passed to WriteString()
, but if the ini file is stored and then reloaded, only text up to the first newline, carriage return, or semi-color will be returned when you call ReadString()
. I'd like to support automatic conversion to escaped '\n', '\r', and '\;' sequences, but I didn't have time to do so for this version.
- If you re-read values you've written before storing and reloading the ini file, you may get different results than you would otherwise. For example, if you write the string
" This is a string ; it sure is.... "
and then read it back before storing and re-loading, you'll get back just what you put in. But if you first store the ini file and then read it back in, ReadString()
will return "This is a string"
. I'd like to get rid of the inconsistency someday, but again, there wasn't enough time for this version.
- The set of sections in an ini file and the set of keys in a section are currently stored as linked lists. This makes searching through large ini files with many sections and/or many keys a slow process. Someday I'll replace the linked lists with something a little more scalable.
- I'm not sure if these classes will work properly on big endian architectures without modification. Make sure you run the verification program in the
test/
subfolder of the archive if you decide to try the classes out on a PPC machine.
- Being able to write out a flattened BMessage is handy, but it'd be a lot nicer to support writing out of built-in types to a user-editable format. I.e. I'd like to have pairs of functions like WriteRect(BRect rect) and ReadRect() that would write out a string
"(rect.left, rect.top, rect.right, rect.bottom)"
and parse it back into an actual BRect. Again, something to add in the future.
What's in the package?
The root of the archive contains the header and source files for IniFile and BIniFile, a base Jamfile and Jamrules file,
and a number of subfolders.
The example/
subfolder contains an example BeOS GUI app that saves and restores its state in
~/config/settings/IniExample.ini
using the BIniFile class. The window's position and size, as well as the contents
of the app's text control and slider, are saved and restored in between sessions.
In addition, the program has a special control that accepts drag-and-drop parcels.
When you drop something on the control, it prints out the contents of the corresponding BMessage, and then holds on to the message until
you drag it back off or drop a new message. Dropped messages are also saved between sessions to the program's ini file.
The test/
subfolder contains a test program that verifies the functionality of the IniFile and BIniFile classes.
The CppUnitShell/
subfolder contains a subset of the CppUnit testing library written by Eric Sommerlade, Michael Feathers,
and Jerome Lacoste. The complete version is available from http://cppunit.sf.net/.
The CppUnitShell/
folder also contains CppUnitShell, a generic command-line testing interface class built around CppUnit.
Both are used by the test program in test/
. CppUnit is released under the LGPL, and thus, for the sake of convenience,
so is CppUnitShell. Everything else in the package is released under the same license as OpenBeOS, the less-restrictive MIT license.
The IniDump/
subfolder contains a small command line program called DumpIni
that reads in an ini file and
dumps its contents out to standard out. The file Example.ini is just like the example ini file shown above.
There are Jamfiles included for every folder. Running jam from the root of the archive should compile and link the entire package.
I've included a precompiled version of jam built by our very own buildmeister Ithamar, since it's likely many of you won't have a copy handy.
And finally, this archive is targeted at BeOS specifically, but the IniFile class is designed to be portable between platforms.
The verification program in /test
compiles and runs (successfully) on Linux with no modifications
(other than removing the BeOS specific BIniFile tests). I haven't had a chance to try it on other platforms yet,
although the original version of IniFile was written on a Windows machine, so I would expect this version would work as well.
Conclusion
Ini files are a handy way to store structured data in a user-editable format.
IniFile and BIniFile make it possible to take advantage of ini files without having to handle any of the necessary parsing,
data structure management, or type conversion.
Those who are interested in ini files might also want to check out the InitFile class that's part of Allen Brunson's TropicLib,
available from http://www.beosdevelopers.org/brunsona/tropiclib.html.
It is similar to the classes presented here, but takes a slightly more structured approach to handling improperly formatted or missing
values when loading an ini file from disk.
Source Code:
IniFile_v1.00_BeOS.zip