Automagically
Cereal provides a separate portable binary archive serializer which takes care of that. Most of the time we dont care though. Even current urho serializer does not.
And a small update on progress:
class CerealTest : public Serializable
{
URHO3D_OBJECT(CerealTest, Serializable);
virtual void Serialize(::cereal::BinaryInputArchive& ar) { ar(*this); }
virtual void Serialize(::cereal::BinaryOutputArchive& ar) { ar(*this); }
virtual void Serialize(::cereal::PortableBinaryInputArchive& ar) { ar(*this); }
virtual void Serialize(::cereal::PortableBinaryOutputArchive& ar) { ar(*this); }
virtual void Serialize(::cereal::XMLInputArchive& ar) { ar(*this); }
virtual void Serialize(::cereal::XMLOutputArchive& ar) { ar(*this); }
virtual void Serialize(::cereal::JSONInputArchive& ar) { ar(*this); }
virtual void Serialize(::cereal::JSONOutputArchive& ar) { ar(*this); }
public:
explicit CerealTest(Context* context) : Serializable(context)
{
}
static void RegisterObject(Context* context)
{
context->RegisterFactory<CerealTest>();
URHO3D_ATTRIBUTE("Integer Foo", int, foo_, 0, AM_DEFAULT);
}
template<typename Archive>
void serialize(Archive& ar)
{
ar(::cereal::make_nvp(BaseClassName::GetTypeNameStatic().CString(), cereal::base_class<BaseClassName>(this))
,CEREAL_NVP(hash_)
,CEREAL_NVP(vector2_)
);
}
int foo_ = 0;
Vector2 vector2_{12, 34};
StringHash hash_;
};
Produces:
<?xml version="1.0" encoding="utf-8"?>
<root>
<value>
<value name="Serializable">
<value name="Integer Foo">123</value>
</value>
<value name="hash_">555</value>
<value name="vector2_">12 34</value>
</value>
</root>
[
{
"Serializable": {
"Integer Foo": 1234
},
"hash_": 555,
"vector2_": [
12.0,
34.0
]
}
]
XML output required some modifications to cereal, but they are pretty minor. Overall i am pretty happy with output. There are a few things to note.
Cereal has support for polymorphic types. Problem with that is that it requires extra macro to be used at global scope. This example would have required following:
CEREAL_REGISTER_TYPE(CerealTest);
CEREAL_REGISTER_POLYMORPHIC_RELATION(CerealTest::BaseClassName, CerealTest)
This sucks as it is extra bookkeeping user must not forget about. Besides we already have this information recorded using URHO3D_OBJECT()
macro. Those macros are also used to enable cereal to serialize object from most derived to the base. I could not get that part working however, so as a workaround i added bunch of virtual void Serialize()
that call a correct templated serialization function variant. At least these can be hidden behind URHO3D_OBJECT()
macro if need be.
Lets look at actual serialization function:
template<typename Archive>
void serialize(Archive& ar)
{
ar(::cereal::make_nvp(BaseClassName::GetTypeNameStatic().CString(), cereal::base_class<BaseClassName>(this))
,CEREAL_NVP(hash_)
,CEREAL_NVP(vector2_)
);
}
Single function handles serialization and deserialization to any number of supported formats. I am still stunned about this make_nvp()
(stands for “make name-value-pair”) for serialization of base class is not strictly necessary, but it makes output nicer. One thing you may find confusing is that Integer Foo
attribute is registered to CerealTest
class, but it is serialized as part of Serializable
class. This is because Serializable
class manages all attributes. This could probably be solved though.
As you see StrignHash
and Vector2
types are serialized transparently to the user as well. Their values in XML and JSON look user-friendly, but it comes at a cost of implementing serialization a little bit differently for every format.
// Vector2
template <class Archive> inline
void CEREAL_SAVE_FUNCTION_NAME(Archive& ar, const Urho3D::Vector2& value)
{
if (std::is_same<Archive, JSONInputArchive>::value || std::is_same<Archive, JSONOutputArchive>::value)
{
size_type size = 2;
ar(make_size_tag(size)); // Make json use array. Without this output would be {"value0": 12.0, "value1": 34.0}
ar(value.x_, value.y_);
}
else if (std::is_same<Archive, XMLInputArchive>::value || std::is_same<Archive, XMLOutputArchive>::value)
ar(std::string(value.ToString().CString())); // Serialize as string to make everything fit into one xml tag.
else
ar(value.x_, value.y_); // Binary archives, at least they are trivial
}
template <class Archive> inline
void CEREAL_LOAD_FUNCTION_NAME(Archive& ar, Urho3D::Vector2& value)
{
if (std::is_same<Archive, JSONInputArchive>::value || std::is_same<Archive, JSONOutputArchive>::value)
{
size_type size = 2;
ar(make_size_tag(size));
ar(value.x_, value.y_);
}
else if (std::is_same<Archive, XMLInputArchive>::value || std::is_same<Archive, XMLOutputArchive>::value)
{
std::string text;
ar(text);
value = Urho3D::ToVector2(text.c_str());
}
else
ar(value.x_, value.y_);
}
URHO3D_SERIALIZE_PLAIN(XML, Urho3D::Vector2); // Hint cereal that this type is simple enough that it does not need multi-level xml tree.
For time being i removed versioning. This matter still needs some thought. Object version has has also to be declared with a macro CEREAL_CLASS_VERSION(Type, Version)
at global scope. Maybe versioning could be achieved by defining using Version = 1;
in the class and have cereal use that automatically if it is present.
Conclusion: current implementation is very early WIP, but it holds promise to satisfy all requirements. Implementation that is on par with current features is possible now (although file formats would change). More concerning part is support for polymorphism, so we could also serialize any subclasses of Serializable
stored in shared pointers and have them restored properly. This would likely require some serious work reimplementing bulk of cereal’s features to use metadata defined in URHO3D_OBJECT()
macro.