SaveLoad

Files CCore/inc/SaveLoad.h CCore/src/SaveLoad.cpp

Serialization basics

Serialization is a process of converting an object state into a byte sequence and inversely. We use the word "save" for the first operation and the "load" for the inverse. The header SaveLoad.h provides a basic binary serialization framework.

To participate in the save/load process a class must implement a save/load methods. The simple example, how to serialize a simple data structure:


struct Data
 {
  T1 t1;
  T2 t2;
  T3 t3;

  // save/load object

  enum { SaveLoadLen = SaveLenCounter<T1,T2,T3>::SaveLoadLen };
  
  template <class Dev>
  void save(Dev &dev) const
   {
    dev.template use<BeOrder>(t1,t2,t3);
   }
  
  template <class Dev>
  void load(Dev &dev)
   {
    dev.template use<BeOrder>(t1,t2,t3);
   }
 };

Four basic types uint8, uint16, uint32, and uint64 are serializable, either in the big-endian or in the little-endian format. An order specifier (BeOrder or LeOrder) affects only the basic type argument's serialization (t1,t2,t3). If none of them has a basic type, you may use the operator ():


  template <class Dev>
  void save(Dev &dev) const
   {
    dev(t1,t2,t3);
   }
  
  template <class Dev>
  void load(Dev &dev)
   {
    dev(t1,t2,t3);
   }

Type may not define the constant SaveLoadLen. But if it is defined, the type must serialize itself in exact number of bytes. This constant is used to optimize a serialization process.

A serialization device does its job with the operator (), which accepts any number of arguments. If an Order must be specified, then the method use<Order> must be used instead.

To calculate a save length of the set of objects the function SaveLen() can be used:


template <class ... TT>
ULenSat SaveLen(const TT & ... tt);

Or the meta-function SaveLenCounter for fixed serialization length types.


template <class ... TT>
struct SaveLenCounter
 {
  enum FlagType { Has_SaveLoadLen = true };
 
  enum LenType : ulen { SaveLoadLen = .... };
 };

template <class ... TT>
struct SaveLenCounter
 {
  enum FlagType { Has_SaveLoadLen = false };
 };

Utilities

ProxyLoad() loads the object using a proxy class. The proxy class type is the first template argument class. Proxy type must be loadable and must have the method get() to extract a value of the type T from it.


template <class Proxy,class T,class Dev>
void ProxyLoad(T &obj,Dev &dev);

The following function family is a range serialization helpers. You may load or save the given range of objects using them. Range can be given as an abstract range cursor class or as the (pointer,length) couple.


template <class R,class Dev>
void SaveRange(R r,Dev &dev);

template <class T,class Dev>
void SaveRange(const T *ptr,ulen len,Dev &dev);
  
template <class R,class Dev>
void LoadRange(R r,Dev &dev);

template <class T,class Dev>
void LoadRange(T *ptr,ulen len,Dev &dev);
  
template <class Custom,class R,class Dev>
void SaveRange_use(R r,Dev &dev);

template <class Custom,class T,class Dev>
void SaveRange_use(const T *ptr,ulen len,Dev &dev);
  
template <class Custom,class R,class Dev>
void LoadRange_use(R r,Dev &dev);

template <class Custom,class T,class Dev>
void LoadRange_use(T *ptr,ulen len,Dev &dev);

The following simple structure family is, in particular, a proxy class family for the unsigned type serialization.


struct SaveLoadBe16;

struct SaveLoadBe32;

struct SaveLoadBe64;

struct SaveLoadLe16;

struct SaveLoadLe32;

struct SaveLoadLe64;

/* struct SaveLoadBe16 */ 

struct SaveLoadBe16
 {
  uint8 buf[2];
  
  // constructors
  
  SaveLoadBe16() : buf() {}
  
  SaveLoadBe16(uint8 b0,uint8 b1) 
   {
    buf[0]=b0;
    buf[1]=b1;
   }
  
  explicit SaveLoadBe16(uint16 value)
   {
    buf[0]=uint8(value>>8);
    buf[1]=uint8(value   );
   }
  
  // methods
  
  uint16 get() const 
   { 
    uint16 b0=buf[0];
    uint16 b1=buf[1];
   
    return uint16( (b0<<8)|b1 ); 
   }
  
  // save/load object
  
  enum { SaveLoadLen = 2 };
 
  template <class Dev>
  void save(Dev &dev) const
   {
    dev.put(buf,2);
   }
   
  template <class Dev>
  void load(Dev &dev)
   {
    dev.get(buf,2);
   }
 };

