API to create remote procedure call services and to call them.
RPC mechanism is client-server-like mechanism based on interfaces. First, the interface must exist. This interface defines a range of operations that can be performed on some object. Next, any process within CMRX is free to provide an implementation of this interface. Then, it can define as many objects which support this implementation as it wants.
Any other process (even the same) - client can then call the implemented operations on the object as long as it knows the address of the object in the memory. For the client, the internal structure and content of the object are opaque and inaccessible. It can only call methods this object support in the implementation. The validity of call is checked at compile time. The validity of pointers is checked at runtime.
RPC interfaces are defined in a form of type definition of structures containing pointers to functions. The sole purpose of interfaces is to declare a set of operations that are available for certain types of objects. Interfaces never provide any implementation, not even default one. Interfaces shall be as abstract as the topic of the interface possibly allows. The interface may consist of any amount of functions that have following limitations:
Each process willing to provide RPC calls - the server - has to provide an implementation of these interfaces. The implementation of interface is performed by defining functions having prototypes equivalent to prototypes of vtable member functions. Unlike interfaces, where the pointer-to-object argument has no specific type (as the interface is abstract), with implementations it is necessary to type this pointer correctly. This is second responsibility of the INSTANCE() macro. During the implementation phase, it typecasts the this
pointer to correct type, so strong type safety can be achieved.
Any amount of processes within one firmware can implement one specific interface. There is no limitation on this. One process can even provide multiple implementations of the same interface, for example in case that it manages multiple objects of different type, which all provide the same manipulation semantics.
In order to publish an implementation of some interface, a process must create a variable using desired interface's vtable structure. This variable will be initialized to contain pointers to functions that implement the interface. This variable must be defined within one of process' modules and must be prefixed by the VTABLE keyword otherwise the kernel will refuse calls using such implementation for security reasons. This variable is known as vtable instance.
After the interface has been implemented for certain process, it is possible to create structures which can be manipulated using the given interface. These services store the actual data for the implementation of the interface. Object structure is thus bound with the interface implementation. The only limitation given to services is that they have to contain a pointer to vtable instance, which is the first member of the structure holding service state. This will be checked at compile time and build will fail if service designer fails to stick to this rule.
Where RPC service is a type which provides data storage to RPC interface implementation, the service instance is actual existing instance of this service. Service can't be used until instantiated. This instance can be allocated and initialized statically or dynamically. There are no limitations on where the instance resides other than limitations imposed by the memory protection mechanism. An instance of service translates to variable having the type of RPC service.
Macros | |
#define | IMPLEMENTATION(service) CMRX_IMPLEMENTATION_HELPER(service, CMRX__INTERFACE__COUNTER) |
Begin implementation of interface for service. | |
#define | IMPLEMENTATION_OF(service, interface) |
Extended version of IMPLEMENTATION macro. | |
#define | INSTANCE(a) void * a ## _ |
Mark function argument as reference to current service instance. | |
#define | this CMRX_THIS_HELPER(CMRX__INTERFACE__COUNTER) |
Access current service instance. | |
__SYSCALL int | _rpc_call (unsigned arg0, unsigned arg1, unsigned arg2, unsigned arg3, void *service, unsigned method, unsigned canary) |
Internal implementation of remote procedure call in userspace. | |
__SYSCALL void | rpc_return () |
The way how RPC returns. | |
#define | rpc_call(service_instance, method_name, ...) CMRX_RPC_CALL(service_instance, method_name __VA_OPT__(,) __VA_ARGS__) |
User-visible way to perform remote procedure call. | |
#define IMPLEMENTATION | ( | service | ) | CMRX_IMPLEMENTATION_HELPER(service, CMRX__INTERFACE__COUNTER) |
Begin implementation of interface for service.
Use of this macro starts implementation of methods for certain service. It enables use of the this
term to refer to the service instance current during the method call.
service | name of type which describes the service whose methods are being implemented |
#define IMPLEMENTATION_OF | ( | service, | |
interface | |||
) |
Extended version of IMPLEMENTATION macro.
Use of this macro starts implementation of methods for certain service. It enables use of the this
term to refer to the service instance current during the method call. Another function of this macro is that it generates a static assert to check, if the type of interface implemented by the service matches the one, which is expected here
service | type of structure, which holds instances of service |
interface | type describing virtual method table of the implemented interface |
#define INSTANCE | ( | a | ) | void * a ## _ |
Mark function argument as reference to current service instance.
Syntactic sugar to make argument type-compatible with interface declaration. This allows to work with any specific type of service inside its implementation and use of such specialized implementations to initialize generic interface.
this
#define rpc_call | ( | service_instance, | |
method_name, | |||
... | |||
) | CMRX_RPC_CALL(service_instance, method_name __VA_OPT__(,) __VA_ARGS__) |
User-visible way to perform remote procedure call.
Calls service published by different thread and/or process. Service can take 0 to 4 arguments, which are passed down to it. Return value of service is passed back to the caller. Services are represented by structures allocated in service provider thread. These structures have to contain pointer to their virtual method table as their first member. Virtual method table is then queried for presence of method name used to perform the call. If such method exists, then arguments used to perform the call are checked against method prototype.
service_instance | address of service instance, which is being called |
method_name | name of method within service, which has to be called |
#define this CMRX_THIS_HELPER(CMRX__INTERFACE__COUNTER) |
Access current service instance.
this
provides access to the current service instance. It is always typed correctly to access members of service currently being implemented.
__SYSCALL int _rpc_call | ( | unsigned | arg0, |
unsigned | arg1, | ||
unsigned | arg2, | ||
unsigned | artg3, | ||
void * | service, | ||
unsigned | method, | ||
unsigned | canary | ||
) |
Internal implementation of remote procedure call in userspace.
This function is actually called when user calles rpc_call(). The only difference is, that this signature takes method number instead it's name. Macro rpc_call will figure this out automatically.
service | address of service instance |
method | offset of method in VMT of service |
__SYSCALL void rpc_return | ( | ) |
The way how RPC returns.
Used automatically.
Kernel uses this to return from RPC. It is hooked into RPC call chain automatically, no need to call it manually from RPC method. It is sufficient to return from RPC to call this.