(Please feel free to edit/comment etc. This is a community document, not a personal document)
An alternative naming is possible: the offering of a C/C++ function could be named 'operation' and the collection of a given set of operations in an interface could be called a 'service'. This definition would line up better with service oriented architectures like OSGi.
Users want to control which thread executes which function, and if they want to wait(block) on the result or not. This all in order to meet deadlines in real-time systems. In practice, this boils down to:
Wait? \ Thread? | Caller | Component |
---|---|---|
Yes | (Method) | (?) |
No | X | (Command) |
For reference, the current RTT 1.x primitives are shown. There are two remarkable spots: the X and the (?).
Another thing you should be aware of that in the current implementation, caller and component must agree on how the service is invoked. If the Component defines a Method, the caller must execute it in its own thread and wait for the result. There's no other way for the caller to deviate from this. In practice, this means that the component's interface dictates how the caller can use its services. This is consistent with how UML defines operations, but other frameworks, like ICE, allow any function part of the interface to be called blocking or non-blocking. Clearly, ICE has some kind of thread-pool behind the scenes that does the dispatching and collects the results on behalf of the caller.
A simpler form of Command will be provided that does not contain the completion condition. It is too seldomly used.
It is to the proposals to show how to emulate the old behavior with the new primitives.
Each proposal should try to solve these issues:
The ability to let caller and component choose which execution semantics they want when calling or offering a service (or motivate why a certain choice is limited):
And regarding easy use and backwards compatibility:
And finally:
This is one of the earliest proposals. It proposes to keep Method as-is, remove Command and replace it with a new primitive: RTT::Message. The Message is a stripped Command. It has no completion condition and is send-and-forget. One can not track the status or retrieve arguments. It also uses a memory manager to allow to invoke the same Message object multiple times with different data.
Emulating a completion condition is done by defining the completion condition as a Method in the component interface and requiring that the sender of the Message checks that Method to evaluate progress. In scripting this becomes:
// Old: do comp.command("hello"); // waits (polls) here until complete returns true // New: Makes explicit what above line does: do comp.message("hello"); // proceeds immediately while ( comp.message_complete("hello") == false ) // polling do nothing;
In C++, the equivalent is slightly different:
// Old: if ( command("hello") ) { //... user specific logic that checks command.done() } // New: if ( message("hello") ) { // send and forget, returns immediately // user specifc logic that checks message_complete("hello") }
Users have indicated that they also wanted to be able to specify in C++:
message.wait("hello"); // send and block until executed.
It is not clear yet how the wait case can be implemented efficiently.
The user visible object names are:
This proposal solves:
This proposal omits:
Other notes:
The idea is that components only define services, and assign properties to these services. The main properties to toggle are 'executed in my thread or callers thread, or even another thread'. But other properties could be added too. For example: a 'serialized' property which causes the locking of a (recursive!) mutex during the execution of the service. The user of the service can not and does not need to know how these properties are set. He only sees a list of services in the interface.
It is the caller that chooses how to invoke a given service: waiting for the result ('call') or not ('send'). If he doesn't want to wait, he has the option to collect the results later ('collect'). The default is blocking ('call'). Note that this waiting or not is completely independent of how the service was defined by the component, the framework will choose a different 'execution' implementation depending on the combination of the properties of service and caller.
This means that this proposal allows to have all four quadrants of the table above. This proposal does not detail yet how to implement case (X) though, which requires a 3rd thread to do the actual execution of the service (neither component nor caller wish to do execute the C function).
This would result in the following scripting code on caller side:
//Old: do comp.the_method("hello"); //New: do comp.the_service.call("hello"); // equivalent to the_method. //Old: do comp.the_command("hello"); //New: do comp.the_service.send("hello"); // equivalent to the_command, but without completion condition.
This example shows two use cases for the same 'the_service' functionality. The first case emulates an RTT 1.x method. It is called and the caller waits until the function has been executed. You can not see here which thread effectively executes the call. Maybe it's 'comp's thread, in which case the caller's thread is blocking until it the function is executed. Maybe it's the caller's thread, in which case it is effectively executing the function. The caller doesn't care actually. The only thing that has effect is that it takes a certain amount of time to complete the call, *and* that if the call returns, the function has been effectively executed.
The second case is emulating an RTT 1.x command. The send returns immediately and there is no way in knowing when the function has been executed. The only guarantee you have is that the request arrived at the other side and bar crashes and infinite loops, will complete some time in the future.
A third example is shown below where another service is used with a 'send' which returns a result. The service takes two arguments: a string and a double. The double is the answer of the service, but is not yet available when the send is done. So the second argument is just ignored during the send. A handle 'h' is returned which identifies your send request. You can re-use this handle to collect the results. During collection, the first argument is now ignored, and the second argument is filled in with the result of the service. Collection may be blocking or not.
//New, with collecting results: var double ignored_result, result; set h = comp.other_service.send("hello", ignored_result); // some time later : comp.other_service.collect(h, "ignored", result); // blocking ! // or poll for it: if ( comp.other_service.collect_if_done( h, "ignored", result ) == true ) then { // use result... }
In C++ the above examples are written as:
//New calling: the_service.call("hello", result); // also allowed: the_service("hello", result); //New sending: the_service.send("hello", ignored_result); //New sending with collecting results: h = other_service.send("hello", ignored_result); // some time later: other_service.collect(h, "ignored", result); // blocking ! // or poll for it: if ( other_service.collect_if_done( h, "ignored", result ) == true ) { // use result... }
Completion condition emulation is done like in Proposal 1.
The definition of the service happens at the component's side. The component decides for each service if it is executed in his thread or the callers thread:
// by default creates a service executed by caller, equivalent to defining a RTT 1.x Method RTT::Service the_service("the_service", &foo_service ); // sets the service to be executed by the component's thread, equivalent to Command the_service.setExecutor( this ); //above in one line: RTT::Service the_service("the_service", &foo_service, this );
The user visible object names are:
This proposal solves:
This proposal omits: