Call code path

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.

Method advertising

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.

Calling a method on an AnyObject/GenericObject

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.

Object type interface implementation

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.

qi::metaCall

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.

Remote objects

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.