Serialization devices

A serialization output device must provide the following methods:


class Dev
 {
  public:

   // put
  
   void put(uint8 value);
   
   void put(const uint8 *ptr,ulen len);
   
   void put(PtrLen<const uint8> data);
   
   PtrLen<uint8> putRange(ulen len);
   
   // save
   
   template <class Custom,class ... TT>
   void use(const TT & ... tt);
    
   template <class ... TT>
   void operator () (const TT & ... tt);
 };

putRange() method may return the empty range. The return range is reserved and must be filled by an object being serialized.

The default implementation of save methods is below:


   // save
   
   template <class Custom>
   void use() {}
   
   template <class Custom,class T,class ... TT>
   void use(const T &t,const TT & ... tt)
    {
     SaveAdapter<T,Custom>::Save(t,*this);
     
     use<Custom>(tt...);
    }
    
   template <class ... TT>
   void operator () (const TT & ... tt)
    {
     use<NeOrder>(tt...);
    }

A serialization input device must provide the following methods:


class Dev
 {
  public:

   // get

   uint8 get();
   
   void get(uint8 *ptr,ulen len);
   
   void get(PtrLen<uint8> buf);
   
   PtrLen<const uint8> getRange(ulen len);
   
   // load
   
   template <class Custom,class ... TT>
   void use(TT & ... tt);
    
   template <class ... TT>
   void operator () (TT & ... tt);
 };

If the next byte is not available, the method get() should return zero, get(<range>) may leave the range partially filled. getRange() may return the empty range. RangeGetDev provides additional load methods (see below).

The default implementation of the load methods is below:


   // load
   
   template <class Custom>
   void use() {}
   
   template <class Custom,class T,class ... TT>
   void use(T &t,TT & ... tt)
    {
     LoadAdapter<T,Custom>::Load(t,*this);
    
     use<Custom>(tt...);
    }
    
   template <class ... TT>
   void operator () (TT & ... tt)
    {
     use<NeOrder>(tt...);
    }

PutDevBase/GetDevBase

These two classes are to simplify a serialization device's implementation. They implement the standard save/load operation sets.


template <class Dev>
class PutDevBase
 {
   ....

  public:
   
   // put
  
   void put(uint8 value);
   
   void put(const uint8 *ptr,ulen len);
   
   void put(PtrLen<const uint8> data);
   
   PtrLen<uint8> putRange(ulen len);
   
   // save
   
   template <class Custom,class ... TT>
   void use(const TT & ... tt);
    
   template <class ... TT>
   void operator () (const TT & ... tt);
 };


template <class Dev>
class GetDevBase
 {
   ....

  public:
   
   // get

   uint8 get();
   
   void get(uint8 *ptr,ulen len);
   
   void get(PtrLen<uint8> buf);
   
   PtrLen<const uint8> getRange(ulen len);
   
   // load
   
   template <class Custom,class ... TT>
   void use(TT & ... tt);
    
   template <class ... TT>
   void operator () (TT & ... tt);
 }; 

To use these classes you must derive a class like this:


class Dev : public PutDevBase<Dev>
 {
   ....

  public:

   void do_put(uint8 value);
   
   void do_put(const uint8 *ptr,ulen len);
   
   PtrLen<uint8> do_putRange(ulen len);
 };


class Dev : public GetDevBase<Dev>
 {
   ....

  public:

   uint8 do_get();
   
   void do_get(uint8 *ptr,ulen len);
   
   PtrLen<const uint8> do_getRange(ulen len);
 };

BufPutDev

BufPutDev is a serialization output device. It is lightweight, in fact, it is a wrapper over the output buffer pointer. It does not check the output overflow, so make sure the output buffer has enough room before serialization.


