Serializing object to byte-array in C++

19,787

Solution 1

It's likely that your code for save and load will be reasonably generic and would work best in a separate 'manager' class, leaving each data class only with the responsibility of rendering itself as re-loadable:

// Interface class
class Serializable
{
public:
    virtual size_t serialize_size() const = 0;
    virtual void serialize(char* dataOut) const = 0;
    virtual void deserialize(const char* dataIn) = 0;
};

// Load / save manager
class EEPromManager
{
public:
    void save( const Serializable& s )
    {
        char * data;
        size_t data_len;
        reserve_memory( data, data_len, s );
        s.serialize( data );
        EEPROM::Save( data , data_len );
        delete [] data;
    }

    void load( Serializable& s )
    {
        char * data;
        size_t data_len;
        reserve_memory( data, data_len, s );
        EEPROM::Load( data, data_len );
        s.deserialize( data );
        delete [] data;
    }

private:
    char* reserve_memory( char*& data, size_t& data_len, const Serializable& s )
    {
        return new char[ s.serialize_size() ];
    }
};

Each class you intend to serialize / de-serialize should inherit from an interface which mandates the virtual interface for these functions. Note that you'll need to do your own memory management here. I've given a simple example but you'd probably want something a bit more robust.

Then each function should sequentially serialize all attributes of the class (chaining bases classes and calling serialize on aggregate objects if needed.)

class Person : public Serializable
{
public:
    virtual size_t serialize_size() const
    {
        return SerializablePOD<char*>::serialize_size(name) +
               SerializablePOD<int>::serialize_size(age) +
               SerializablePOD<float>::serialize_size(weight);
    }

    virtual void serialize(char* dataOut) const
    {
        dataOut = SerializablePOD<char*>::serialize(dataOut, name);
        dataOut = SerializablePOD<int>::serialize(dataOut, age);
        dataOut = SerializablePOD<float>::serialize(dataOut, weight);
    }
    virtual void deserialize(const char* dataIn)
    {
        dataIn = SerializablePOD<char*>::deserialize(dataIn, name);
        dataIn = SerializablePOD<int>::deserialize(dataIn, age);
        dataIn = SerializablePOD<float>::deserialize(dataIn, weight);
    }

private:
    char* name;
    int   age;
    float weight;
};

You'll benefit from generic code to serialize / de-serialize each separate type so you don't keep having code to write the length of strings etc. I.e. a serialize / de-serialize for each POD type:

template <typename POD>
class SerializablePOD
{
public:
    static size_t serialize_size(POD str)
    {
        return sizeof(POD);
    }
    static char* serialize( char* target, POD value )
    {
        return memcpy( target, &value, serialize_size(value) );
    }
    static const char* deserialize( const char* source, POD& target )
    {
        memcpy( &target, source, serialize_size(target) );
        return source + serialize_size(target);
    }
};

template<>
size_t SerializablePOD<char*>::serialize_size(char* str)
{
    return sizeof(size_t) + strlen(str);
}

template<>
const char* SerializablePOD<char*>::deserialize( const char* source, char*& target )
{
    size_t length;
    memcpy( &length, source, sizeof(size_t) );
    memcpy( &target, source + sizeof(size_t), length );
    return source + sizeof(size_t) + length;
}

Incidentally, you might also need to consider what will happen if you change the schema of an object in a software upgrade. Your saved objects would potentially become corrupted on reloading, unless you code round this using - for example - a class version identifier.

Final thought: At a micro level, what you're doing is in many ways similar to the way POD data is serialised for network transmission, so it may be that you can take advantage of libraries to do that - even if you don't have access to an operating system.

Solution 2

To save a string to binary, usually we save its length and then its content. To save other primitive data, we can simply store their binary form. So in your case, all you need to store is:

Length to name
char array of name
age
weight

So the code to serial is:

size_t buffer_size = sizeof(int) + strlen(name) + sizeof(age) + sizeof(weight);
char *buffer = new char[buffer_size];
*(int*)p = strlen(name);  p += sizeof(int);
memcpy(p, name, strlen(name));  p += strlen(name);
*(int*)p = age;  p += sizeof(int);
*(float*)p = weight;
EEPROM::Save(buffer, buffer_size);
delete[] buffer;

And to read a string from binary buffer, you read its length first, and then copy its data.

Share:
19,787
Jolle
Author by

Jolle

Updated on June 04, 2022

Comments

  • Jolle
    Jolle almost 2 years

    I am working on an embedded device (microcontroller), and I want to save objects to permanent storage (an EEPROM). Most of the serialization solutions I can find, use the file-system in some way, but my target has no file-system.

    Therefore my question is, how can I serialize an object to a byte-array so I can save that byte-array to an EEPROM afterwards?

    Here is an example of what i am trying to do:

    class Person{
         //Constructor, getters and setters are omitted
    
        void save(){
             char buffer[sizeof(Person)]; 
             serialize(buffer);
             EEPROM::Save(buffer, sizeof(Person)); 
        }
    
        void load(){
             char buffer[sizeof(Person)]; 
             EEPROM::Load(buffer, sizeof(Person));
             deserialize(buffer);
        }
    
        void serialize(char* result){
            //?????
        }
    
        Person deserialize(char* buffer){
            //??????
        }
    
    private:
        char* name;  
        int   age; 
        float weight; 
    };