3 Building Integration Objects for C++ Clients

Enabling C++ clients to successfully store C++ based objects within a Coherence cluster relies on a platform-independent serialization format known as POF (Portable Object Format). POF allows value objects to be encoded into a binary stream in such a way that the platform and language origin of the object is irrelevant.

While the Coherence C++ API includes several POF serializable classes, custom data types require serialization support.

Note:

This document assumes familiarity with the Coherence C++ Object Model, including advanced concepts such as specification-based class definitions. For more information on these topics, see Chapter 2, "Understanding the Coherence C++ Object Model."

3.1 Serialization Options

While the Coherence C++ API offers a single serialization format (POF), it offers a variety of APIs for making a class serializable. Ultimately whichever approach is used, the same binary POF format is produced. The following approaches are available for making a class serializable:

Table 3-1 lists some of the requirements and limitations of each approach.

Table 3-1 Requirements and Limitations of Serialization Options

Approach Requires derivation from Object Supports const data-members External serialization routine Requires zero-arg constructor

Managed<T>

No

Yes

Yes

Yes

PortableObject

Yes

No

No

Yes

PofSerializer

Yes

Yes

Yes

No


All of these approaches share certain similarities:

  • you must implement serialization routines that will allow the data items to be encoded to POF

  • the data object's fields are identified by using numeric indices

  • the data object class and serialization mechanism must be registered with Coherence

  • data objects used as cache keys, must support equality comparisons, and hashing

3.1.1 Managed<T> (Free-Function Serialization)

For most pre-existing data object classes, the use of Managed<T> offers the easiest means of integrating with Coherence for C++.

For a non-managed class to be compatible with Managed<T> it must have the following characteristics:

  • zero parameter constructor (public or protected): CustomType::CustomType()

  • copy constructor (public or protected): CustomType::CustomType(const CustomType&)

  • equality comparison operator: bool operator==(const CustomType&, const CustomType&)

  • std::ostream output function: std::ostream& operator<<(std::ostream&, const CustomType&)

  • hash function: size_t hash_value(const CustomType&)

The following example presents a simple Address class, which has no direct knowledge of Coherence, but is suitable for use with the Managed<T> template.

Note:

In the interest of brevity, example class definitions are in-lined within the declaration.

Example 3-1 A Non-Managed Class

class Address
  {
  public:
    Address(const std::string& sCity, const std::String& sState, int nZip)
       : m_sCity(sCity), m_sState(sState), m_nZip(nZip) {}

    Address(const Address& that) // required by Managed<T>
       : m_sCity(that.m_sCity), m_sState(that.m_sState), m_nZip(that.m_nZip) {}

  protected:
    Address() // required by Managed<T>
      : m_nZip(0) {}

  public:
    std::string  getCity()  const {return m_sCity;}
    std::string  getState() const {return m_sState;}
    int          getZip()   const {return m_nZip;}

  private:
    const std::string m_sCity;
    const std::string m_sState;
    const int         m_nZip;
  };

bool operator==(const Address& addra, const Address& addrb) // required by Managed<T>
  {
  return addra.getZip()   == addrb.getZip() &&
         addra.getState() == addrb.getState() &&
         addra.getCity()  == addrb.getCity();
  }

std::ostream& operator<<(std::ostream& out, const Address& addr) // required by Managed<T>
  {
  out << addr.getCity() << ", " << addr.getState() << "  " << addr.getZip();
  return out;
  }

size_t hash_value(const Address& addr) // required by Managed<T>
  {
  return (size_t) addr.getZip();
  }

When combined with Managed<T>, this simple class definition becomes a true "managed object", and is usable by the Coherence C++ API. This definition has yet to address serialization. Serialization support is added Example 3-2:

Example 3-2 Managed Class using Serialization

#include "coherence/io/pof/SystemPofContext.hpp"

#include "Address.hpp"

using namespace coherence::io::pof;

