Registering types in the type system

Introduction

For types to be available in the type system and allow them to be type-erased, sent over the network or through signals, each one of them needs to be registered in the type system.

Most of these registration can be done through macros. It is very important that you call macros made to be used in .cpp only in .cpp files, these macros should be processed in a single translation unit.

All registration classes and macros are in:

#include <qi/anyobject.hpp>

Structures

To register a structure, you need to call the macro QI_TYPE_STRUCT() in the corresponding .hpp file.

// point.hpp

namespace Graph {
  struct Point {
    int x;
    int y;
  };
}

QI_TYPE_STRUCT(Graph::Point, x, y);

Or you can do the register in the .cpp:

// point.cpp

// call this outside of any namespace
QI_TYPE_STRUCT_REGISTER(Graph::Point, x, y);

Using PIMPL in structures

Sometime, you have a structure of data, but you don’t want the user to access it directly because you want to constrain values or to updates multiple values at the same time so that the structure stays coherent.

class Vector {
  public:
    void setValues(int x, int y)
    {
      _x = x;
      _y = y;
      _length = std::sqrt(x*x + y*y);
    }
    float length() { return _length; }
    // ...

  private:
    int _x;
    int _y;
    float _length;
};

// How can I register this??

You can’t register this struct because _x, _y and _length are private and you don’t want to make them public because the user could change _x and _y without changing _length.

To register that to the type system, you need to use pimpl:

// vector.hpp

class Vector {
  public:
    Vector();
    // you need a copy constructor because qitype makes copies
    Vector(const Vector& other);
    void setValues(int x, int y) { /* ... */ }
    float length() { /* ... */ }

  private:
    boost::scoped_ptr<struct VectorPrivate> _p;

    // you need this for later
    friend struct VectorPrivate* vectorPrivateAccess(Vector*);
};

// vector.cpp

struct VectorPrivate {
  int _x;
  int _y;
  float _length;
};

Vector::Vector() : _p(new VectorPrivate) {}
Vector::Vector(const Vector& other) : _p(new VectorPrivate(*other._p)) {}

Then you can register the private part of the struct and tell qitype how to access it:

VectorPrivate* vectorPrivateAccess(Vector* vector) {
  return vector->_p;
}

// call these outside of any namespace
QI_TYPE_STRUCT_REGISTER(VectorPrivate, _x, _y, _length);

QI_TYPE_STRUCT_BOUNCE_REGISTER(Vector, VectorPrivate, vectorPrivateAccess);

Every time you transfer a Vector, qimessaging will also transfer its private part and no one can access it without using the accessors.

Struct extension

It is sometime necessary during the lifetime of software to update the structs that are exchanged. Such updates consist usually in adding or removing fields from structures and possibly filling them with data from other fields.

Adding or dropping fields automatically

There are helper macros that allow saying that a field has been added or removed from a structure. If you just add or remove fields, when two versions of your program try to communicate, it will trigger errors.

Let’s say that you update the structure Cat and you want to drop the field hairQuality and are a new field power.

// your structure before
struct Cat
{
  std::string eyesColor;
  bool largeEars;
  float hairQuality;
};
QI_TYPE_STRUCT(Cat, eyesColor, largeEars, hairQuality)

// your structure after
struct Cat
{
  std::string eyesColor;
  bool largeEars;
  float power;
};
QI_TYPE_STRUCT(Cat, eyesColor, largeEars, power)

If an old version of your program tries to call a new version of your program over the network, there will be a call error when it tries to transfer the Cat structure because the signature doesn’t match.

libqi provides the macros to handle these cases. The firsts are QI_TYPE_STRUCT_EXTENSION_DROPPED_FIELDS and QI_TYPE_STRUCT_EXTENSION_ADDED_FIELDS and allow simply adding or dropping fields. They must be called in your new program, before the register macro, like this:

