Type system

libqi does type erasure in similar way to Python. A type erased value is composed of a void* and a TypeInterface* (see AnyReferenceBase). The TypeInterface class is abstract and has a kind() method which returns an enum value.

To each kind corresponds a TypeInterface, like ListTypeInterface, ObjectTypeInterface, etc. A value can’t be of multiple kinds. The list of all kinds is in fwd.hpp and their mapping in signatures is in signature.hpp.

This system allows a value to hold anything, for example a PyObject representing an integer. This PyObject will not be converted to a C++ integer until it is needed. Note that it may be never needed if the value is passed back to Python code.

Type interfaces usually exist as unique instances within the program. This is not enforced though and it may happen that a type interface exist multiple times within a single program.

Type interfaces should be stateless, this is not enforced though and their functions are not marked const.

About kinds

A value of type Unknown cannot be used in a type-erased context. Its only use is when you use an unregistered type to do a type-erased call, if you give a type T and your function takes exactly a type T (the std::type_info must be equal), then the type system uses it as an Unknown value, but the call succeeds because the value is not transformed. You have the same behavior if you do qi::AnyValue::from(someT) and then yourvalue.as<T>().

A value of type Pointer is a typed pointer, it can be a raw pointer or a shared pointer. If it is a raw pointer, libqi does nothing to manage its lifetime, it’s up to the user to handle that.

A value of type Dynamic is an untyped pointer. There is no such thing in C++, but you can see it as boost::any. It is used to type-erase PyObject.

You can look at the interfaces of PointerTypeInterface and DynamicTypeInterface in typeinterface.hpp. PointerTypeInterface returns a static type that does not depends on the value (the pointedType function does not take the void* as argument), but the DynamicTypeInterface can return types different for each value through the get method.

A value of type Tuple, as the name suggests, is a list of values of different, fixed, types. Same as std::tuple in C++. C++ structures are represented as annotated tuples (the annotations correspond to the name of the structure and the name of its fields).

A value of type Raw is a raw buffer, conceptually like a std::vector<char> or a std::pair<char*, size_t>. qi::Buffer is implemented as Raw. This type is different from std::vector<char> which is a List of Int8. A List is something that gets introspected and that can be heterogeneous, a Raw is just a buffer of untyped data.

A value of type VarArgs is a variadic argument. Such object has single type, and any number of objects of that type. You can see it as Java’s variadic function which look like void f(String... strArray). If you need different types, you can use a VarArgs of Dynamic.

Relation between types and signatures

A type has a signature which is based on its kind and on some static information (functions you can call on the TypeInterface without a void* pointer). For example, a value that contains a string would have a kind String and a signature "s".

This is not based uniquely on the kind. For example, the signatures "i" and "I" both represent a kind Integer but one is signed and the other is unsigned. There exist also different signatures for different integer sizes.

Tuples are represented by parenthesis, for example "(sis)" is a tuple of String, Integer and String.

Lists are represented by square brackets, for example "[f]" is a list of Float. It is invalid to put more than one type inside the brackets.

Value’s states

A value can be invalid (for example when it is not initialized). An invalid value must never be used. Even though it looks like a Void value, it is not, and lot of functions will throw (or crash?) with an invalid value.

A value can be of Void kind. This corresponds to “no-value”. It has no equivalent in C++ but it works like Python’s None. You can instantiate such a value in C++ though:

qi::AnyValue value(qi::typeOf<void>());

Implementing type interfaces

Type interfaces must implement functions that receive a void* and must do the action on it. For example IntTypeInterface::get has the following interface and possible implementation:

int64_t Int64TypeInterface::get(void* storage)
{
  return *(int64_t*)ptrFromStorage(&storage);
}

These basic types for c++ are registered in the file registration.cpp. Note that the function may not be that trivial, for example for Python:

int64_t PyLongTypeInterface::get(void* storage)
{
  return PyLong_GetLong((PyObject*)ptrFromStorage(&storage));
}

This last function is implemented in pyobjectconvert.cpp.

Consider that ptrFromStorage(&storage) returns storage. For more information about this function, read the Pointer as pointer and pointer as value section below.

What functions a type interface must implement

A basic type interface must implement some basic functions. You can see the pure virtual functions in TypeInterface in detail/typeinterface.hpp.

These functions include default construction, copying, destruction and a less-than comparison operator. If default construction or copy is not possible, these functions are allowed to throw. In general, any function is allowed to throw if the operation is impossible. For example, lots of set throw in our Python type interfaces because Python values are usually immutable.

The less function is used to compare objects when they are stored in a container that need such a function to sort the items.

These functions are usually implemented through default implementations. Most (all?) type interfaces declare an alias to a class with default implementation and use a macro to bounce functions to it. For example:

using Methods = qi::DefaultTypeImplMethods<YourType>;

_QI_BOUNCE_TYPE_METHODS(Methods);

There are macros that bounce only some methods so that you can implement the others yourself.

The DefaultTypeImplMethods class lies in typeimpl.hxx.

Pointer as pointer and pointer as value

