Object type erasure¶
One kind of type is Object, which is complex enough to have its own section.
An object is something that has shared semantics. It can contain methods, signals and properties. It has no data exposed through type erasure (but you can expose accessor methods if you want).
The “signature” of an object is a MetaObject
, it is unique for all instances
of a same object. The MetaObject
contains a list of MetaMethod
,
MetaSignal
and MetaProperty
. These three classes contain all the
information required to characterize their respective entity.
Each method, signal and property has an id, which is fixed and stored in the meta object. Overloading is only supported for methods, over their arguments, as in C++.
Though it may be possible to change the meta object of an object after it has been created, it is not a good idea. This feature has not been well tested and is known not to work on the messaging layer because it caches the meta object to avoid sending them every time.
The MetaObject
is returned by the ObjectTypeInterface
. The method receives
a void*
, so different instances of the same type may have different meta
objects (this feature is used by the DynamicObjectTypeInterface
, discussed
below).
Typing¶
There are two classes that inherit from ObjectTypeInterface
,
StaticObjectTypeBase
(which is not a base, this name is misleading) and
DynamicObjectTypeInterface
. It is of course possible to create your own
implementation by inheriting from ObjectTypeInterface
, but these two classes
seem to handle all the cases already.
The names may be misleading, but StaticObjectTypeBase
is for C++ objects and
DynamicObjectInterface
is for all other objects (from other languages, but
also objects over the messaging layer). C++ is the only statically typed
language that we support for the moment, but I think that if we are to support
others, they must be implemented through DynamicObjectTypeInterface
or have
their own class.
With StaticObjectTypeBase
, the meta object is contained in the type, but with
DynamicObjectTypeBase
, there is only one instance of that class per process
and the meta objects are contained in the storage.
DynamicObjectTypeInterface¶
DynamicObjectTypeInterface
is never really used, the class is in the private
file dynamicobject.cpp, and the only public access you have to it is
qi::getDynamicTypeInterface
which returns a singleton of that class. The
storage corresponding to that class is a DynamicObject
which contains all the
information needed for the type to work. That class is defined in
dynamicobject.hpp.
It is usually not necessary to deal with DynamicObject
either actually. It is
possible to use a builder to create a dynamic object. Such a builder create an
abstraction that simplifies the task in C++.
The corresponding builder for dynamic objects is DynamicObjectBuilder
,
defined in dynamicobjectbuilder.hpp. It does not expose type interfaces,
nor dynamic objects, but directly an AnyObject
that you can manipulate. There
is only one AnyObject
per DynamicObjectBuilder
, the object
method
always return the same instance.
DynamicObjectBuilder
allows you to create an object with C++ functions,
signals and properties easily. There is no this
instance though, it is the
user’s task to keep track of it. Here’s an example usage:
struct Data {
int val;
};
auto self = std::make_shared<Data>();
DynamicObjectBuilder builder;
// the builder can't deduce the signature of a lambda (thanks C++), so we
// need an explicit cast
// builders recognize anything AnyFunction recognizes
builder.advertiseMethod("get", boost::function<int()>([self](){
return self->val;
}));
builder.advertiseMethod("add", boost::function<void(int)>([self](int x){
self->val += x;
}));
auto object = builder.object(); // can specify a destructor here
It is still a bit cumbersome. Dynamic objects like that are only used in tests. You can find a real example of dynamic object usage in the Python bindings in pyobject.cpp.
StaticObjectTypeBase¶
Similarly to DynamicObjectTypeInterface
, you don’t need to deal with
StaticObjectTypeBase
directly. For that too, you use a builder.
The corresponding builder is ObjectTypeBuilder
(yes, the name is not very
explicit), defined in objecttypebuilder.hpp. Its interface is similar to
that of DynamicObjectBuilder
, but has some more features.
First, it is templated on the class you want to register. This allows you to advertise the method pointers of the class directly.
It also has an inherits
method that declares that your type inherits from
another. This allows casting to parent types in type-erased contexts.
Warning
Virtual inheritance is not supported and this method will crash if used in that case.
This method relies on undefined behavior in C++, it was realized by professionals, do not try this at home.
You can see the definition of inherits
in objecttypebuilder.hxx. It
tries to create a pointer to a random address in memory, casts it, and
saves the difference, without ever dereferencing it.
The problem with virtual inheritance is that the shift between the base class and the derived class is not fixed and is thus saved somewhere in the derived object’s memory. This means that this cast actually dereferences that invalid pointer and tries to read the address of the base class there.
We couldn’t find how to overcome this limitation in pure C++.
In the end, the registerType
function must be called. It will register the
type in the global map so that every time you use an object of type T, the
correct type interface will be retrieved.
Usage¶
Objects are usually handled by AnyObject
, which contains a shared pointer to
a GenericObject
.
GenericObject¶
At the lowest level, the object is type-erased as a GenericObject
, defined in
genericobject.hpp. This class contains a void*
and an
ObjectTypeInterface*
. It does not own the object. It provides all the
functions you need to call methods, trigger signals, connect to them and access
properties.
It inherits from Manageable
which adds some hidden methods for tracing and
statistics. These methods are also exposed through type erasure.
AnyObject¶
AnyObject
wraps a GenericObject
. It is defined in object.hxx. It owns
the GenericObject
through a shared pointer and this shared pointer owns the
real object thanks to a custom destructor (or shared pointer refcount sharing).
It re-exposes all the methods of GenericObject
through GenericObjectBounce
.
Object¶
AnyObject
is actually not a class but only an alias to Object<qi::Empty>
.
Object
is defined in object.hxx. It has an operator->()
that returns
the T
it has been specialized to (after a runtime check in the function
checkT
which can throw).
This allows you to write code like:
qi::Object<MyClass> obj(new MyClass());
obj->directCall();
Assuming MyClass
is a type registered in the type system.
About futures¶
Futures are not part of the function signature. A function returning a type
T
has the same signature as a function returning a Future<T>
in the
type system.
This implies some complex code to extract the potential future the method
returns and unwrapping it. The code to do that is in GenericObject::call
and
GenericObject::async
, in genericobject.hpp. This task is accomplished by
extractFuture
and adaptFutureUnwrap
.
There is one more trick to that: futures are registered in the type system, as Objects. This case is explained in Registering template types.