A simple client-server scenario Björn Döbel and Norman Feske Abstract ######## This tutorial will give you a step-by-step introduction for creating your first little client-server application scenario using the Genode OS Framework. We will create a server that provides two functions to its clients and a client that uses these functions. The code samples in this section are not necessarily complete. You can can find the complete source code at the _repos/hello_tutorial_ directory within Genode's source tree. Prerequisites ############# We assume that you have acquainted yourself with the basic concepts of Genode and have read the "Getting started" section of the Genode Foundations book. Our can download the book from [http://genode.org]. Setting up the build environment ################################ The Genode build system enables developers to create software in different repositories that don't need to interfere with the rest of the Genode tree. We will do this for our example now. In the Genode root directory, we create the following subdirectory structure: ! hello_tutorial ! hello_tutorial/include ! hello_tutorial/include/hello_session ! hello_tutorial/src ! hello_tutorial/src/hello ! hello_tutorial/src/hello/server ! hello_tutorial/src/hello/client In the remaining document when referring to non-absolute directories, these are local to _hello_tutorial_. Now we tell the Genode build system that there is a new repository. Therefore we add the path to our new repository to _build/etc/build.conf_: ! REPOSITORIES += /path/to/your/hello_tutorial Later we will place build description files into the tutorial subdirectories so that the build system can figure out what is needed to build your custom components. You can then build these components from the _build_ directory using one of the following commands: ! make hello ! make hello/server ! make hello/client The first command builds both the client and the server whereas the latter two commands build only the specific target respectively. Defining an interface ##################### In our example, we are going to implement a server providing two functions: :'void say_hello()': makes the server print a message, and :'int add(int a, int b)': adds two integers and returns the result. The interface of a Genode service is called a _session_. We will define it as a C++ class in 'include/hello_session/hello_session.h' !#include !#include ! !namespace Hello { struct Session; } ! !struct Hello::Session : Genode::Session !{ ! static const char *service_name() { return "Hello"; } ! ! enum { CAP_QUOTA = 2 }; ! ! virtual void say_hello() = 0; ! virtual int add(int a, int b) = 0; ! ! GENODE_RPC(Rpc_say_hello, void, say_hello); ! GENODE_RPC(Rpc_add, int, add, int, int); ! GENODE_RPC_INTERFACE(Rpc_say_hello, Rpc_add); !}; As a good practice, we place the Hello service into a dedicated namespace. The _Hello::Session_ class defines the public interface for our service as well as the meta information that Genode needs to perform remote procedure calls (RPC) across component boundaries. Furthermore, we use the interface to specify the name of the service by providing the 'service_name' method. This method will later be used by both the server for announcing the service at its parent and the client for requesting the creation of a "Hello" session. The 'CAP_QUOTA' definition specifies the amount of capabilities required to establish the session. The specified amount is transferred from the client to the server at session creation time. For the "Hello" session, two capabilities are required, namely a dataspace capability for the server-side memory occupied by the session object and the actual session capability that refers to the RPC interface. The 'GENODE_RPC' macro is used to declare an RPC function. Its first argument is a type name that is used to refer to the RPC function. The type name can be chosen freely. However, it is a good practice to prefix the type name with 'Rpc_'. The remaining arguments are the return type of the RPC function, the server-side name of the RPC implementation, and the function arguments. The 'GENODE_RPC_INTERFACE' macro declares the list of RPC functions that the RPC interface is comprised of. Under the hood, the 'GENODE_RPC*' macros enrich the compound class with the type information used to automatically generate the RPC communication code at compile time. They do not add any members to the 'Session' struct. Writing server code ################### Now let's write a server providing the interface defined by _Hello::Session_. We will put all of this code in 'src/hello/server/main.cc' Implementing the server side ============================ We place the implementation of the session interface into a class called 'Session_component' derived from the 'Rpc_object' class template. By instantiating this template class with the session interface as argument, the 'Session_component' class gets equipped with the communication code that will make the server's functions accessible via RPC. !#include !#include !#include ! !namespace Hello { struct Session_component; } ! !struct Hello::Session_component : Genode::Rpc_object !{ ! void say_hello() { ! Genode::log("I am here... Hello."); } ! ! int add(int a, int b) { ! return a + b; } !}; Getting ready to start ====================== The server component won't help us much as long as we don't use it in a server application. Starting a service with Genode works as follows: * Create and announce a root capability to our parent. * When a client requests our service, the parent invokes the root capability to create session objects and session capabilities. These are then used by the client to communicate with the server. The class 'Hello::Root_component' is derived from Genode's 'Root_component' class template. This class defines a '_create_session' method, which is called each time a client wants to establish a connection to the server. This function is responsible for parsing the parameter string the client hands over to the server and for creating a 'Hello::Session_component' object from these parameters. !#include !#include ! !namespace Hello { class Root_component; } ! !class Hello::Root_component !: ! public Genode::Root_component !{ ! protected: ! ! Session_component *_create_session(const char *args) ! { ! Genode::log("creating hello session"); ! return new (md_alloc()) Session_component(); ! } ! ! public: ! ! Root_component(Genode::Entrypoint &ep, ! Genode::Allocator &alloc) ! : ! Genode::Root_component(ep, alloc) ! { ! Genode::log("creating root component"); ! } !}; Now we only need the actual application code that instantiates the root component and the service to our parent. It is good practice to represent the applications as a class called 'Main' with its constructor taking the component's environment as argument. !#include ! !namespace Hello { struct Main; } ! !struct Hello::Main !{ ! Genode::Env &env; ! ! Genode::Sliced_heap sliced_heap { env.ram(), env.rm() }; ! ! Hello::Root_component root { env.ep(), sliced_heap }; ! ! Main(Genode::Env &env) : env(env) ! { ! env.parent().announce(env.ep().manage(root)); ! } !}; The sliced heap is used for the dynamic allocation of session objects. It interacts with the component's RAM session to obtain the backing store for the allocations, and the component's region map to make backing store visible within its virtual address space. The announcement of the service is performed by the body of the constructor by creating a capability for the root component as return value of the 'manage' method, and passing this capability to the parent. The 'Component::construct' function of the hello server simply constructs a singleton instance of 'Hello::Main' as a _static_ local variable. !Genode::size_t Component::stack_size() { return 64*1024; } ! !void Component::construct(Genode::Env &env) !{ ! static Hello::Main main(env); !} Making it fly ============= In order to run our application, we need to perform two more steps: Tell the Genode build system that we want to build 'hello_server'. Therefore we create a 'target.mk' file in 'src/hello/server': ! TARGET = hello_server ! SRC_CC = main.cc ! LIBS = base To tell the init component to start the new program, we have to add a '' entry to init's 'config' file, which is located at 'build/bin/config'. ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! For information about the configuring concept, please refer to the "System configuration" section of the Genode Foundations book. Writing client code ################### In the next part, we are going to have a look at the client-side implementation. The most basic steps here are: * Obtain a capability for the "Hello" service from our parent * Invoke RPCs via the obtained capability A client object =============== We will encapsulate the Genode RPC interface in a 'Hello::Session_client' class. This class derives from 'Hello:Session' and implements a client-side object. Therefore edit 'include/hello_session/client.h': !#include !#include !#include ! !namespace Hello { struct Session_client; } ! ! !struct Hello::Session_client : Genode::Rpc_client !{ ! Session_client(Genode::Capability cap) ! : Genode::Rpc_client(cap) { } ! ! void say_hello() ! { ! Genode::log("issue RPC for saying hello"); ! call(); ! Genode::log("returned from 'say_hello' RPC call"); ! } ! ! int add(int a, int b) ! { ! return call(a, b); ! } !}; A 'Hello::Session_client' object takes a 'Capability' as constructor argument. This capability is tagged with the session type and gets passed to the inherited 'Rpc_client' class. This class contains the client-side communication code via the 'call' template function. The template argument for 'call' is the RPC type as declared in the session interface. A connection object =================== Whereas the 'Hello::Session_client' is able to perform RPC calls to an RPC object when given a capability for such an object, the question of how the client obtains this capability is still open. Here, the so-called connection object enters the picture. A connection object has the purposes: * It transforms session-specific parameters into a format that can be passed to the server along with the session request. The connection object thereby hides the details of how the session parameters are represented "on the wire". * It issues a session request to the parent and retrieves a session capability as response. * It acts as a session-client object such that the session's RPC functions can directly be called on the connection object. By convention, the wrapper is called 'connection.h' and placed in the directory of the session interface. For our case, the file 'include/hello_session/connection.h' looks like this: !#include !#include ! !namespace Hello { struct Connection; } ! !struct Hello::Connection : Genode::Connection, Session_client !{ ! Connection(Genode::Env &env) ! : ! /* create session */ ! Genode::Connection(env, session(env.parent(), ! "ram_quota=4K, cap_quota=4")), ! /* initialize RPC interface */ ! Session_client(cap()) { } !}; Client implementation ===================== The client-side implementation using the 'Hello::Connection' object is pretty straightforward. Put this code into 'src/hello/client/main.cc': !#include !#include !#include ! !Genode::size_t Component::stack_size() { return 64*1024; } ! !void Component::construct(Genode::Env &env) !{ ! Hello::Connection hello(env); ! ! hello.say_hello(); ! ! int const sum = hello.add(2, 5); ! Genode::log("added 2 + 5 = ", sum); ! ! Genode::log("hello test completed"); !} Ready, set, go... ================= Add a 'target.mk' file with the following content to 'src/hello/client/': ! TARGET = hello_client ! SRC_CC = main.cc ! LIBS = base Extend your init _config_ as follows to also start the hello-client component: ! ! ! Creating a run script to automate your work flow ================================================ The procedure of building, configuring, integrating, and executing Genode system scenarios across different kernels can be automated using a run script, which can be executed directly from within your build directory. A run script for the hello client-server scenario should be placed at the _run/hello.run_ and look as follows: !build { core ld.lib.so init hello } ! !create_boot_directory ! !install_config { ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !} ! !build_boot_image { core init hello_client hello_server } ! !append qemu_args " -nographic " ! !run_genode_until "hello test completed.*\n" 10 When executed via 'make run/hello', it performs the given steps in sequence. Note that the run script is kernel-agnostic. Hence, you can execute the system scenario on all the different kernels supported by Genode without any modification. The regular expression specified to the 'run_genode_until' step is used as pattern for detecting the success of the step. If the log output produced by the scenario matches the pattern, the run script completes successfully. If the pattern does not appear within the specified time (in this example ten seconds), the run script aborts with an error. By creating the run script, we have not just automated our work flow but have actually created an automated test case for our components.