How to Create Custom Variant Types

Apart from the built-in types, Variants are capable of handling instances of arbitrary types as long as they inherit from VariantDataClass. To create a custom Variant type, four steps need to be taken:

  • Write an Ice interface with the basic type description
  • Add a Variant type id for the new type
  • Write a C++ implementation of the type
  • Add the created type to the object factory registry

Ice Interface

The first part of a custom Variant type is an Ice interface that allows using the new type in Ice communication. The interface should inherit from VariantDataClass and define the intended data structure.

In this example, we implement a custom Variant type for complex numbers, defined by a real and an imaginary part, both stored as floats.

module armarx
{
class ComplexFloatBase extends VariantDataClass
{
float real;
float imag;
};
};

Type Id

Add a VariantTypeId declaration somewhere in the variant's header.

{
const VariantTypeId TestComplexFloat = Variant::addTypeName("::armarx::ComplexFloatBase");
}

Note: armarx::ComplexFloatBase must be unique in all ArmarX projects.

C++ Implementation

The second part of a custom Variant data type is the C++ implementation of the defined Ice interface ComplexFloatBase. There are three types of methods that need to be present in the implementation:

  • Custom data type interface
    • Parameterized constructors
    • getReal, getImag for accessing the data value
  • Standard data type interface
    • ice_clone
    • clone
    • output for printing the data value
    • getType for retrieving the Id of the new type
    • validate for checking the data's sanity
  • Serialization -
    • serialize, deserialize for serializing to JSON for MemoryX
class TestComplexFloat : virtual public ComplexFloatBase
{
template <class BaseClass, class VariantClass>
friend class GenericFactory;
public:
TestComplexFloat()
{
real = 0;
imag = 0;
}
TestComplexFloat(float real, float imag)
{
this->real = real;
this->imag = imag;
}
float getReal()
{
return real;
}
float getImag()
{
return imag;
}
Ice::ObjectPtr ice_clone() const override
{
return this->clone();
}
VariantDataClassPtr clone(const Ice::Current& c = Ice::emptyCurrent) const override
{
return new TestComplexFloat(*this);
}
std::string output(const Ice::Current& c = Ice::emptyCurrent) const override
{
std::stringstream s;
s << "(" << real << " + " << imag << "i)";
return s.str();
}
VariantTypeId getType(const Ice::Current& c = Ice::emptyCurrent) const override
{
return armarx::VariantType::TestComplexFloat;
}
bool validate(const Ice::Current& c = Ice::emptyCurrent) override
{
return true;
}
public:
void serialize(const armarx::ObjectSerializerBasePtr& serializer, const ::Ice::Current& = Ice::emptyCurrent) const override
{
AbstractObjectSerializerPtr obj = AbstractObjectSerializerPtr::dynamicCast(serializer);
obj->setFloat("real", real);
obj->setFloat("imag", imag);
}
void deserialize(const armarx::ObjectSerializerBasePtr& serializer, const ::Ice::Current& = Ice::emptyCurrent) override
{
AbstractObjectSerializerPtr obj = AbstractObjectSerializerPtr::dynamicCast(serializer);
real = obj->getFloat("real");
imag = obj->getFloat("imag");
}
};

The next step is to define a pointer type for the new data type:

using TestComplexFloatPtr = IceInternal::Handle<TestComplexFloat>;

Object Factory

Finally, the new type needs to be registered to allow it to be used in Ice communication. The factory tells ArmarX, which exact type to instantiate when the respective interface is used in communication. In this case, whenever a ComplexFloatBase is transferred via Ice, it is translated into a TestComplexFloat on reception.

{
class TestFactories : public FactoryCollectionBase
{
public:
ObjectFactoryMap getFactories() override
{
add<ComplexFloatBase, TestComplexFloat>(map);
return map;
}
static const FactoryCollectionBaseCleanUp TestFactoriesVar;
};
}

The variable needs to be initalized in the the cpp-file of the Object Factory like this:

{
const FactoryCollectionBaseCleanUp TestFactories::TestFactoriesVar = FactoryCollectionBase::addToPreregistration(new armarx::ObjectFactories::TestFactories());
}

Usage

The new data type can be used as if it was one of the types already included in ArmarX:

Variant varComplex(new TestComplexFloat(1, 2));

If you want to receive Variants via Ice (e.g. implement an interface that uses variants), the ObjectFactory-header of the library, where the variant is implemented, needs to be included and the library needs to be linked.

@note If you want to use the variant in the statechart editor, see How to add your own data type to a statechart

c
constexpr T c
Definition: UnscentedKalmanFilterTest.cpp:43
armarx::ObjectFactoryMap
std::map< std::string, Ice::ValueFactoryPtr > ObjectFactoryMap
Definition: FactoryCollectionBase.h:61
IceInternal::Handle
Definition: forward_declarations.h:8
armarx::FactoryCollectionBase::addToPreregistration
static FactoryCollectionBaseCleanUp addToPreregistration(FactoryCollectionBasePtr factoryCollection)
Definition: FactoryCollectionBase.cpp:41
armarx::VariantType
Definition: ChannelRef.h:160
armarx::VariantTypeId
Ice::Int VariantTypeId
Definition: Variant.h:44
armarx::navigation::client::validate
void validate(const std::vector< WaypointTarget > &path)
Definition: ice_conversions.h:70
armarx::AbstractObjectSerializerPtr
IceInternal::Handle< AbstractObjectSerializer > AbstractObjectSerializerPtr
Definition: AbstractObjectSerializer.h:45
armarx::ObjectFactories
Definition: CoreObjectFactories.h:54
armarx::aron::type::ObjectPtr
std::shared_ptr< Object > ObjectPtr
Definition: Object.h:36
armarx::ctrlutil::s
double s(double t, double s0, double v0, double a0, double j)
Definition: CtrlUtil.h:33
armarx
This file offers overloads of toIce() and fromIce() functions for STL container types.
Definition: ArmarXTimeserver.cpp:28
armarx::Variant::addTypeName
static VariantTypeId addTypeName(const std::string &typeName)
Register a new type for the use in a Variant.
Definition: Variant.cpp:751