COH_REGISTER_MANAGED_CLASS(1234, Address); // type ID registration—this must
                                           // appear in the .cpp not the .hpp 

template<> void serialize<Address>(PofWriter::Handle hOut, const Address& addr)
  {
  hOut->writeString(0, addr.getCity());
  hOut->writeString(1, addr.getState());
  hOut->writeInt32 (2, addr.getZip());
  }

template<> Address deserialize<Address>(PofReader::Handle hIn)
  {
  std::string sCity  = hIn->readString(0);
  std::string sState = hIn->readString(1);
  int         nZip   = hIn->readInt32 (2);
  return Address(sCity, sState, nZip);
  }

Note:

The serialization routines must have knowledge of Coherence. They do not, however, need to be part of the class definition file. They can be placed in an independent source file, and if they are linked into the final application, they will take effect.

With the above pieces in place, Example 3-3 illustrates instances of the Address class wrapped by using Managed<T> as Managed<Address>, and supplied to the Coherence APIs:

Example 3-3 Instances of a Class Wrapped with Managed<T>

// construct the non-managed version as usual
Address office("Redwood Shores", "CA", 94065);

// the managed version can be initialized from the non-managed version
// the result is a new object, which does not reference the original
Managed<Address>::View vOffice = Managed<Address>::create(office);
String::View           vKey    = "Oracle";

// the managed version is suitable for use with caches
hCache->put(vKey, vAddr);
vOffice = cast<Managed<Address>::View>(hCache->get(vKey));

// the non-managed class's public methods/fields remain accessible
assert(vOffice->getCity()  == office.getCity());
assert(vOffice->getState() == office.getState());
assert(vOffice->getZip()   == office.getZip());

// conversion back to the non-managed type may be performed using the
// non-managed class's copy constructor.
Address officeOut = *vOffice;

3.1.2 PortableObject (Self-Serialization)

The PortableObject interface is supported by the Java, .NET, and C++ versions of Coherence. It is an interface similar in concept to java.io.Externalizable, which allows an object to control how it is serialized. Any class which extends from coherence::lang::Object is free to implement the coherence::io::pof::PortableObject interface to add serialization support. Note that the class must extend from Object, which then dictates its life cycle.

In Example 3-4, we can re-write the above Address example as a managed class, and implement the PortableObject interface. In doing so, we are choosing to fully embrace the Coherence object model as part of the definition of the class, for instance using coherence::lang::String rather then std::string for data members.

Example 3-4 A Managed Class that Implements PortableObject

#include "coherence/lang.ns"

#include "coherence/io/pof/PofReader.hpp"
#include "coherence/io/pof/PofWriter.hpp"
#include "coherence/io/pof/PortableObject.hpp"

#include "coherence/io/pof/SystemPofContext.hpp"

using namespace coherence::lang;

using coherence::io::pof::PofReader;
using coherence::io::pof::PofWriter;

class Address
  : public cloneable_spec<Address,
      extends<Object>,
      implements<PortableObject> >
  {
  friend class factory<Address>;

  protected: // constructors
    Address(String::View vsCity, String::View vsState, int32_t nZip)
       : m_vsCity(self(), vsCity), m_vsState(self(), vsState), m_nZip(nZip) {}

    Address(const Address& that)
       : super(that), m_vsCity(self(), that.m_vsCity), m_sState(self(), that.m_vsState), m_nZip(that.m_nZip) {}

    Address() // required by PortableObject
      : m_nZip(0) {}

  public: // Address interface    virtual String::View  getCity()  const {return m_vsCity;}
    virtual String::View  getState() const {return m_vsState;}
    virtual int32_t       getZip()   const {return m_nZip;}

  public: // PortableObject interface    virtual void writeExternal(PofWriter::Handle hOut) const
      {
      hOut->writeString(0, getCity());
      hOut->writeString(1, getState());
      hOut->writeInt32 (2, getZip());
      }

    virtual void readExternal(PofReader::Handle hIn)
      {
      m_vsCity  = hIn->readString(0);
      m_vsState = hIn->readString(1);
      m_nZip    = hIn->readInt32 (2);
      }

  public: // Objectinterface    virtual bool equals(Object::View that) const
      {
      if (instanceof<Address::View>(that))
        {
        Address::View vThat = cast<Address::View>(that);

        return getZip() == vThat->getZip() &&
               Object::equals(getState(), vThat->getState()) &&
               Object::equals(getCity(), vThat->getCity());
        }

      return false;
      }

    virtual size32_t hashCode() const
      {
      return (size32_t) m_nZip;
      }

    virtual void toStream(std::ostream& out) const
      {
      out << getCity() << ", " << getState() << "  " << getZip();
      }

  private:
    MemberView<String> m_vsCity;
    MemberView<String> m_vsState;
    int32_t            m_nZip;
  };
