Introduction
One of the always present points in a non trivial program is the need to keep some of its internal data among executions. Normally, the variables must need an initial value for code to work, and must accept modifications, both externally, or by the program itself. This is called persistence, and is sometimes associated with another property called serialization.
One of the evidences of how difficult it is to do this in the right way is the bunch of methods used, both good and bad. In MSDOS times, some well known guys used the global environment variables to satisfy their local configuration needs, overloading this resource. Windows registry, one of the Windows weak points is used and abused also for this task.
Background
Actually, all C variables are composed of sets of a few pure types. We can take advantage of the ability given by C++ templates to manage these types. This template set provides you, as developer:
- Initialization: The config file is filled at startup with the compile time assigned values, once created. You can call this "zero user configuration".
- Modification: You can use a text or XML editor to modify any value on file, it's "human readable".
- Fault tolerancy: As long as the XML file keeps coherent, you can remove whole or pieces of this config file. The effect is that the removed parts will be restored with the default values.
- Unorder tolerancy: You can change the order of items they will be read correctly.
- Non intrusive: This means we don't need to derive saved classes from nothing, so their memory structure remains untouched.
Using the Code
I've included two projects, both Visual Studio and code::blocks, which is my main IDE so you can try the example both in Windows and Linux.
This code must be linked against expat library. The source package includes all that is necessary. In a production environment, you need really a few files of package, but I've included all, as well as a code::blocks (cbp) and Visual Studio (dsw) project.
You also have an XP win32 simple executable (and the expat DLL) so that you can test the config file creation at once.
You must tell about the parts you want to make persistent in your objects this way.
highlighted
text is what you need to include in your existing code to keep an unbreakable XML config file for your existing program.
#include "xmlconf.h" /* library access */
struct RootStruct
{ int storedInt;
LeaveStruct aBranch[5];
char * ptr;
XMLCONFSTART
XMLCONFITEM( storedInt );
XMLCONFARRY( aBranch );
XMLCONFEND
};
This will be your modified main file:
include "yourinclude.h"
LeaveStruct leave;
RootStruct root;
int globalInteger;
short globalArray[]= {1,2,3,4,5,6,7,8,9};
unsigned char globalBytes[]= {10,20,30,40,50,60,70,80,90};
XMLCONFDECLARESTART( "testcfg.xml" ) XMLCONFITEM( leave ); XMLCONFITEM( root ); XMLCONFITEM( globalInteger ); XMLCONFARRY( globalArray ); XMLCONFARRY( globalBytes ); XMLCONFDECLAREEND
int main()
{ leave.aInteger[0]++; root.storedInt++;
char p[10];
printf( "Config stored values -> (%s) %d %d\n"
, root.aBranch[0].aString
, leave.aInteger[0]
, root.storedInt );
return( 0 );
}
This is the config file that the templates, and libstdc++ keeps for the provided example:
="1.0"
<config name="testcfg.xml">
<LeaveStruct name="leave">
<byte aByte="0"/>
<int aInteger="28,0,0,0,0"/>
<str aString=""/>
</LeaveStruct>
<RootStruct name="root">
<int storedInt="1262"/>
<LeaveStruct name="aBranch" idx="0">
<byte aByte="0"/>
<int aInteger="0,0,0,0,0"/>
<str aString="Hello, world hola"/>
</LeaveStruct>
<LeaveStruct name="aBranch" idx="1">
<byte aByte="0"/>
<int aInteger="0,0,0,0,0"/>
<str aString="These are"/>
</LeaveStruct>
<LeaveStruct name="aBranch" idx="2">
<byte aByte="0"/>
<int aInteger="0,0,0,0,0"/>
<str aString="the default values"/>
</LeaveStruct>
<LeaveStruct name="aBranch" idx="3">
<byte aByte="0"/>
<int aInteger="0,0,0,0,0"/>
<str aString="on the"/>
</LeaveStruct>
<LeaveStruct name="aBranch" idx="4">
<byte aByte="0"/>
<int aInteger="0,0,0,0,0"/>
<str aString="config file"/>
</LeaveStruct>
</RootStruct>
<int globalInteger="0"/>
<short globalArray="1,2,3,4,5,6,7,8,9"/>
<byte globalBytes="10,20,30,40,50,60,70,80,90"/>
</config>
This is a basic use of this code, but you can also modify the constructor and destructor of an object to make it XML file persistent. Changes on config file will be reflected in the program.
Points of Interest
Templates are real powerful instruments. A smart use of them can avoid big mess, but they have many problems: very optimized code became illegible, and so, difficult to debug or modify. Side effects are a pain and several compilers give sometimes different behaviors and results.
When you compile and strip a program, all "human names" simply disappear, but there is a narrow gateway between our realm and processor realm called typeid
(gnu FUNCTION_NAME
is also usable) this code takes advantage of this door and other less romantic properties to reflect your program status in an almost human form. Below is the function used that returns your object name in a human readable and portable way:
template < class t > inline const char * typeId( const t & obj )
{ const char * ptr= typeid( obj ).name();
#ifdef __GNUG__
while( *ptr && (*ptr < 'A' ))
{ ptr++;
}
#endif
#ifdef _MSC_VER
const char * needle;
while( needle= strchr( ptr, ' ' ) )
{ ptr= needle+1;
}
#endif
return( ptr );
}
This is the tricky part. I've done an "optimization for ease of use" The templates arrange a path of codlets in a tree fashion. In read, metadata sequentially given by expat library is tested against the codlets, to reach their right place. For writing, at the end of the program or in object disposal, the codlet tree is sequentially stepped again, writing each value. These codlets are capable of read and writing, so is only necessary one of them for each variable in our object. We get all this stuff working only by declaring a line each variable to be saved in object
History
As commented, I've searched for a portable way to avoid those setup prerequisites on a new installation, and this little piece of code is the result.
It may be not difficult to modify the code to use any tree-type data container, like a database, to store this config data.