Server code is code that provides components in the form described by those components' interfaces, employing interprocess communications mechanisms to expose those components to clients.
The following example interfaces will be used to illustrate the mechanisms described in this document. Firstly, in calc.idl:
interface Calc { void add(in int left, in int right, out int result); void subtract(in int left, in int right, out int result); void multiply(in int left, in int right, out int result); void divide(in int numerator, in int denominator, out int result); };
Secondly, in counter.idl:
interface Counter { void increment(out int result); };
Finally, in calc_counter.idl:
import "calc.idl"; import "counter.idl"; interface MappedFileObject composes Dataspace, File, Flush, MappedFile, Notification;
To expose these interfaces to other programs, a server program would do the following in each case:
The sections below describe how this is done in the different supported programming languages.
Note that the mechanism by which interface descriptions are processed for use in programs is described in the L4Re Support document. The idl manual page provides details for developers wishing to use the tool directly.
Given the existence of generated files for the interface, a program would include the server header file:
#include "calc_server.h"
To expose components as servers, a libipc header file is also needed:
#include <ipc/server.h>
A server exposing the Calc interface will employ an object having the Calc type. This encapsulates a reference to the object state and a reference to the interface details.
Since the aim in implementing a server is to provide access to state information held within a process, the object state reference will refer to a location holding such information via a pointer member. The following could be used for the state supporting the Calc interface.
ref_Calc ref = {.ptr=0};
Here, a value of 0 is used because the interface operations are stateless (they act like plain functions), and so it does not really matter what ptr is set to.
The Calc object is initialised using the chosen reference value and a reference to interface information:
Calc obj = {.ref=ref, .iface=&server_iface_Calc};
Unlike in the client initialisation where a predefined interface already exists, it is up to the component implementer to define the functions that support the exposed interface. For example:
iface_Calc server_iface_Calc = { .add=calc_add, .subtract=calc_subtract, .multiply=calc_multiply, .divide=calc_divide, .pow=calc_pow };
The Calc object is then associated with a server capability and invoked when incoming messages are directed towards it.
Where operations are not stateless and instead operate on some kind of state or object, the reference is initialised as in this example involving the Counter interface:
int counter = 0; ref_Counter ref = {.ptr=&counter};
Here, the state modified by the Counter operations is a simple integer. As long as the operations are written to interpret the state correctly, it does not matter what kind of object provides the state. For C language servers, there is not necessarily a requirement to have a dedicated data structure containing the state.
When supporting multiple interfaces, the general initialisation resembles that shown above. Firstly, the object state reference is defined, as in this example involving a reference suitable for a CalcCounter object:
ref_CalcCounter ref = {.ptr=0, .as_Counter={.ptr=&counter}};
Here, the pointer member is given as 0 because there is no dedicated state for the CalcCounter compound interface. However, when the component is to be interpreted as a Counter, the as_Counter member provides the corresponding reference, this indicating the address of a counter as the pointer to the appropriate state, as previously suggested.
The object employed by the server is itself populated in the same way as shown above:
CalcCounter obj = {.ref=ref, .iface=&server_iface_CalcCounter};
The principal difference between simple and compound interface definition involves the interface details. With a compound interface, there must be a way to address the individual interfaces, and this is done using members employing a specific naming convention:
iface_CalcCounter server_iface_CalcCounter = { .to_Calc=&server_iface_Calc, .to_Counter=&server_iface_Counter };
Here, the to_Calc member provides a reference to suitable details for the Calc interface, and the to_Counter member provides the corresponding reference for the Counter interface.
Implementations of operations employ a signature where the first parameter is the object state reference value. This permits access to the state information and allows functions to update the state of several objects independently:
long counter_increment(ref_Counter _self, int *result) { int *counter = (int *) (_self.ptr); *counter = *counter + 1; *result = *counter; return L4_EOK; }
Here, the pointer member is used to access the information referenced when the object was initialised. As noted above, the interpretation of the pointer member is left to the operations, with the chosen interpretation being a simple integer in this example.
An object representing the component will have the Calc type. An object providing a specific implementation of the component might have a type called server_Calc and be initialised as follows:
server_Calc obj;
Such an object type would need to be a subtype of Calc and would resemble the following:
class server_Calc : public Calc { public: long add(int left, int right, int *result); long subtract(int left, int right, int *result); long multiply(int left, int right, int *result); long divide(int numerator, int denominator, int *result); };
The server_Calc object is then associated with a server capability and invoked when incoming messages are directed towards it.
When supporting multiple interfaces, the general initialisation resembles that shown above, as in this example involving a specific object type derived from the CalcCounter type:
server_CalcCounter obj;
Similarly, a definition of this type is required supporting the range of operations associated with all of the individual interfaces:
class server_CalcCounter : public CalcCounter { int counter = 0; public: long add(int left, int right, int *result); long subtract(int left, int right, int *result); long multiply(int left, int right, int *result); long divide(int numerator, int denominator, int *result); long increment(int *result); };
Since C++ manages access to object state transparently, the way of accessing such state is more straightforward:
long server_CalcCounter::increment(int *result) { counter = counter + 1; *result = counter; return L4_EOK; }
In L4Re, component objects can be made available to other programs by associating them with interprocess communication (IPC) "gate" capabilities and then waiting for incoming messages.
Given an existing capability accessible via the program environment, a component can be exposed to other programs as follows:
ipc_server_loop_for(Calc, &obj, "server");
This convenience function (or, more accurately, macro) binds the main thread to the named capability. This also handles the details of waiting for messages and directing requests to the component, with the following parameters being specified:
This macro invocation expands to a call to _ipc_server_loop_for with the following parameters:
Here, the macro casts the object address to the appropriate type. In simple cases, this is a superfluous operation since the object type will be directly compatible with the interface type used to expose the object, and such a casting operation will not change the address. However, in more complicated cases such as where an object type inherits from the interface type and one or more other types, such a casting operation may change the address.
It is important to note that the IPC handling mechanism treats the object address as an opaque value, presenting it to the handler function for interpretation as a pointer to an object of the interface type. Where the object type is directly compatible (such that casting to the interface type does not change the address), this reinterpretation of the address will be correct. Where the type is not directly compatible, however, (such that casting to the interface type does change the address) the reinterpretation is incorrect: subsequent accesses via the pointer will be erroneous.
Consequently, it is essential to perform casting to the interface type before the address is presented to the IPC handling mechanism. The macro is provided to ensure that this step is not overlooked.
The pkg/idl4re-examples directory contains some example packages for L4Re that feature some of the above code and demonstrate the techniques involved. The idl4re-examples directory can be copied into the L4Re distribution's pkg directory and built as follows:
make O=mybuild S=pkg/idl4re-examples
The mybuild directory name should be adjusted to match your own choice of build output directory.