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