Hello world
This section introduces the steps needed to create and execute a simple custom component that prints a hello-world message.
Using a custom source-code repository
In principle, it would be possible to add a new component to one of the existing source-code repositories found at <genode-dir>/repos/. However, unless the component is meant to be incorporated into upstream development of the Genode project, it is generally recommended to keep custom code separate from Genode's code base. This eases future updates to new versions of Genode and allows you to pick a revision-control system of your choice.
The new repository must appear within the <genode-dir>/repos/ directory. This can be achieved by either hosting it as a subdirectory or by creating a symbolic link that points to an arbitrary location of your choice. For now, let us host a new source-code repository called "lab" directly within the repos/ directory.
cd <genode-dir> mkdir repos/lab
The lab repository will contain the source code and build rules for a single component as well as a run script for executing the component within Genode. Component source code reside in a src/ subdirectory. By convention, the src/ directory contains further subdirectories for hosting different types of components, in particular server (services and protocol stacks), drivers (hardware-device drivers), and app (applications). For the hello-world component, an appropriate location would be _src/app/hello/_:
mkdir -p repos/lab/src/app/hello
Source code and build description
The hello/ directory contains both the source code and the build description of the component. The main part of each component typically resides in a file called main.cc. Hence, for a hello-world program, we have to create the repos/lab/src/app/hello/main.cc file with the following content:
#include <base/component.h> #include <base/log.h> void Component::construct(Genode::Env &) { Genode::log("Hello world"); }
The base/component.h header contains the interface each component must implement. The construct function is called by the component's execution environment to initialize the component. The interface to the execution environment is passed as argument. This interface allows the application code to interact with the outside world. The simple example above merely produces a log message. The log function is defined in the base/log.h header.
The component does not exit after the construct function returns. Instead, it becomes ready to respond to requests or signals originating from other components. The example above does not interact with other components though. Hence, it will just keep waiting infinitely.
Please note that there exists a recommended coding style for genuine Genode components. If you consider submitting your work to the upstream development of the project, please pay attention to these common guidelines.
- Coding-style guidelines
-
https://genode.org/documentation/developer-resources/coding_style
The source file main.cc is accompanied by a build-description file called target.mk. It contains the declarations for the source files, the libraries used by the component, and the name of the component. Create the file repos/lab/src/app/hello/target.mk with the following content:
TARGET = hello SRC_CC = main.cc LIBS += base
Building the component
With the build-description file in place, it is time to build the new component, for example from within the x86_64 build directory as created in Section A simple system scenario. To aid the build system to find the component, we have to extend the build configuration <build-dir>/etc/build.conf by appending the following line:
REPOSITORIES += $(GENODE_DIR)/repos/lab
By adding this line, the build system will consider our custom source-code repository. To build the component, issue the following command:
make app/hello
This step compiles the main.cc file and links the executable ELF binary called "hello". The result can be found in the <build-dir>/app/hello/ subdirectory.
Defining a system scenario
For testing the component, we need to define a system scenario that incorporates the component. As mentioned in Section A simple system scenario, such a description has the form of a run script. To equip the lab repository with a run script, we first need to create a lab/run/ subdirectory:
mkdir <genode-dir>/repos/lab/run
Within this directory, we create the file <genode-dir>/repos/lab/run/hello.run with the following content:
build "core init app/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-service> </default-route> <default caps="100"/> <start name="hello"> <resource name="RAM" quantum="10M"/> </start> </config> } build_boot_image "core ld.lib.so init hello" append qemu_args "-nographic -m 64" run_genode_until {Hello world.*\n} 10
This run script performs the following steps:
-
It builds the components core, init, and app/hello.
-
It creates a fresh boot directory at <build-dir>/var/run/hello. This directory contains all files that will end up in the final boot image.
-
It creates a configuration for the init component. The configuration starts the hello component as the only child of init. Session requests originating from the hello component will always be directed towards the parent of init, which is core. The <default> node declares that each component may consume up to 100 capabilities.
-
It assembles a boot image with the executable ELF binaries core, ld.lib.so (the dynamic linker), init, and hello. The binaries are picked up from the <build-dir>/bin/ subdirectory.
-
It instructs Qemu (if used) to disable the graphical output.
-
It triggers the execution of the system scenario and watches the log output for the given regular expression. The execution ends when the log output appears or after a timeout of 10 seconds.
The run script can be executed from within the build directory via the command:
make run/hello KERNEL=linux BOARD=linux
After the boot output of the used kernel, the scenario will produce the following output:
[init -> test-printf] Hello world Run script execution successful.
The label within the brackets at the start of each line identifies the component where the message originated from. The final line is printed by the run tool after it successfully matched the log output against the regular expression specified to the run_genode_until command.
Responding to external events
Most non-trivial components respond to external events such as user input, timer events, device interrupts, the arrival of new data, or RPC requests issued by other components.
The following example presents the typical skeleton of such a component. The construct function merely creates an object representing the application as a static local variable. The actual component code lives inside the Main class.
#include <base/component.h> #include <base/log.h> #include <timer_session/connection.h> namespace Hello { struct Main; } struct Hello::Main { Genode::Env &_env; Timer::Connection _timer { _env }; void _handle_timeout() { log("woke up at ", _timer.elapsed_ms(), " ms"); } Genode::Signal_handler<Main> _timeout_handler { _env.ep(), *this, &Main::_handle_timeout }; Main(Genode::Env &env) : _env(env) { _timer.sigh(_timeout_handler); _timer.trigger_periodic(1000*1000); Genode::log("component constructed"); } }; void Component::construct(Genode::Env &env) { static Hello::Main main(env); }
First, note the Hello namespace. As a good practice, component code typically lives in a namespace. The component-specific namespace may incorporate other namespaces - in particular the Genode namespace - without polluting the global scope.
The constructor of the Main object takes the Genode environment as argument and stores it as the reference member variable _env. The member variable is prefixed with an underscore to highlight the fact that it is private to the Main class. In principle, Main could be a class with _env being part of the private section, but as Main is the top-level class of the component that is not accessed by any other parts of the program, we use a struct for brevity while still maintaining the convention to prefix private members with an underscore character. When spotting the use of such a prefixed variable in the code, we immediately see that it is part of the code's object context, not being an argument or a local variable.
By aggregating a Timer::Connection as a member variable, the Main object requests a session to a timer service at construction time. As this session request requires an interaction with the outside world, the _env needs to be passed to the _timer constructor.
In order to respond to events from the timer, the Main class hosts a _timeout_handler object. Its constructor arguments refer to the object and a method to be executed whenever an event occurs. The timeout handler object is registered at the _timer as the recipient of timeout events via the sigh method. Finally, the timer is instructed to trigger timeout events at a rate of 1 second.
The following remarks are worth noting:
-
The programming style expresses what the component is rather than what the component does.
-
The component does not perform any dynamic memory allocation.
-
When called, the _handle_timeout method has its context (the Main object) readily available, which makes the application of internal state changes as response to external events very natural.
-
Both the construct function as well as the Main::_handle_timeout method do not block for external events.
-
The component does not receive any indication about the number of occurred events, just the fact that at least one event occurred. The _handle_timeout code explicitly requests the current time from the timer driver via the synchronous RPC call elapsed_ms.
To execute the new version of the component, we need to slightly modify the run script.
build "core init timer app/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="100"/> <start name="timer"> <resource name="RAM" quantum="1M"/> <provides> <service name="Timer"/> </provides> </start> <start name="hello"> <resource name="RAM" quantum="10M"/> </start> </config> } build_boot_image "core ld.lib.so init timer hello" append qemu_args "-nographic -m 64" run_genode_until forever
The modifications are as follows:
-
Since the hello component now relies on a timer service, we need to build and integrate a timer driver into the scenario by extending the build and build_boot_image steps accordingly.
-
We instruct init to spawn the timer driver as an additional component by adding a <start> node to init's configuration. Within this node, we declare that the component provides a service of type "Timer".
-
To enable the hello component to open a "Timer" session at the timer driver, the default route is modified to consider any children as servers whenever the requested service is not provided by the parent.
-
This time, we let the scenario run forever so that we can watch the messages printed at periodic intervals.
When starting the run script, we can observe the periodic activation of the component in the log output:
[init] child "timer" announces service "Timer" [init -> hello] component constructed [init -> hello] woke up at 12 ms [init -> hello] woke up at 1008 ms [init -> hello] woke up at 2005 ms ...