|
Loading and saving files is a lot of work. If you write a tool,
you have to stream in and out every single variable you created in your document.
My first implementations of serializing classes were very bloated. My current method is
very simple but efficient and easy to use. I just use *data++ for reading and writing everything.
I like simple code. The method presented here is as simple as you can possibly get. The point
is that it actually works, and that I use it in large applications without problems.
Image I have some structures you want to save:
/****************************************************************************/
struct BlaOp // one operator for an intro
{
sInt x,y,w; // position in the page
sChar Name[32]; // name of the operator
sInt ClassId; // class of the operator
sU32 Data[64]; // payload of the operator
}
struct BlaDoc // the document class
{
sInt Count; // count of operators
BlaOp *Pages[256]; // list of operators
};
/****************************************************************************/
Now I want to write a code that reads and writes the document. I assume
that I have already implemented sReadString() and sWriteString().
/****************************************************************************/
sInt BlaOp::Write(sU32 *&data)
{
*data++ = 1; // write version
*data++ = x;
*data++ = y;
*data++ = z;
*data++ = ClassId;
*data++ = 0; // some dummy data for easy extension
*data++ = 0;
*data++ = 0;
sWriteString(data,Name);
sCopyMem(data,Data,64*4); data+=64;
return sTRUE;
}
sInt BlaOp::Read(sU32 *&data)
{
sInt version = *data++;
if(version<1 || version>1) return sFALSE;
x = *data++;
y = *data++;
z = *data++;
ClassId = *data++;
data+=3;
sReadString(data,Name,sizeof(Name));
sCopyMem(Data,data,64*4); data+=64;
return sTRUE;
}
sInt BlaDoc::Write(sU32 *&data)
{
*data++ = 1;
*data++ = Count;
*data++ = 0;
*data++ = 0;
for(sInt i=0;i<Count;i++)
if(!Ops[i]->Write(data))
return sFALSE;
return sTRUE;
}
sInt BlaDoc::Read(sU32 *&data)
{
sInt version = *data++;
if(version<1 || version>1) return sFALSE;
Count = *data++;
data+=2;
for(sInt i=0;i<Count && ok;i++)
{
Ops[i] = new BlaOp;
if(!Ops[i]->Read(data))
return sFALSE;
}
return sTRUE;
}
/****************************************************************************/
Ok, you get the idea. This is straight forward. But it has serious problems,
but we can get around these problems.
- All data is written as 32 bits
This wastes a lot of space, but I found out that it is just not worth saving
these extra bits. Note, we are not writing data into a 64k intro, we are
writing data for the tool.
- When we write, we need to allocate a huge amount of memory, and if
the saved data exceeds this memory, the application will crash.
Usually I allocate 64MBytes and that should be enough. I make shure that
the application crashes with a reasonable message when I run out of
memory so that I know what happened.
I had lots of fear about this problem, but in practice this simply is not
an issue. The number of crashes because of this implementation is very small,
less than five since fr-08. The number of crashes
I had in the past because of buggy load/save code was much larger.
- You have to duplicate the members code in Read() and Write()
Yes, but it is important to keep this style, you should never use
memcpy(). This ensures that you can still read old files when you
changed the struct.
Ok, that "out of memory" crash stinks. But there is a way around it: In the
beginning of each Write() function, call sWritePatch(data).
This function will check that the pointer is not too near to the end of
the buffer. If it is, it may flush the buffer and change the data pointer
to the new start of the buffer. This assumes that no Write() function writes
more than a certain amount of data (lets say, 64KBytes). If a function
write more than that, like a bitmap or vertex data, you can use a modified
memcpy() function that calls sWritePatch() from time to time.
|