COH_REGISTER_PORTABLE_CLASS(1234, Address); // type ID registration—this must
                                            // appear in the .cpp not the .hpp 

Example 3-5 illustrates a managed variant of the Address that does not require the use of the Managed<T> adapter and can be used directly with the Coherence API:

Example 3-5 A Managed Class without Managed<T>

Address::View vAddr = Address::create("Redwood Shores", "CA", 94065);
String::View  vKey  = "Oracle";

hCache->put(vKey, vAddr);
Address::View vOffice = cast<Address::View>(hCache->get(vKey));

Serialization by using PortableObject is a good choice when the application has already decided to make use of the Coherence object model for representing its data objects. One drawback to PortableObject is that it does not easily support const data members, as the readExternal method is called after construction, and must assign these values.

3.1.3 PofSerializer (External Serialization)

The third serialization option is also the lowest level one. PofSerializers are classes that provide the serialization logic for other classes. For example, we will write an example AddressSerializer which can serialize a non-PortableObject version of the above managed Address class. Under the covers the prior two approaches were delegating through PofSerializers, they were just being created automatically rather then explicitly. In most cases, it will not be necessary to use this approach, as either the Managed<T> or PortableObject approaches will suffice. This approach is primarily of interest when you have a managed object with const data members. Consider Example 3-6, a non-PortableObject version of a managed Address.

Example 3-6 A non-PortableObject Version of a Managed Class

#include "coherence/lang.ns"

using namespace coherence::lang;

class Address
  : public cloneable_spec<Address> // extends<Object> is implied
  {
  friend class factory<Address>;

  protected: // constructors
    Address(String::View vsCity, String::View vsState, int32_t nZip)
       : m_vsCity(vsCity), m_vsState(vsState), m_nZip(nZip) {}


    Address(const Address& that)
       : super(that), m_vsCity(that.m_vsCity), m_sState(that.m_vsState), m_nZip(that.m_nZip) {}

  public: // Address interface    virtual String::View  getCity()  const {return m_vsCity;}
    virtual String::View  getState() const {return m_vsState;}
    virtual int32_t       getZip()   const {return m_nZip;}

  public: // Objectinterface    virtual bool equals(Object::View that) const
      {
      if (instanceof<Address::View>(that))
        {
        Address::View vThat = cast<Address::View>(that);

        return getZip() == vThat->getZip() &&
               Object::equals(getState(), vThat->getState()) &&
               Object::equals(getCity(), vThat->getCity());
        }

      return false;
      }

    virtual size32_t hashCode() const
      {
      return (size32_t) m_nZip;
      }

    virtual void toStream(std::ostream& out) const
      {
      out << getCity() << ", " << getState() << "  " << getZip();
      }

  private:
    const String::View m_vsCity;
    const String::View m_vsState;
    const int32_t      m_nZip;
  };

Note that this version uses const data members, which makes it not well-suited for PortableObject. Example 3-7 illustrates an external class, AddressSerializer, which will be registered as being responsible for serialization of Address instances.

