mirror of
https://github.com/genodelabs/genode.git
synced 2024-12-19 05:37:54 +00:00
91e81591fe
Bring the docs up to date with the current run definition at https://github.com/genodelabs/genode/blob/master/repos/hello_tutorial/run/hello.run as well as changes to the imports of https://github.com/genodelabs/genode/blob/master/repos/hello_tutorial/src/hello/server/main.cc Fix #5291 Fix #5292
453 lines
14 KiB
Plaintext
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.
|