Creating your first Genode application

Abstract

This section will give you a step-by-step introduction for writing your first little client-server application 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 download the complete tutorial source code from the link at the bottom of this page.

Prerequisites

We assume that you know how to write code and have read:

Norman Feske and Christian Helmuth: "Design of the Genode OS Architecture", TU Dresden technical report TUD-FI06-07, Dresden, Germany, December 2006.

http://genode-labs.com/publications/bastei-design-2006.pdf

so that you have a basic understanding of what Genode is and how things work. Of course, you will also need to check out Genode before going any further.

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 = ../base ../base-linux ../os ../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 applications. You can then build these apps 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 "Hello world."

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

namespace Hello {

  class Session
  {
    protected:

      enum Opcode { SAY_HELLO = 23, ADD = 42 };

    public:

      static const char *service_name() { return "Hello"; }

      virtual ~Session() { }

      virtual void say_hello() = 0;
      virtual int add(int a, int b) = 0;
  };
}

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 a protected enumeration defining the opcodes for the RPCs we are going to implement. Furthermore, we use the interface to specify the name of the service by using the service_name function. This function 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.

A client using the Hello service will invoke the service functions by using a session capability. So let us create a session-capability type for our service interface. By convention, this type is defined in a file called capability.h in the same directory as the interface, in our case include/hello_session/capability.h.

#include <session/capability.h>
#include <hello_session/hello_session.h>

namespace Hello {

  typedef Genode::Typed_capability<Session, Genode::Session_capability>
          Session_capability;
}

This type definition expresses that the new Hello::Session_capability type is compatible to Genode::Session_capability and it is associated with the Hello::Session interface defined above.

Writing server code

Now let's write a server providing the interface defined by Hello::Session.

Implementing the server interface

First of all, we are going to implement a server-side communication stub that dispatches the Hello::Session interface and is derived from this abstract class as well as from the Server_object class defined by the Genode framework. Therefore, we need to include hello_session.h and the base/server.h that includes the definition of Server_object. A server object contains one important method:

dispatch(int opcode, Ipc_istream &is, Ipc_ostream &os)

This method is called whenever a client calls our server. From the opcode parameter it can decide, which actions are to be performed. The second parameter, Ipc_istream is, is a stream of input arguments, the third one is the stream to which output parameters are written.

With this knowledge, we can implement our server-side Hello::Session_server class in\\ 'include/hello_session/server.h':

#include <hello_session/hello_session.h>
#include <base/server.h>

namespace Hello {

  class Session_server : public Genode::Server_object,
                         public Hello::Session
  {
    public:

      Session_server() {
        PDBG("Creating session component."); }

      int dispatch(int op, Genode::Ipc_istream &is,
                           Genode::Ipc_ostream &os)
      {
        int a = 0;
        int b = 0;

        PDBG("dispatch op %d", op);

        switch(op) {

        case SAY_HELLO:
          say_hello();
          break;

        case ADD:
          is >> a;
          is >> b;
          PDBG("%d + %d ?", a, b);
          os << add(a,b);
          break;

        default:
          PWRN("Invalid opcode.");
        }

        return 0;
      }
  };
}

We learn some new things here:

  • The dispatch method is basically a switch statement concerning the Opcodes we defined in Hello::Session.

  • For the ADD opcode we see how the server makes use of the Ipc_istream and Ipc_ostream parameters. Before calling the add method, we read our parameters from the Ipc_istream. Afterwards we write the result to the Ipc_ostream. Genode's IPC framework takes care of marshalling and unmarshalling the messages passed between client and server.

Note It is important that clients write their arguments to the IPC streams in the order the server expects them to do so.

For a detailed discussion of the RPC framework and comparison to traditional approaches refer to:

Norman Feske: "A Case Study on the Cost and Benefit of Dynamic RPC Marshalling for Low-Level System Components", SIGOPS OSR Special Issue on Secure Small-Kernel Systems, 2007

Now that we have created the server-side dispatch code, the only thing that is missing is the implementation of the actual functionality provided via the RPC interface. We place the implementation of the virtual functions as declared in session interface into the Session_component class derived from Session_server. By placing the implementation of these functions into a separate component class, the implementation of the RPC interface remains independent from the communication stub code such that one and the same RPC interface (Session, Session_client, Session_server) could have multiple implementations. Because the Session_component class is not part of the RPC interface but part of the server implementation, we place this class into the file src/hello/server/component.h local to the source code of the server implementation:

#include <hello_session/server.h>

namespace Hello {

  class Session_component : public Hello::Session_server
  {
    void say_hello() {
      PDBG("I am here... Hello."); }

    int add(int a, int b) {
      return a + b; }
  };
}

Getting ready to start

The server object 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:

  • Open a CAP session to our parent, so that we are able to create capabilities.

  • 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.

We did not yet define a root object, so we do it now in src/hello/server/main.cc. The class Hello::Root_component derives from Genode's Root_component class. This class defines a _create_session method which is called when 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 creates a Hello::Session_component object from these parameters.

#include <base/printf.h>
#include <base/env.h>
#include <base/sleep.h>
#include <cap_session/connection.h>
#include <root/component.h>
#include "component.h"

namespace Hello {

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

      Hello::Session_component *_create_session(const char *args)
      {
        PDBG("creating hello session.");
        return new (md_alloc()) Hello::Session_component();
      }

    public:

      Root_component(Genode::Server_entrypoint *ep,
                     Genode::Allocator         *allocator)
      : Genode::Root_component<Hello::Session_component>(ep, allocator)
      {
        PDBG("Creating root component.");
      }
  };
}

