Remote Call Framework 3.4
Advanced Serialization

Serializing generic C++ objects is a complex task in and of itself. This section describes some of the more advanced features of RCF's internal serialization framework.

Polymorphic Serialization

RCF will automatically detect and serialize polymorphic pointers and references, as fully derived types. However, to do this, RCF needs to be configured with two pieces of information about the polymorphic type being serialized. First, RCF needs a runtime identifier string for the derived type. Second, it needs to know which base types the derived type will be serialized through.

Here is an example of a polymorphic class hierarchy, with associated serialization functions:

class A
{
public:
virtual ~A()
{}
void serialize(SF::Archive &ar)
{
ar & mA;
}
int mA = 0;
};
class B : public A
{
public:
void serialize(SF::Archive &ar)
{
SF::serializeParent<A>(ar, *this);
ar & mB;
}
int mB = 0;
};
class C : public B
{
public:
void serialize(SF::Archive &ar)
{
SF::serializeParent<B>(ar, *this);
ar & mC;
}
int mC = 0;
};

Note that SF::serializeParent() is used to invoke base class serialization code. If you try to serialize the parent class directly, e.g. by calling ar & static_cast<A&>(*this), RCF will detect that the parent class is actually a derived class, and will try to serialize the derived class once again.

Now consider the following RCF interface, which utilizes a class X containing polymorphic A pointers:

class X
{
public:
void serialize(SF::Archive &ar)
{
ar & mAPtr;
}
std::shared_ptr<A> mAPtr;
};
RCF_BEGIN(I_Echo, "I_Echo")
RCF_METHOD_R1(X, Echo, X)
RCF_END(I_Echo)

In order to polymorphically serialize X::mAPtr, we need to do two things:

  • Use SF::registerType() to set runtime identifiers for the B and C classes. The runtime identifiers will be included in serialized archives when B and C objects are serialized, and will allow the deserialization code to construct objects of the appropriate type.
  • Use SF::registerBaseAndDerived() to specify which base classes the derived classes will be serialized through.

In our case, the following code is sufficient:

// Register polymorphic types.
SF::registerType<B>("B");
SF::registerType<C>("C");
// Register base/derived relationships for polymorphic types.
SF::registerBaseAndDerived<A, B>();
SF::registerBaseAndDerived<A, C>();

This code needs to be executed at the startup of both the client and the server. Once executed, we can make remote calls to the Echo() method and the polymorphic A pointers will be serialized and deserialized, as fully derived types.

RcfClient<I_Echo> client(( RCF::TcpEndpoint(port)));
X x1;
x1.mAPtr.reset( new B() );
X x2 = client.Echo(x1);
x1.mAPtr.reset( new C() );
x2 = client.Echo(x1);

Pointer Tracking

If you serialize a pointer to the same object twice, RCF will by default serialize the entire object twice. This means that when the pointers are deserialized, they will point to two distinct objects. In most applications this is usually not an issue. However, some applications may want the deserialization code to instead create two pointers to the same object.

RCF supports this through a pointer tracking concept, where an object is serialized in its entirety, only once, regardless of how many pointers to it are serialized. Upon deserialization, only a single object is created, and multiple pointers can then be deserialized, pointing to the same object.

To demonstrate pointer tracking, here is an an I_Echo interface with an Echo() function that takes a pair of std::shared_ptr<> objects:

typedef
std::pair< std::shared_ptr<std::string>, std::shared_ptr<std::string> >
PointerPair;
RCF_BEGIN(I_Echo, "I_Echo")
RCF_METHOD_R1(PointerPair, Echo, PointerPair)
RCF_END(I_Echo2)

Here is client-side code to call Echo():

std::shared_ptr<std::string> ptr1( new std::string("one"));
std::shared_ptr<std::string> ptr2( ptr1 );
PointerPair ret = client.Echo( std::make_pair(ptr1, ptr2));

If we make a call to Echo() with a pair of shared_ptr<> objects pointing to the same std::string, we'll find that the returned pair points to two distinct std::string objects. To get them to point to the same std::string, we can enable pointer tracking, on the client-side:

RcfClient<I_Echo> client(( RCF::TcpEndpoint(port)));
client.getClientStub().setEnableSfPointerTracking(true);

, and also on the server-side:

class EchoImpl
{
public:
template<typename T>
T Echo(T t)
{
return t;
}
};

The two returned shared_ptr<> objects will now point to the same instance.

Pointer tracking is a relatively expensive feature and should only be enabled if your application requires it. Pointer tracking requires the serialization framework to track all pointers and values that are being serialized to and from an archive, resulting in significant performance overhead.

Interchangeable Types

Serialization and deserialization tend to be performed symmetrically, in the sense that serialization code writes a type T to an archive, and deserialization code reads the same type T from the archive.

Symmetry is not a requirement for serialization, however. If two classes A and B have serialization functions that serialize the same number and type of members, then you can serialize an A instance to an archive and deserialize a B instance from the archive. Essentially, the classes A and B are interchangeable from a serialization point of view.

RCF guarantees serialization interchangeability for a number of types. For example, for any type T, T and T * are interchangeable. So you can serialize a pointer, and then deserialize it as a value:

// Serializing a pointer.
int * pn = new int(5);
std::ostringstream ostr;
SF::OBinaryStream(ostr) << pn;
delete pn;
// Deserializing a value.
int m = 0;
std::istringstream istr(ostr.str());
SF::IBinaryStream(istr) >> m;
// m is now 5

Furthermore, smart pointers are interchangeable with native pointers, so you can serialize a std::shared_ptr<T> and then deserialize it into a value T. Here is another example:

// Client-side implementation of X, using int.
class X
{
public:
int n;
void serialize(SF::Archive & ar, unsigned int)
{
ar & n;
}
};
// Server-side implementation of X, using shared_ptr<int>.
class X
{
public:
std::shared_ptr<int> spn;
void serialize(SF::Archive & ar)
{
ar & spn;
}
};
// Even with different X implementations, client and server can still
// interact through this interface.
RCF_BEGIN(I_EchoX,"I_EchoX")
RCF_METHOD_R1(X, Echo, X)
RCF_END(I_EchoX)

A significant consequence of this is that if you start out using T, T*, std::shared_ptr<T>, std::unique_ptr<T> or some other smart pointer in your RCF interface, you can always change it at a later point in time, without breaking backward compatibility.

The following are classes of types that RCF guarantees to be interchangeable:

  • T, T *, std::unique_ptr<T>, std::shared_ptr<T>, boost::scoped_ptr<T>, boost::shared_ptr<T>
  • Integral types of equivalent size, e.g. unsigned int and int
  • C++98 enums and 32 bit integral types
  • STL containers of T, where T is non-primitive
  • STL containers of T (except std::vector<> and std::basic_string<>), where T is primitive
  • std::vector<T> and std::basic_string<T>, where T is primitive
  • std::string, std::vector<char>, and RCF::ByteBuffer

Unicode Strings

Serialization of std::wstring objects presents some portability issues, as different platforms have different definitions of wchar_t and different Unicode encodings for std::wstring.

RCF resolves this by converting std::wstring objects to UTF-8 before serializing.

  • On platforms with 16 bit wchar_t, RCF assumes that any std::wstring passed to it, is encoded in UTF-16, and converts between UTF-16 and UTF-8.
  • On platforms with 32 bit wchar_t, RCF assumes that any std::wstring passed to it is encoded in UTF-32, and converts between UTF-32 and UTF-8.

If your application is sending or receiving std::wstring objects, encoded in something other than the assumed UTF-16 or UTF-32 encodings, you should be aware that cross-platform portability may be affected.