Example 3-7 An External Class Responsible for Serialization

#include "coherence/lang.ns"

#include "coherence/io/pof/PofReader.hpp"
#include "coherence/io/pof/PofWriter.hpp"
#include "coherence/io/pof/PortableObject.hpp"
#include "coherence/io/pof/SystemPofContext.hpp"

#include "Address.hpp"

using namespace coherence::lang;

using coherence::io::pof::PofReader;
using coherence::io::pof::PofWriter;

class AddressSerializer
  : public class_spec<AddressSerializer,
      extends<Object>,
      implements<PofSerializer> >
  {
  friend class factory<AddressSerializer>;

  protected:
    AddressSerializer();

  public: // PofSerializer interface    virtual void serialize(PofWriter::Handle hOut, Object::View v) const
        {
        Address::View vAddr = cast<Address::View>(v);
        hOut->writeString(0, vAddr->getCity());
        hOut->writeString(1, vAddr->getState());
        hOut->writeInt32 (2, vAddr->getZip());
        hOut->writeRemainder(NULL);
        }

    virtual Object::Holder deserialize(PofReader::Handle hIn) const
      {
      String::View vsCity  = hIn->readString(0);
      String::View vsState = hIn->readString(1);
      int32_t      nZip    = hIn->readInt32 (2);
      hIn->readRemainder();

      return Address::create(vsCity, vsState, nZip);
      }
  };
COH_REGISTER_POF_SERIALIZER(1234, TypedBarrenClass<Address>::create(),AddressSerializer::create()); // This must appear in the .cpp not the .hpp

Usage of the Address remains unchanged:

Address::View vAddr = Address::create("Redwood Shores", "CA", 94065);
String::View  vKey  = "Oracle";

hCache->put(vKey, vAddr);
Address::View vOffice = cast<Address::View>(hCache->get(vKey));

3.2 POF Registration

In addition to being made serializable, each class must also be associated with numeric type IDs. These IDs are well-known across the cluster. Within the cluster, the ID-to-class mapping is configured by using POF user type configuration elements; within C++, the mapping is embedded within the class definition in the form of an ID registration, which is placed within the class's .cpp source file.

The registration technique differs slightly with each serialization approach:

  • COH_REGISTER_MANAGED_CLASS(ID, TYPE)—for use with Managed<T>

  • COH_REGISTER_PORTABLE_CLASS(ID, TYPE)—for use with PortableObject

  • COH_REGISTER_POF_SERIALIZER(ID, CLASS, SERIALIZER)—for use with PofSerializer

Examples of these registrations can be found in above examples.

Note:

Registrations must appear only in the implementation (.cpp) files.

3.3 Need for Java Classes

After completing any of the above approaches your data object will be ready to be stored within the Coherence cluster. This will allow you to perform get and put based operations with your objects. If however you want to make use of more advanced features of Coherence, such as queries, or entry processors you will need to write some Java code. For these advanced features to work the Coherence Java based cache servers need to be able to interact with your data object, rather then simply holding onto a serialized representation of it. To interact with it, and access its properties, a Java version must be made available to the cache servers. The approach to making the Java version serializable over POF is quite similar to the above examples, see com.tangosol.io.pof.PortableObject, and com.tangosol.io.pof.PofSerializer for details, either of which is compatible with all three of the C++ based approaches.

3.4 Performance

Both Managed<T> and PortableObject behind the scenes use a PofSerializer to perform serialization. Each of these approaches also adds some of its own overhead, for instance the Managed<T> approach involves the creation of a temporary version of non-managed form of the data object during deserialization. In the case of PortableObject the lack of support for const data members can have a cost as it avoids optimizations which would have been allowed for const data members. Overall the performance differences may be negligible, but if seeking to achieve the maximum possible performance, direct utilization of PofSerializer may be worth consideration.