10 Building Integration Objects (C++)

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. The stream can then be deserialized in an alternate language using a similar POF-based class definition.

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

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 9, "Using the Coherence C++ Object Model."

The following sections are included in this chapter:

10.1 POF Intrinsics

The following types are internally supported by POF, and do not require special handling by the user:

  • String

  • Integer16 .. Integer64

  • Float32, Float64

  • Array<> of primitives

  • ObjectArray

  • Boolean

  • Octet

  • Character16

Additionally, automatic POF serialization is provided for classes implementing these common interfaces:

  • Map

  • Collection

  • Exception

10.2 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 10-1 lists some requirements and limitations of each approach.

Table 10-1 Requirements and Limitations of Serialization Options

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

Managed<T>

No

No

Yes

Yes

Yes

PortableObject

Yes

Yes

No

No

Yes

PofSerializer

Yes

Yes

Yes

Yes

No


All of these approaches share certain similarities:

  • Serialization routines that allow the data items to be encoded to POF must be implemented.

  • The data object's fields are identified by using numeric indexes.

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

  • Data objects used as cache keys must support equality comparisons and hashing.

10.2.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 10-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 10-2:

Example 10-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. However, they are not required as 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 take effect.

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

Example 10-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;

10.2.2 PortableObject (Self-Serialization)

The PortableObject interface is 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 10-4, the above Address example can be rewritten as a managed class, and implement the PortableObject interface, which fully embraces the Coherence object model as part of the definition of the class. For example, using coherence::lang::String rather then std::string for data members.

Example 10-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)
      {
       initialize(m_vsCity, hIn->readString(0));
       initialize(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:
     FinalView<String> m_vsCity;
     FinalView<String> m_vsState;
     const int32_tm_nZip;
  };
COH_REGISTER_PORTABLE_CLASS(1234, Address); // type ID registration—this must
                                            // appear in the .cpp not the .hpp 

Example 10-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 10-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 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.

10.2.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, an AddressSerializer is written 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. Typically, it is not necessary to use this approach, as either the Managed<T> or PortableObject approaches suffice. This approach is primarily of interest when you have a managed object with const data members. Consider Example 10-6, a non-PortableObject version of a managed Address.

Example 10-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(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) {}

  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 MemberView<String> m_vsCity;
    const MemberView<String> m_vsState;
    const int32_t      m_nZip;
  };

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

Example 10-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));

10.3 Registering Custom C++ Types

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. A POF configuration file is only needed on the nodes where objects are serialized and deserialize.

10.4 Implementing a Java Version of a C++ Object

After completing any of the above approaches, a data object is ready to be stored within the Coherence cluster. You can perform get and put based operations with your objects. However, to make use of more advanced features of Coherence: such as queries or entry processors; or if you use a key that is not a simple type; or when you use a cache loader and cache store, you must write some Java code. For these advanced features to work the Coherence Java based cache servers must 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.

10.5 Understanding Serialization 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. For 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.