A call in libqi employs a very twisted code path before arriving to its destination. This section will try to decompose that path and explain what each part does.
As explained in Object type erasure, you must advertise your methods so
that they can be used in type-erased contexts. When you advertise them, they are
wrapped in an AnyFunction
with AnyFunction::from
.
That from
function is defined in anyfunctionfactory.hxx and try to match
the type you give it (a member function pointer, a boost function, etc) with
AnyFunctionMaker
. This will create a type interface for your function which
implements FunctionTypeInterface
which is defined in anyfunction.hpp.
When instantiating the FunctionTypeInterfaceEq
, a mask is given to the
constructor. This mask tells if each argument must be transfered by value or by
pointer (see Pointer as pointer and pointer as value). Bit 0 is for return type,
other bits are for arguments.
transformRef
adds the indirection for each argument that need it. Then the
makeCall
functions, defined by the makeCall
macro call the function and
cast each argument to the correct type. It should be possible to rewrite this
code without using macros, only with meta programing.
The return value of the function is caught by AnyReferenceCopy
which allocates
a copy of the return value on the heap so that it can travel in a type-erased
manner. It overrides its operator,
as a trick to get the return value of the
function and not having a compilation failure when the function returns
void
.
As we’ll see below, the return value is returned as an AnyReference which must
be destroy()
-ed by the caller.
AnyObject
re-exposes the interfaced of GenericObject
through
GenericObjectBounce
, defined in object.hxx, from which it inherits.
GenericObject
has generic call
and async
methods defined in
genericobject.hpp.
These method catch all their arguments by AutoAnyReference
which is the same
as AnyReference
but with a templated constructor that calls
AnyReference::from
. call
and async
call metaCall
which is
another member function which adds a layer of type-erasure. They specify if the
call must be synchronous or not and give the method name and the parameters. The
signature of the return type is given for the compatibility layer that libqi
provides for structures described in Struct extension.
From this point on, methods return a Future<AnyReference>
which points to
the return value of the called method. It is the responsibility of the caller to
free it.
This first overload of metaCall
(called by call
and async
) takes a
nameWithOptionalSignature
. It will use the MetaObject::findMethod
method,
giving it that name and the arguments of the call, to resolve which overload
must be called. findMethod
then returns an integer which is the id of the
method or -1
if it couldn’t be found. In the latter case, it generates an
error string with a meaningful error message. On success, it calls the other
overload of metaCall
which takes an integer and not a name. This last method
just forwards the call to ObjectTypeInterface::metaCall
.
The returned Future<AnyReference>
may actually be a Future<Future<T>>
,
if the method returns a future and the call is made in async. As explained in
About futures, a method can return a value or a future in
a transparent way to the caller, so the nested future must be extracted. This is
done through extractFuture
and adaptFutureUnwrap
, defined in
futureadapter.hxx depending on the type of the call.
The metaCall
method has the following signature:
qi::Future<AnyReference> metaCall(
void* instance,
AnyObject context,
unsigned int method,
const GenericFunctionParameters& params,
MetaCallType callType = MetaCallType_Auto,
Signature returnSig = Signature());
The first argument is the storage, as described in Type system.
The second argument is the AnyObject
the call was made in. It is used to run
the call in a specific ExecutionContext
, associated to the AnyObject
and to
add stats and tracing to the AnyObject
. It receives also the id of the method
to be called, the arguments of the call in the form close to a
vector<AnyReference>
which must not be freed. The callType
argument
specifies if the user wanted the call to be synchronous or asynchronous. It is
used only as a hint of what the user wants and it depends on the implementation
of metaCall
if the call is synchronous or not. For example, if the object
has a strand, the call can not be synchronous. The last argument is the
signature of the expected return value. It is used for structure compatibility.
One possible implementation is in staticobjecttype.cpp. This method checks that the method exists and that it will be able to convert the returned value to the expected value. If it can’t, it doesn’t do the call and just return an error.
The other implementation is very similar and is in dynamicobject.cpp.
DynamicObjectInterface::metaCall
actually calls DynamicObject::metaCall
which does almost the same thing as StaticObjectTypeBase::metaCall
.
After that, it gets the ExecutionContext
on which to run the call. If the id
of the method is small, it means the method belongs to Manageable
which we’ll
talk about later. Otherwise, the method belongs to the object.
In our case, the metaCall
method prepends the this
argument and calls
the method qi::metaCall
, which is the generic method which will execute the
final call. It gives it the method to call as an AnyFunction
.
This method is defined in anyobject.cpp. It first decides if the call must
be synchronous or not. If the call is direct, it bounces to the call
method
directly. If the call is asynchronous, it posts a functor on the
ExecutionContext
which will call call
when it’s called.
call
will handle tracing and stats if they are enabled and trigger the
appropriate signals. It will do the call itself through the AnyFunction
and
set the promise to the returned AnyReference
.
Objects that reside in other processes through qimessaging are exposed through
RemoteObject
which inherits from DynamicObject
and implements its own
metaCall
. It sends a message and returns a future that will be set when the
call reply is received.