Object type erasure

On this page

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 .