genode/repos/hello_tutorial/doc/hello_tutorial.txt

453 lines
14 KiB
Plaintext

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 <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>
!#include <base/heap.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 lib/ld init hello }
!
!create_boot_directory
!
!install_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>
! <start name="hello_client">
! <resource name="RAM" quantum="1M"/>
! </start>
!</config>}
!
!build_boot_image [build_artifacts]
!
!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.