Now we only need a main method that announces the service to our parent:

using namespace Genode;

int main(void)
{
  /*
   * Get a session for the parent's capability service, so that we are able
   * to create capabilities.
   */
  Cap_connection cap;

  /*
   * A sliced heap is used for allocating session objects - thereby we can
   * release objects separately.
   */
  static Sliced_heap sliced_heap(env()->ram_session(), env()->rm_session());

  /*
   * Create objects for use by the framework.
   *
   * Msgbufs are used for sending and receiving messages using a
   * 'Server_activation'. A 'Server_entrypoint' is created to announce
   * our service's root capability to our parent and manage incoming
   * session creation requests.
   */
  static Msgbuf<256>             snd_msg, rcv_msg;
  static Server_activation<4096> act(&snd_msg, &rcv_msg);
  static Server_entrypoint       entry(∩, &act);

  static Hello::Root_component hello_root(&entry, &sliced_heap);
  env()->parent()->announce(Hello::Session::service_name(),
                            Root_capability(entry.manage(&hello_root)));

  /*
   * We are done with this and only act upon client requests now.
   */
  sleep_forever();

  return 0;
}

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   = cxx env server

To tell Init to start the new program, we have to add the following entry to Init's config file, which is located at build/bin/config.

 <config>
   <start>
     <filename>hello_server</filename>
     <ram_quota>256K</ram_quota>
   </start>
 </config>

Now rebuild hello/server, go to build/bin, run ./core, and get excited.

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:

  • Get a capability for the Hello service from our parent.

  • Create a Genode::Ipc_client using this capability.

  • Invoke RPCs through the Ipc_client.

A client object

We will encapsulate the Genode IPC 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 <hello_session/capability.h>
#include <base/ipc.h>
#include <base/printf.h>

namespace Hello {

  class Session_client : public Hello::Session
  {
    protected:

      Genode::Msgbuf<256> _sndbuf, _rcvbuf;
      Genode::Ipc_client  _ipc_client;

    public:

      Session_client(Hello::Session_capability cap)
      : _ipc_client(cap, &_sndbuf, &_rcvbuf) { }


      ~Session_client() { }

      void say_hello()
      {
        PDBG("Saying Hello.");
        _ipc_client << SAY_HELLO << Genode::IPC_CALL;
      }

      int add(int a, int b)
      {
        int ret = 0;
        _ipc_client << ADD << a << b << Genode::IPC_CALL >> ret;
        return ret;
      }

      void invalid_op()
      {
        PDBG("Calling server with invalid opcode.");
        _ipc_client << 1234 << Genode::IPC_CALL;
      }
  };
}

Things to note:

  • A Hello::Session_client object is created using a Session_capability. From the given capability and two static message buffers we create an Ipc_client object, that is used to perform our IPC calls.

  • An IPC client is a C++ stream to which we write our RPC. The first argument for an RPC is always the opcode. It is followed by the parameters and finally by Genode::IPC_CALL if you want RPC semantics (which means to sleep until the RPC's result is available).

  • For demonstration purposes, there is another function in the Hello::Session_client class called invalid_op. This function is used to show what happens if an invalid opcode is delivered to the server.

Client implementation

The client-side implementation using the Hello::Session_client object is pretty straightforward. We request a capability for the Hello service from our parent. This call blocks as long as the service has not been registered at the parent. Afterwards, we create a Hello::Session_client object with it and invoke calls. In addition, we use the Timer service that comes with Genode. This server enables us to sleep for a certain amount of milliseconds.

#include <base/env.h>
#include <base/printf.h>
#include <hello_session/client.h>
#include <timer_session/connection.h>

using namespace Genode;

int main(void)
{
  Hello::Session_capability
    h_cap(env()->parent()->session(
      Hello::Session::service_name(), "foo, ram_quota=4K"));
  Hello::Session_client h(h_cap);

  Timer::Connection timer;

  while (1) {
    h.say_hello();
    timer.msleep(1000);

    int foo = h.add(2,5);
    PDBG("Added 2 + 5 = %d", foo);
    timer.msleep(1000);
  }

  return 0;
}

Compared to the creation of the Timer session, the creation of "Hello" session looks rather inconvenient and takes multiple lines of code. For this reason, it is a good practice to supply a convenience wrapper for creating sessions as used for the timer session. This wrapper is also the right place to for documenting session-construction arguments and assembling the argument string. 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 {

  class Connection : public Genode::Connection<Session_capability>,
                     public Session_client
  {
    public:

      /**
       * Constructor
       */
      Connection() :

        /* create session */
        Genode::Connection<Hello::Session_capability>(
          session(service_name(), "foo, ram_quota=4K")),

        /* initialize RPC interface */
        Session_client(cap()) { }
  };
}

With the Connection class in place, we can now use Hello sessions by just instantiating Hello::Connection objects and invoke functions directly on such an object. For example:

Hello::Connection hello;
int foo = hello.add(2, 5);

Ready, set, go...

Add a target.mk file with the following content to 'src/hello/client/':

 TARGET = hello_client
 SRC_CC = main.cc
 LIBS   = cxx env

Add the following entries to your config file:

 <start>
   <filename>timer</filename>
   <ram_quota>256K</ram_quota>
 </start>
 <start>
   <filename>hello_client</filename>
   <ram_quota>256K</ram_quota>
 </start>

Build drivers/timer, and hello/client, go to build/bin, and run ./core again. You have now successfully implemented your first Genode application.

Document Actions