It is possible to apply an optimization so that when the type is small enough, instead of storing a pointer to it in the void* storage, you store the value itself. This is commonly called small buffer optimization. For example, you would have a value with an IntTypeInterface and a void* that would not be a int* pointing to a value of 42, but a void* that holds the value 42 itself (and must not be dereferenced of course).

This optimization is actually not enabled for integers and is usually not a good idea. This means that the following code would not have the expected behavior:

int i = 42;
auto ref = qi::AnyReference::from(i);
ref.setInt(10);
assert(i == 10);

You can see the code used to implement this optimization commented out in type.hxx, in the class TypeImplMethodsBySize. This class is actually only used for the C++ IntTypeInterface in inttypeinterface.hxx.

It is enabled though on all pointer types. It seems safe enough and this optimization relied upon in function type erasure, implemented in anyfunctionfactory.hxx.

Anyway, the behavior of your type interface depends on the behavior of the ptrFromStorage function. This function must return a void*. The only two possible implementation (that make sense) are the following:

void* ptrFromStorage(void** storage)
{
  return *storage; // use value as value
  // OR
  return storage; // use pointer as value
}

As said before, any function accessing the void* storage or void** storage must call this function. This is also why setter functions receive a void** instead of a void*, it allows the setter to change the pointer itself.

You can have a default implementation for this method too through the DefaultTypeImplMethods class. You configure its behavior through the second template argument you give it, which defaults to TypeByPointer<T> and is the expected behavior. If you want to use the other behavior, use TypeByValue<T>.

Construction, destruction, cloning

It is possible to change the behavior of the ptrFromStorage method, with the default construction method, cloning method, etc, by changing the second argument you give to TypeByPointer, for types that are non-copyable or non-default constructible. You can specify that for TypeByValue, the type will have default construction and default copy.

The second argument to TypeByPointer is a TypeManager which has an implementation for these functions. This TypeManager can be specialized and has a default implementation which allows the user to specify what is possible to do with their types thanks to some macros.

By default, the TypeManager makes default-constructible and copyable types for PODs, and non-default-constructible and non-copyable types for the rest, as can be seen in typeimpl.hxx.

It is possible to use the macros declared in typeinterface.hpp to specialize TypeManager to declare that a type (that may or may not be known to the type system, see Unknown kinds above) is or is not default-constructible or copyable.

If an attempt is made to default-construct a type that is non-default-constructible or to copy a type that is non-copyable, the corresponding function will just throw.

Finally, there is the TypeByPointerPOD access which forces default-constructibility and copyability.

How types are retrieved

You will reach a moment when the type system will receive a C++ object and will need to know its TypeInterface. This happens for example:

  • when a struct is registered, the type system needs to know the type interfaces of the underlying fields,
  • when an object’s method is registered, libqi needs the type interface of the parameters and return value
  • when you use qi::typeOf<T>()
  • when you use qi::AnyValue::from, which uses qi::TypeOf<T>

It will first search through a static map to find if the type has been registered at runtime. This part is done in typeinterface.cpp in the function getType.

If the type is unknown to the type system yet, it will instantiate TypeImpl<T> and use that. This part happens in type.hxx, in the function typeOfBackend.

How types are registered

This section is tightly tied to the previous one, be sure to read it first.

To register something in the global map, you usually use the function qi::registerType in typeinterface.cpp.

The other way to “register” a type is to specialize the qi::TypeImpl class. This may be useful to register full classes of types with C++ partial specialization. For example:

template <typename T>
class StdVectorTypeInterface : public ListTypeInterface
{
  // ...
};

template <typename T>
class TypeImpl<std::vector<T>> : public StdVectorTypeInterface
{};

The type interface of std::vector is implemented in listtypeinterface.hxx.

If you need to register a template class, this is the only way to make it work for any type T.

The default implementation of TypeImpl<T> is a type interface of Unknown kind which uses DefaultTypeImplMethods with default values to implement its methods. This means that it is configurable through the macros described in Construction, destruction, cloning.

About type_info comparison

To know if two types are equal, the type system uses simple type_info comparison. This feature is very weakly defined by C++, especially when shared libraries are used.

It is very important that the symbols of the type interfaces are exported in shared libraries so that the type_info themselves are exported and the runtime library can identify that two same types from different binaries are in fact the same.

Mac’s compiler has a very strict policy about that, so there is a hack where type_info are compared by mangled string representation instead of by value under Apple systems. This hack is found in typeinterface.cpp.

If someday, libqi stops using plain std::type_info and uses Boost.TypeIndex, this by-string-comparison behavior will be enforced on all platforms.

AnyValue and AnyReference

AnyValue has value semantic and AnyReference has pointer semantic. AnyReference never frees its value (unless destroy() is explicitly called). AnyValue usually owns its value and frees it automatically at destruction. It doesn’t own it sometimes, depending on the arguments given at its construction.

Users should always use AnyValue, AnyReference is usually for internal use.

Be careful when using AnyReference, do not create reference to temporaries. Your compiler won’t be able to detect it, you will not get a warning.

int i;
auto ref = qi::AnyReference::from(i); // good

ref = qi::AnyReference::from(boost::python::object(i)); // BAD
auto value = ref.toInt(); // undefined behavior