class BufPutDev
 {
   uint8 *buf;
 
  public:
  
   explicit BufPutDev(uint8 *buf_) : buf(buf_) {}
   
   uint8 * getRest() const { return buf; }

   // put
  
   void put(uint8 value);
   
   void put(const uint8 *ptr,ulen len);
   
   void put(PtrLen<const uint8> data);
   
   PtrLen<uint8> putRange(ulen len);
   
   // save
   
   template <class Custom,class ... TT>
   void use(const TT & ... tt);
    
   template <class ... TT>
   void operator () (const TT & ... tt);
 };

Constructor takes a pointer to the output buffer. Serialization bytes will be stored in this buffer. You can make a copy of an object of this class.

getRest() returns the current output position.

Remaining methods are the standard serialization output device methods. Put methods just put bytes and adjust the position.

CountPutDev

This device is used to count the number of output bytes with the overflow control. Data itself goes nowhere. To represent the byte number the type ULenSat is used.


class CountPutDev
 {
   ULenSat count;
   
  public: 
  
   CountPutDev() {}
   
   // methods
   
   operator ULenSat() const { return count; }
   
   // put
  
   void put(uint8 value);
   
   void put(const uint8 *ptr,ulen len);
   
   void put(PtrLen<const uint8> data);
   
   PtrLen<uint8> putRange(ulen len);
   
   // save
   
   template <class Custom,class ... TT>
   void use(const TT & ... tt);
    
   template <class ... TT>
   void operator () (const TT & ... tt);
 };

BufGetDev

BufGetDev is a serialization input device. It is lightweight, in fact, it is a wrapper over the input buffer pointer. It does not check the input underflow. So it should be used mostly with fixed serialization length types.


class BufGetDev
 {
   const uint8 *buf;
 
  public:
  
   explicit BufGetDev(const uint8 *buf_) : buf(buf_) {}
   
   const uint8 * getRest() const { return buf; }

   // get

   uint8 get();
   
   void get(uint8 *ptr,ulen len);
   
   void get(PtrLen<uint8> buf);
   
   PtrLen<const uint8> getRange(ulen len);
   
   // load
   
   template <class Custom,class ... TT>
   void use(TT & ... tt);
    
   template <class ... TT>
   void operator () (TT & ... tt);
 };

Constructor takes a pointer to the input buffer. Serialization bytes will be taken from this buffer. You can make a copy of an object of this class.

getRest() returns the current input position.

Remaining methods are the standard serialization input device methods. Get methods just get bytes and adjust the position.

RangeGetDev

RangeGetDev is a serialization input device. It takes bytes from the given byte range. There is a error flag. This flag is set in case of underflow or manually by the object being serialized.


class RangeGetDev
 {
   PtrLen<const uint8> range;
   bool nok;
   
  public: 
  
   explicit RangeGetDev(PtrLen<const uint8> range_) : range(range_),nok(false) {}
   
   bool operator ! () const { return nok; }
   
   PtrLen<const uint8> getRest() const { return range; }
   
   // get
   
   uint8 get(); 
   
   void get(uint8 *ptr,ulen len); 
    
   void get(PtrLen<uint8> buf);
   
   PtrLen<const uint8> getRange(ulen len);
    
   // load
   
   template <class Custom,class ... TT>
   void use(TT & ... tt)
    
   template <class ... TT>
   void operator () (TT & ... tt);

   // extra
   
   PtrLen<const uint8> getFinalRange();
    
   PtrLen<const uint8> getFinalRange(ulen len);
    
   void fail() { nok=true; }
   
   NegBool finish();
 };

Constructor argument is an input byte range.

operator ! returns the error flag.

getRest() returns the current input range.

The next methods are the the standard serialization input device methods. They set the error flag in case of underflow. Load methods are optimized if a sequence of fixed serialization length objects are loaded.

The final group of methods is extra load methods.

getFinalRange() pops the current input range.

getFinalRange(ulen len) pops the current input range, if it has the given length. Otherwise it sets the error flag.

fail() sets the error flag.

finish() sets the error flag if the current input range is not empty. The error flags in form of NegBool is returned.