// you can specify multiple arguments to these two macros
QI_TYPE_STRUCT_EXTENSION_DROPPED_FIELDS(Cat, "hairQuality")
QI_TYPE_STRUCT_EXTENSION_ADDED_FIELDS(Cat, "power")
QI_TYPE_STRUCT(Cat, eyesColor, largeEars, power)

In this case, when an old version of the struct is sent where a new version is expected, libqi will allow the hairQuality field to be dropped and will value-initialize the power field (like T()).

This also allows sending structures the other way. When a new structure is sent to something that expects an old one, libqi will allow the power field to be dropped and will value-initialize the hairQuality field.

Adding or dropping fields manually

Sometimes you may need a more complex behavior, like filling new fields according to the values of the dropped fields, or dropping fields only when they have some special value. Let’s take the latter as an example.

Let’s have a color structure:

struct Color
{
  int r;
  int g;
  int b;
};
QI_TYPE_STRUCT(Color, r, g, b)

And we want to update that structure and add an alpha field:

struct Color
{
  int r;
  int g;
  int b;
  int a;
};
QI_TYPE_STRUCT(Color, r, g, b, a)

Now, we want that, when an old Color is sent, the a field is filled with the value 255 in the new Color we receive. In the other way, when sending a new Color, it can be converted to Color only if the value of a is 255. If the value is different from that, we are sending a color that is not supported by the old program and we want the conversion to fail.

To do that, it is possible to provide two handlers that will be called whenever a conversion is necessary. These handlers must then check that the conversion is possible and attempt it.

The signature of these handlers is the following:

bool handler(std::map<std::string, ::qi::AnyValue>& fields,
             const std::vector<std::tuple<std::string, qi::TypeInterface*>>& missing,
             const std::map<std::string, ::qi::AnyReference>& droppedFields);

When a conversion is attempted, the fields that haven’t changed are filled in fields. In our example, fields will contain r, g and b with the values from the previous structure.

When converting from the old structure to the new one, missing will contain a and its corresponding type because this is a field that does not exist and must be created. The handler must fill-in fields with a and give it a value. droppedFields will be empty because all fields in the old structure are present in the new one.

When converting from the new structure to the old one, droppedFields will contain a with a value and the handler must only accept the field to be dropped or not. missing will be empty because all the fields in the old structure are present in the new one.

There is a different handler for each one of these two conversions. In each case, the handler must return true or false if the conversion succeeded or not. If the handler returns false, it must not modify the fields argument.

In our example, we want to initialize a to 255 and accept to drop it only when its value is 255. Here is how we must implement our handlers:

// from new Color to old Color
bool colorTo(std::map<std::string, ::qi::AnyValue>& fields,
             const std::vector<std::tuple<std::string, qi::TypeInterface*>>& missing,
             const std::map<std::string, ::qi::AnyReference>& droppedFields)
{
  // if there are fields in the target structure that we don't have, we
  // can't convert
  if (!missing.empty())
    return false;
  // we don't accept dropping any other field than "a"
  if (droppedFields.size() != 1 || droppedFields.begin()->first != "a")
    return false;

  try
  {
    // and then, we drop "a" only if its value is 255
    return droppedFields.begin()->second.toInt() == 255;
  }
  catch (...)
  {
    // "a" was not an int, something went terribly wrong
    return false;
  }
}

// from old Color to new Color
bool colorFrom(std::map<std::string, ::qi::AnyValue>& fields,
               const std::vector<std::tuple<std::string, qi::TypeInterface*>>& missing,
               const std::map<std::string, ::qi::AnyReference>& droppedFields)
{
  // we don't accept any field to be dropped
  if (!droppedFields.empty())
    return false;
  // there can be only one missing field from the old Color, which is "a"
  if (missing.size() != 1 || std::get<0>(missing.front()) != "a")
    return false;
  // fill that field with our default value
  fields["a"] = qi::AnyValue::from(255);
  return true;
}

It is now possible to register the structure with these two handlers:

QI_TYPE_STRUCT_EXTENSION_CONVERT_HANDLERS(Color, colorFrom, colorTo);
QI_TYPE_STRUCT_REGISTER(Color, r, g, b, a);

