A simple client-server scenario

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 <session/session.h>
#include <base/rpc.h>

namespace Hello { struct Session; }

struct Hello::Session : Genode::Session
{
  static const char *service_name() { return "Hello"; }

  enum { CAP_QUOTA = 4 };

  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 resources definition specifies the amount of capabilities and RAM required by the server to establish the session. The specified amount is transferred from the client to the server at session creation time.

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 <base/log.h>
#include <hello_session/hello_session.h>
#include <base/rpc_server.h>

namespace Hello { struct Session_component; }

struct Hello::Session_component : Genode::Rpc_object<Session>
{
  void say_hello() override {
    Genode::log("I am here... Hello."); }

  int add(int a, int b) override {
    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 <base/log.h>
#include <root/component.h>

namespace Hello { class Root_component; }

class Hello::Root_component
:
  public Genode::Root_component<Session_component>
{
  protected:

    Session_component *_create_session(const char *) override
    {
      Genode::log("creating hello session");
      return new (md_alloc()) Session_component();
    }

  public:

    Root_component(Genode::Entrypoint &ep,
                   Genode::Allocator &alloc)
    :
      Genode::Root_component<Session_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 <base/component.h>

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 <start> entry to init's config file, which is located at build/bin/config.

 <config>
   <parent-provides>
     <service name="LOG"/>
     <service name="PD"/>
     <service name="CPU"/>
     <service name="ROM"/>
   </parent-provides>
   <default-route>
     <any-service> <parent/> <any-child/> </any-service>
   </default-route>
   <default caps="60"/>
   <start name="hello_server">
     <resource name="RAM" quantum="1M"/>
     <provides> <service name="Hello"/> </provides>
   </start>
 </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 <hello_session/hello_session.h>
#include <base/rpc_client.h>
#include <base/log.h>

namespace Hello { struct Session_client; }


struct Hello::Session_client : Genode::Rpc_client<Session>
{
  Session_client(Genode::Capability<Session> cap)
  : Genode::Rpc_client<Session>(cap) { }

  void say_hello() override
  {
    Genode::log("issue RPC for saying hello");
    call<Rpc_say_hello>();
    Genode::log("returned from 'say_hello' RPC call");
  }

  int add(int a, int b) override
  {
    return call<Rpc_add>(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 <hello_session/client.h>
#include <base/connection.h>

namespace Hello { struct Connection; }

struct Hello::Connection : Genode::Connection<Session>, Session_client
{
  Connection(Genode::Env &env)
  :
    /* create session */
    Genode::Connection<Hello::Session>(env, Label(),
                                       Ram_quota { 8*1024 }, Args()),
    /* 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 <base/component.h>
#include <base/log.h>
#include <hello_session/connection.h>

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:

 <start name="hello_client">
   <resource name="RAM" quantum="1M"/>
 </start>

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 init hello }

create_boot_directory

install_config {
<config>
  <parent-provides>
    <service name="LOG"/>
  </parent-provides>
  <default-route>
    <any-service> <parent/> <any-child/> </any-service>
  </default-route>
  <default caps="60"/>
  <start name="hello_server">
    <resource name="RAM" quantum="1M"/>
    <provides> <service name="Hello"/> </provides>
  </start>
  <start name="hello_client">
    <resource name="RAM" quantum="1M"/>
  </start>
</config>}

build_boot_image { core ld.lib.so init hello_client hello_server }

append qemu_args " -nographic "

run_genode_until "hello test completed.*\n" 10

When executed via make run/hello KERNEL=linux, it performs the given steps in sequence and runs the scenario on Genode/Linux. 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.