NoesisGUI

Serialization

Implementing serializable components

Serialization is the process by which components can be saved or loaded from a stream. All kind of streams are supported. That way, serialization can be used to save objects to disk or to a memory buffer.

To make a serializable component, it must implement the ISerializable interface.

NS_INTERFACE ISerializable: public ICommon
{
    virtual NsUInt32 GetVersion() const = 0;
    virtual void Serialize(SerializationData* data) const = 0;
    virtual void Unserialize(UnserializationData* data, NsUInt32 version) = 0;

    virtual void PostUnserialize() = 0;
};

The Serialize() method receives a SerializationData instance with overloaded Serialize methods to serialize all kind of data: simple data, strings, stl data, structs and other serializable components. The same for the Unserialize() method but receiving a UnserializationData instance. Both implementations, Serialize and Unserialize, must remain symmetric, loading the same data that was saved and in the same order.

The following example implements a serializable component

class TestComponent: public BaseComponent, public ISerializable
{
public:
    /// Constructor
    TestComponent():
        i8(50), i16(100), i32(150), i64(250),
        ui8(251), ui16(450), ui32(550), ui64(600), i(45), ui(200), size(333),
        b(true),
        f32(20.0f), f64(5000.0),
        c8('a'), c16(NST('q')), s8("ansi string"), s16(NST("unicode string"))
    {
        bytes.push_back(33);
        bytes.push_back(31);
        bytes.push_back(13);
        bytes.push_back(44);
        bytes.push_back(41);
        bytes.push_back(14);

        heights[0] = 10.0f;
        heights[1] = 20.0f;
        heights[2] = 30.0f;
        heights[3] = 40.0f;
        heights[4] = 50.0f;

        color = Blue;
    }

    /// From ISerializable
    //@{
    NsUInt32 GetVersion() const
    {
        return 123;
    }

    void Serialize(SerializationData* data) const
    {
        data->Serialize(NST("i8"), i8);
        data->Serialize(NST("i16"), i16);
        data->Serialize(NST("i32"), i32);
        data->Serialize(NST("i64"), i64);

        data->Serialize(NST("ui8"), ui8);
        data->Serialize(NST("ui16"), ui16);
        data->Serialize(NST("ui32"), ui32);
        data->Serialize(NST("ui64"), ui64);

        data->Serialize(NST("i"), i);
        data->Serialize(NST("ui"), ui);

        data->Serialize(NST("size"), size);

        data->Serialize(NST("bool"), b);

        data->Serialize(NST("f32"), f32);
        data->Serialize(NST("f64"), f64);

        data->Serialize(NST("c8"), c8);
        data->Serialize(NST("c16"), c16);
        data->Serialize(NST("s8"), s8);
        data->Serialize(NST("s16"), s16);

        data->Serialize(NST("bytes"), bytes);
        data->Serialize(NST("heights"), heights);

        data->Serialize(NST("color"), color);
    }

    void Unserialize(UnserializationData* data, NsUInt32 version)
    {
        NS_ASSERT(version == GetVersion());

        data->Unserialize(NST("i8"), i8);
        data->Unserialize(NST("i16"), i16);
        data->Unserialize(NST("i32"), i32);
        data->Unserialize(NST("i64"), i64);

        data->Unserialize(NST("ui8"), ui8);
        data->Unserialize(NST("ui16"), ui16);
        data->Unserialize(NST("ui32"), ui32);
        data->Unserialize(NST("ui64"), ui64);

        data->Unserialize(NST("i"), i);
        data->Unserialize(NST("ui"), ui);

        data->Unserialize(NST("size"), size);

        data->Unserialize(NST("bool"), b);

        data->Unserialize(NST("f32"), f32);
        data->Unserialize(NST("f64"), f64);

        data->Unserialize(NST("c8"), c8);
        data->Unserialize(NST("c16"), c16);
        data->Unserialize(NST("s8"), s8);
        data->Unserialize(NST("s16"), s16);

        data->Unserialize(NST("bytes"), bytes);
        data->Unserialize(NST("heights"), heights);

        data->Unserialize(NST("color"), color);
    }

    void PostUnserialize() {}
    //@}

    NsInt8 i8;
    NsInt16 i16;
    NsInt32 i32;
    NsInt64 i64;
    NsUInt8 ui8;
    NsUInt16 ui16;
    NsUInt32 ui32;
    NsUInt64 ui64;

    NsInt i;
    NsUInt ui;

    NsSize size;

    NsBool b;

    NsFloat32 f32;
    NsFloat32 f64;

    NsChar8 c8;
    NsChar8 c16;
    NsString8 s8;
    NsString16 s16;

    std::vector<NsByte> bytes;
    NsFloat32 heights[5];

    enum Color
    {
        Red,
        Green,
        Blue
    };

    Color color;

    /// Reflection
    //@{
    NS_BEGIN_REFLECTION(NSS("TestComponent"), TestComponent, BaseComponent)
        NS_IMPL(ISerializable)
    NS_END_REFLECTION

    NS_IMPLEMENT_INLINE_COMPONENT_REFLECTION
    //@}
};

To serialize a component, it must implement the ISerializable interface and must be registered in the kernel component factory.

To serialize a struct, it must implement the Serialize and Unserialize methods. For example:

struct Target
{
    NsFloat32 x, y, z;
    Ptr<BaseComponent> target;

