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 add 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::Strand and qi::Actor.
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.