Anytime a conversion from an old Color to a new Color is attempted, the convertFrom handler will be called and when converting from a new Color to an old Color, the colorTo handler will be called.

Enums

Enums are easy to register:

// color.hpp

namespace Graph {
  enum Color {
    Red,
    Green,
    Blue
  };
}

// call this outside of any namespace
QI_TYPE_ENUM_REGISTER(Graph::Color);

Classes

Using registration helper

Classes can only be registered in .cpp files:

// drawer.hpp

namespace Graph {
  class Drawer {
    public:
      bool draw(const Point& p, Color color) {
        std::cout << "Drawing point" << std::endl;
        drawDone(p);
        return true;
      }

      qi::Signal<Point> drawDone;

      qi::Property<Point> origin;
  };
}

// drawer.cpp

namespace Graph {
  // call this from inside the namespace of the class
  QI_REGISTER_OBJECT(Drawer, draw, drawDone, origin);
}

There are two threading models for classes. Drawer is registered as single threaded in the above example. When doing multiple calls of its methods in parallel, they will be sequenced. If you need your object to support multithreaded calls, use the MT macro:

// drawer.cpp

namespace Graph {
  // call this from inside the namespace of the class
  QI_REGISTER_MT_OBJECT(Drawer, draw, drawDone, origin);
}

Doing it manually

The helper won’t always allow you to register a class, for example when you have method overloading in your class. In these cases, you need to register your type manually with qi::ObjectTypeBuilder so that you can specify the signature of the function.

// drawer.hpp

namespace Graph {
  class Drawer {
    public:
      bool init() {
        std::cout << "Initializing drawer" << std::endl;
      }
      void draw(const Point& p, Color color) {
        std::cout << "Drawing point with color" << std::endl;
      }
      void draw(const Point& p) {
        std::cout << "Drawing point" << std::endl;
      }

      qi::Signal<void> objectDrawn;
      qi::Property<int> objectCount;
  };
}

// drawer.cpp

namespace Graph {
  // this won't work because we can't differentiate the two draw methods
  //QI_REGISTER_OBJECT(Drawer, draw, draw);
}

namespace Graph {
  static bool _qiregisterDrawer() {
    ::qi::ObjectTypeBuilder<Drawer> builder;
    // use static_cast to remove ambiguity between overloads
    builder.advertiseMethod("draw",
        static_cast<void (Drawer::*)(const Point&, Color)>(&Drawer::draw));
    builder.advertiseMethod("draw",
        static_cast<void (Drawer::*)(const Point&)>(&Drawer::draw));
    // no need to static_cast if there is no overload
    builder.advertiseMethod("init", &Drawer::init);
    builder.advertiseSignal("objectDrawn", &Drawer::objectDrawn);
    builder.advertiseProperty("objectCount", &Drawer::objectCount);
    builder.registerType();
    return true;
  }
  static bool __qi_registrationDrawer = _qiregisterDrawer();
}

More on Threading Model

If you need your object to be multithreaded, set it on your builder:

builder.setThreadingModel(qi::ObjectThreadingModel_MultiThread);

It is also possible to have a single-threaded class on which only some methods are multithreaded:

builder.advertiseMethod("multiThreadedMethod", &Drawer::multiThreadedMethod,
    qi::MetaCallType_Queued);

If you also want non-type-erased calls to be single-threaded (on async, signals, etc), you must inherit from Actor. See qi::Actor and qi::Strand.

Warning about registration order

It is important to know that all _REGISTER_ macros depend on static initialization (as you can see in Doing it manually) and they are thus vulnerable to the static initialization order fiasco.

If you have types that depend on each other, like a structure that contains a structure or a class that has a function that returns a structure, you must register them in the same file. Note that only macros containing _REGISTER_ in their name are vulnerable to that.

The common practice is to create a file named registration.cpp that contains all these macros in correct order. Cyclic dependency is not supported.