    void Serialize(SerializationData* data) const
    {
        data->Serialize(NST("x"), x);
        data->Serialize(NST("y"), y);
        data->Serialize(NST("z"), z);
        data->Serialize(NST("target"), target);
    }

    void Unserialize(UnserializationData* data)
    {
        data->Unserialize(NST("x"), x);
        data->Unserialize(NST("y"), y);
        data->Unserialize(NST("z"), z);
        data->Unserialize(NST("target"), target);
    }

    /// Reflection
    //@{
    NS_BEGIN_REFLECTION(NSS("Target"), Target, NoParent)
    NS_END_REFLECTION

    NS_IMPLEMENT_INLINE_CLASS_REFLECTION
    //@}
};

Enum serialization

Enumeration are serialized by default as its native size (int32)

enum Color
{
   Red,
   Green,
   Blue
};

Color color;
data->Serialize(NST("color"), color);

If you want to optimize for size, enums can be serialized with the SerializeEnum function. To that function you must pass the size the enum will be converted to:

enum Color
{
   Red,
   Green,
   Blue
};

Color color;
data->SerializeEnum<NsUInt8>(NST("color"), color);

Saving to Stream

To save a component to a stream, a SerializationManager is needed. You start the serialization process with the SerializeBegin() method passing the stream and the serialization options.

Ptr<ISerializationManager> sm = factory->CreateComponent(NSS("SerializationManager"));

sm->SerializeBegin(stream);
sm->Serialize(component0);
sm->Serialize(component1);
sm->Serialize(component2);
sm->SerializeEnd();

Although both components and structs can be serialized, root objects (those passed to the SerializationManager::Serialize()) can only be components.

Loading from a Stream

To load a component from a stream, a SerializationManager is needed. The first thing to do is invoking UnseralizeBegin(). This method will return the number of objects inside the stream.

Ptr<ISerializationManager> sm = factory->CreateComponent(NSS("SerializationManager"));

NsSize numComponents = sm->UnserializeBegin(stream);
for (NsSize i = 0; i < numComponents; i++)
{
    Ptr<TestComponent> component = sm->Unserialize();
}
sm->UnserializeEnd();

Versioning

Versioning is the mechanism supported by the Serialization to allow loading and saving components in a backward compatible way. This means that you can load old version of a component.

The current version of a component is returned in the ISerializable::GetVersion() method. When that component is unserialized, in the method Unserialize() you receive the current version fo the component stored in the stream. This allow for code like this:

void Unserialize(UnserializationData* data, NsUInt32 version)
{
    data->Unserialize(NST("name"), mName);

    if (version > 0x100)
    {
        data->Unserialize(NST("name"), mName2);
    }

    if (version > 0x150)
    {
        data->Unserialize(NST("index"), mIndex);
    }
}

With this code, you are allowed to unserialize object from version 0x000, 0x100 and 0x150.

The versioning process allows for changing how components are serialized while maintaining compatibility with old formats.

Parent serialization

When mixing component in a hierarchy, care must be taken when serializing. Each component will have its own version that must be respected. The macros NS_SERIALIZE_PARENT, NS_UNSERIALIZE_PARENT and NS_POSTUNSERIALIZE_PARENT must be used for that purpose.

////////////////////////////////////////////////////////////////////////////////////////////////////
void DX9FileTexture2D::Serialize(Core::SerializationData* data) const
{
    NS_SERIALIZE_PARENT(data);

    NS_ASSERT(mFileTextureData);
    NS_ASSERT(mFileTextureData->mFaces.size() == 1);
    const Ptr<DX9TextureFace>& face = mFileTextureData->mFaces[0];
    data->Serialize(NST("Levels"), face->mLevels);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void DX9FileTexture2D::Unserialize(Core::UnserializationData* data, NsUInt32 version)
{
    NS_UNUSED(version);
    NS_UNSERIALIZE_PARENT(data);

    NS_ASSERT(!mFileTextureData);
    mFileTextureData = new DX9FileTextureData();
    mFileTextureData->mFaces.push_back(new DX9TextureFace());
    const Ptr<DX9TextureFace>& face = mFileTextureData->mFaces[0];
    data->Unserialize(NST("Levels"), face->mLevels);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void DX9FileTexture2D::PostUnserialize()
{
    NS_POSTUNSERIALIZE_PARENT;
}

Metadata

By default components are serialized with metadata information. The metadata is extra information useful for debugging purposes or for exporting serialized data to other formats like xml for example. With metadata actived the serialization and unserialization process is more robuts because a lot more checks are performed.

It is recommended that your optimized serialized data do not include metadata. Without metadata your files will be smaller and the load times will be faster.

sm->SerializeBegin(stream, SerializeOption_DisableMetaData);

The unserialization process will detect automatically the metadata. You do not have to pass any extra information. But if you want to ignore the metadata even when it is stored in the stream you have to force it

sm->UnserializeBegin(stream, UnserializeOption_IgnoreMetaData);

Endianess

By default, the serialization is done with the endianess of the platform that is serializing. You can force the other endianess with the SerializeOption_SwapEndian flag

sm->SerializeBegin(stream, SerializeOption_SwapEndian);

The unserialization process will detect automatically the endianess of the source data. If the endianess do not match with the platform it will be converted. This is a slow process so it is recommended that you save your data prepared for the correct endianess.

© 2017 Noesis Technologies