CAmkES Tutorial

This is a short introduction to using CAmkES and how to create CAmkES-based applications in OKL4 3.0.

Getting CAmkES

Instructions for getting and setting up CAmkES are in Getting Started

Tutorial 1: My First Component-based Application

In this section we walk through the process of defining, implementing, building and running a simple componentised application. The application consists of two components: HelloClient and HelloComponent. HelloClient has a connection to HelloComponent and invokes the hello() method on its Hello interface.

   -------           ----------
  | Hello |         |Hello     |
  | Client|         |Component |
  |       |         |          | 
  |       |--(O-----|          |
  |       | Hello   |          | 
   -------           ----------    

We start by creating a directory for the application:

  $ mkdir iguana/apps/hello
  $ cd iguana/apps/hello

Then we create the SConscript file for the application

  $ vi SConscript
  $ cat SConscript
  
  Import("*")
  app = env.KengeComponentApplication("hello")
  Return("app")
  
  $

After this we create a camkes directory and the hello.camkes file. The hello.camkes file specifies all the components and how they are connected together.

  $ mkdir camkes
  $ vi camkes/hello.camkes
  $ cat camkes/hello.camkes
  
  import "Hello.idl4"; 
  import "std_connector.camkes";
  
  component HelloComponent {
          provides Hello h;
  }
  
  component HelloClient {
          control;
          uses Hello h;
  }
  
  assembly {
          composition {
                  component HelloComponent hcomp;
                  component HelloClient hclient;
  
                  connection IguanaRPC hello(from hclient.h, to hcomp.h);
          }
  }
  
  $

After this we create the include directory and the Hello.idl4 file, which defines the hello interface:

  $ mkdir include 
  $ vi include/Hello.idl4
  $ cat include/Hello.idl4
  
  interface Hello {
          void hello(in smallstring msg);
  };
  
  $

The components and the application are now fully specified.

We then move to the implementations of the two components (HelloClient and HelloComponent)

  $ mkdir HelloClient
  $ mkdir HelloComponent
  
  $ cd HelloComponent
  $ mkdir src
  $ vi src/hello.c
  $ cat src/hello.c
  
  #include <stdio.h>
  #include <HelloComponent_h.h> 
  
  void h__init(void) {
  }
  
  void h_hello(char *msg) {
          printf("This is the hello component saying %s\n", msg);
  }
  
  $

The file hello.c contains the implementation of the HelloComponent. HelloComponent_h.h (which will be generated when we compile the hello.camkes during the build process) contains the definition of the functions that HelloComponent must implement. The name of this file is generated by taking the name of the component and the interface instance name.

Next we write the SConscript file for the component

  $ vi SConscript
  $ cat SConscript
  Import("env")
  comp = env.KengeComponent("HelloComponent", LIBS=["c"])
  Return("comp")
  $

This specifies the component name (so that we know which component is being built) and any dependencies, compile flags etc.

We then go on to implement HelloClient

  $ cd ../HelloClient
  $ mkdir src
  $ vi src/client.c
  $ cat src/client.c
  
  #include <stdio.h>
  #include <HelloClient_h.h>
  
  void run(void);
  void run(void) {
          printf("Starting the client\n");
          printf("-------------------\n");
          while (1) {
                  h_hello("hello world");
          }
  
          printf("After the client\n");
  }
  
  $

The HelloClient component is different from HelloComponent in that it does not provide any interfaces and has been declared to have a thread of control (using the control keyword). As such it has a main function that initialises the component and then spawns a thread to perform the user-specified run() function. The run method is where the actual work done by the component is specified. In this example it repeatedly calls the hello() method on the h interface.

Then we add the SConscript file, specifying how to build the HelloClient component:

  $ vi SConscript
  $ cat SConscript
  Import("env")
  comp = env.KengeComponent("HelloClient", LIBS=["c"])
  Return("comp")
  $

After this we are ready to build the application (see Getting Started for more detail on building and required toolchains).

  $ cd ../../../
  $ tools/build.py PYFREEZE=False machine=gumstix project=iguana mutex_type=kernel apps=hello 

and run it in the simulator:

  $ tools/build.py PYFREEZE=False machine=gumstix project=iguana mutex_type=kernel apps=hello simulate 

the output should eventually show:

  This is the hello component saying hello world
  This is the hello component saying hello world
  This is the hello component saying hello world
  This is the hello component saying hello world
  ...

Tutorial 2: A Compound Component

A somewhat more complex application uses compound components. We also cover reusable components, that is components that are general and could be used by many different applications. We extend our initial application to look like this.

   -------           ----------------------------
  | Hello |         |HelloCompund                |
  | Client|         |                            |
  |       |         |    -------       -------   | 
  |       |--(O-----|-O-| Hello |     | World |  |
  |       | Hello   |   | Comp  |--(O-| Comp  |  | 
   -------          |   |       |World|       |  |
                    |    -------       -------   |
                    |                            | 
                     ----------------------------    

The iguana/apps/hello/camkes/hello.camkes file now becomes:

  $ cat iguana/apps/hello/camkes/hello.camkes
  
  import "Hello.idl4";
  import "World.idl4";
  
  import "std_connector.camkes";
  
  import "HelloComp.camkes";
  import "WorldComp.camkes";
  
  
  component HelloClient {
          control;
          uses Hello h;
  }
  
  
  component HelloCompound {
          provides Hello h;
  
          composition {
                  component HelloComp hcomp;
                  component WorldComp wcomp;
  
                  connection IguanaRPC hworld(from hcomp.w, to wcomp.w);
                  connection IguanaExportRPC helloexport(from hcomp.h, to h);
          }
  }
  
  assembly {
          composition {
                  component HelloCompound hellocomp;
                  component HelloClient hclient;
  
                  connection IguanaRPC hello(from hclient.h, to hellocomp.h);
          }
  }
  
  $

The HelloClient component remains unchanged.

The HelloComp and WorldComp components will be placed in an application independent directory for reusable components (that is, components that can be reused in different applications). This directory is the components/ directory at the root of the OKL4 directory tree,

  $ cd components
  $ mkdir HelloComp
  $ mkdir WorldComp

The directory structure for each reusable component is similar to that for application-specific components (e.g., our HelloComponent in the previous example).

  $ cd HelloComp
  $ mkdir camkes
  $ mkdir src
  $ cd camkes

The definition for HelloComp is in its own .camkes file

  $ vi HelloComp.camkes
  $ cat HelloComp.camkes
  
  import "Hello.idl4";
  import "World.idl4";
  
  component HelloComp {
          provides Hello h;
          uses World w;
  }
  
  $

The source file for the component is simple

  $ cd ..
  $ vi src/hellocomp.c
  $ cat src/hellocomp.c
  
  #include <stdio.h>
  #include <HelloComp_h.h> 
  #include <HelloComp_w.h> 
  
  void h__init(void) {
  }
  
  void h_hello(char *msg) {
          char *world_msg;
          printf("This is the hello component saying hello\n");
          world_msg = w_world(msg);
          printf("The world component told us: %s\n", world_msg);
  }
  
  $

and the SConscript file

  $ vi SConscript
  $ cat SConscript
  
  Import("env")
  comp = env.KengeComponent("HelloComp", LIBS=["c"])
  Return("comp")
  
  $

We have a similar structure for WorldComp

  $ cd ..
  $ cat WorldComp/camkes/WorldComp.camkes
  
  import "World.idl4";
  
  component WorldComp {
          provides World w;
  }
  
  $ cat WorldComp/src/world.c
  
  #include <stdio.h>
  #include <WorldComp_w.h> 
  
  void w__init(void) {
  }
  
  char world_buf[256];
  
  char * w_world(char *msg) {
          sprintf(world_buf, "World got a message: %s", msg);
          return world_buf;
  }
  
  $ cat WorldComp/SConscript
  
  Import("env")
  comp = env.KengeComponent("WorldComp", LIBS=["c"])
  Return("comp")
  
  $ 

For WorldComp there is still one part that hasn't been defined, and that is World.idl4. We place this in a directory for global reusable IDL interfaces components/include.

  $ cd include
  $ vi World.idl4
  $ cat World.idl4
  
  interface World {
      smallstring world(in smallstring msg);
  };
  
  $

Since the Hello interface is now also being used by a reusable component we move the Hello.idl4 file to components/include as well.

We can now go back to the root OKL4 directory and do

  $ tools/build.py PYFREEZE=False machine=gumstix project=iguana mutex_type=kernel apps=hello simulate 

to build and run the new application.

The output should eventually show:

  This is the hello component saying hello
  The world component told us: World got a message: hello world
  This is the hello component saying hello
  The world component told us: World got a message: hello world
  This is the hello component saying hello
  The world component told us: World got a message: hello world
  ...

Note that we did not create a HelloCompound directory or component anywhere. Since HelloCompound is a compound component that encapsulates other components, all the code for it is generated automatically.

Tutorial 3: Using Events

In this example we cover components that communicate using Events. The scenario is as follows:

   -------           -------
  | Event |         | Event |
  | Client|--->>----| Hello |
  |       |  EHello |       |
  |       |          -------
  |       |          -------
  |       |         | Event |
  |       |--->>----| World |
  |       |  EWorld |       |
   -------           -------

We prepare the directory

  $ mkdir iguana/apps/hello3
  $ cd iguana/apps/hello3

and the relevant files

  $ vi camkes/hello3.camkes
  $ cat camkes/hello3.camkes
  import "std_connector.camkes";
  
  component EventClient {
          control;
          emits EHello hevent;
          emits EWorld wevent;
  }
  
  component EventHello {
          control;
          consumes EHello hevent;
  }
  
  component EventWorld {
          control;
          consumes EWorld wevent;
  }
  
  assembly {
          composition {
                  component EventClient eclient;
                  component EventHello ehello;
                  component EventWorld eworld;
  
                  connection IguanaAsynchEvent hello_event(from eclient.hevent, to ehello.hevent);
                  connection IguanaAsynchEvent world_event(from eclient.wevent, to eworld.wevent);
          }
  }
  
  $ vi SConscript
  $ cat SConscript
  Import("*")
  app = env.KengeComponentApplication("hello3")
  Return("app")
  
  $ 

We also create the relevant files for each of the components.

  $ mkdir EventClient EventHello EventWorld
  $ vi EventClient/SConscript
  $ cat EventClient/SConscript
  Import("*")
  comp = env.KengeComponent("EventClient", LIBS=["c"])
  Return("comp")
  
  $ mkdir EventClient/src
  $ vi EventClient/src/client.c
  $ cat EventClient/src/client.c
  #include <stdio.h>
  #include <EventClient_hevent.h>
  #include <EventClient_wevent.h>
  
  void run(void);
  void run(void) {
          printf("Starting the client\n");
          printf("--------------------\n");
  
          for (int i = 0; i < 20; i++) {
                  hevent_emit();
                  printf("sent hevent %d\n", i);
          }
          for (int i = 0; i < 20; i++) {
                  wevent_emit();
                  printf("sent wevent %d\n", i);
          }
  
          printf("Finished the client\n");
  }

For an event emitter an appropriate emit() function is generated for every event interface that a component defines. In this case the functions are hevent_emit() and wevent_emit().

  $ vi EventHello/SConscript
  $ cat EventHello/SConscript
  Import("*")
  comp = env.KengeComponent("EventHello", LIBS=["c"])
  Return("comp")
  
  $ mkdir EventHello/src
  $ vi EventHello/src/hello.c
  $ cat EventHello/src/hello.c
  #include <stdio.h>
  #include <EventHello_hevent.h>
  
  void run(void);
  void run(void) {
          int i;
          for (i =0; i < 20; i++) {
                  hevent_wait();
                  printf("EventHello component says hello\n");
          }
  }
  

For an event consumer, there are several different ways to receive events. The EventHello component uses the generated wait() function called hevent_wait(). This function blocks until the appropriate event arrives. If an event arrived before the wait() function is called then it will return immediately without waiting. Note that if multiple events arrive between calls to wait() then they are all merged into a single event.

  $ vi EventWorld/SConscript
  $ cat EventWorld/SConscript
  Import("*")
  comp = env.KengeComponent("EventWorld", LIBS=["c"])
  Return("comp")
  
  $ mkdir EventWorld/src
  $ vi EventWorld/src/world.c
  $ cat EventWorld/src/world.c
  
  #include <stdio.h>
  #include <EventWorld_wevent.h>
  
  void say_world(void *not_used);
  
  void say_world(void *not_used) {
          printf("EventWorld component says world\n");
          wevent_reg_callback(say_world);
  }
  void run(void);
  void run(void) {
  
          wevent_reg_callback(say_world);
  }
  

Another way that consumers can use events is to register a callback function using the reg_callback() function. The EventWorld uses this approach, registering the say_world() callback using the wevent_reg_callback() function. The callback is invoked when an event arrives. Note, however, that a callback does not remain registered after it is invoked. Since, in this example, we wish the callback function to be invoked every time an event arrives we re-register it within the function itself.

Finally, then we can build the application

  $ tools/build.py PYFREEZE=False machine=gumstix project=iguana mutex_type=kernel apps=hello3 simulate

When run the output will be something like:

  Starting the client
  --------------------
  sent hevent 0
  sent hevent 1
  sent hevent 2
  sent hevent 3
  sent hevent 4
  sent hevent 5
  sent hevent 6
  sent hevent 7
  sent hevent 8
  sent hevent 9
  sent hevent 10
  sent hevent 11
  sent heventEventHello component says hello
   12
  sent hevent 13
  sent hevent 14
  sent hevent 15
  sent hevent 16
  sent hevent 17
  sent hevent 18
  sent hevent 19
  sent wevent 0
  sent wevent 1
  sent wevent 2
  sent wevent 3
  sent wevent 4
  sent EventHello component says hello
  wevent 5
  sent wevent 6
  sent wevent 7
  sent wevent 8
  sent wevent 9
  sent wevent 10
  sent wevent 11
  sent wevent 12
  sent wevent 13
  sent wevent 14
  sent wevent 15
  sent wevent 16
  sent wevent 17
  sent wevent 18
  sent wevent 19
  Finished the client
  EventWorld component says world

Since events are asynchronous, execution doesn't switch to the two receiving components right away, and therefore they don't see all the individual events.

Tutorial 4: Passing Data Through Dataports

Dataports are regions of shared memory between two components. This example covers the basics of implementing a simple dataport connection between two components. The application will look like the following:

   -------    _      ----------
  | Hello |--|_|----|Hello     |
  | Client|  Data   |Component |
  |       |         |          | 
  |       |--(O-----|          |
  |       | Hello   |          | 
   -------           ----------    

We prepare a directory for this application as usual

  $ mkdir iguana/apps/hello4
  $ cd iguana/apps/hello4

and the relevant files

  $ vi SConscript
  $ cat SConscript
  Import("*")
  app = env.KengeComponentApplication("hello4")
  Return("app")
  
  $ 

We also create the relevant files for each of the components.

  $ mkdir HelloClient HelloComponent
  $ vi HelloClient/SConscript
  $ cat HelloClient/SConscript
  Import("*")
  comp = env.KengeComponent("HelloClient", LIBS=["c"])
  Return("comp")
  
  $ vi HelloComponent/SConscript
  $ cat HelloComponent/SConscript
  Import("*")
  comp = env.KengeComponent("HelloComponent", LIBS=["c"])
  Return("comp")
  
  $

The next thing we do is create a type for the dataport. This is defined as a typedef in a C header file placed in the components/include directory. Support for placing these in the application's include directory will be added soon.

  $ vi components/include/dataport_hellodefs.h 
  $ cat components/include/dataport_hellodefs.h
  #define DATA_SIZE 512
  
  /* Definition used in hello4 */
  typedef struct hellodata {
          //The current data size
          int size;       
          //The data
          char data[DATA_SIZE];
  }Data;
  
  $

Unfortunately, there is currently no support for including .h files directly in .camkes files, so this file must be included in an IDL interface file that is imported by your camkes file. We'll put it in the Hello.idl4 file.

  $ vi include/Hello.idl4
  $ cat include/Hello.idl4
  
  import "dataport_hellodefs.h";
  
  interface Hello {
          void hello(void);
  };
  
  $

Then onto the main camkes file:

  $ vi camkes/hello4.camkes
  $ cat camkes/hello4.camkes
  
  import "std_connector.camkes";
  import "Hello.idl4";
  
  component HelloComponent {
          dataport Data buf;
          provides Hello h;
  }
  
  component HelloClient {
          control;
          dataport Data buf;
          uses Hello h;    
  }
  
  assembly {
  
          composition {
                  component HelloComponent hcomp;
                  component HelloClient hclient;
  
                  connection IguanaRPC hello(from hclient.h, to hcomp.h);
                  connection IguanaSharedData hellodata(from hclient.buf, to hcomp.buf);
          }
  }
  
  $

Notice that the type of dataport immediately follows the dataport keyword. If a dataport is declared with Buf as its type, CAmkES will treat it as a default, untyped dataport and no type needs to be specified. If a dataport is given a type that doesn't exist or hasn't been included in one of the imported IDL files (with the exception of Buf), you'll most likely get an error when the compiler can't find a definition for the type. Currently CAmkES just blindly generates the stub files, and lets the C compiler deal with any undefined types.

The next step is to implement the client and server components.

  $ mkdir HelloClient/src
  $ vi HelloClient/src/client.c
  $ cat HelloClient/src/client.c
  #include <stdio.h>
  #include <string.h>
  #include <HelloClient_h.h>  
  #include <HelloClient_buf.h>
  
  void run(void);
  void run(void) {
  
          printf("Starting the client\n");
          printf("-------------------\n");
  
           //Write data to shared mem section
          strcpy(buf_data->data, "hello world");
  
          //Indicate how large the data is and where to read from
          buf_data->size = strlen(buf_data->data);
  
          while (1) {
                  h_hello();
          }
  
          printf("After the client\n");
  }
  
  $ mkdir HelloComponent/src
  $ vi HelloComponent/src/hello.c
  $ cat HelloComponent/src/hello.c
  
  #include <stdio.h>
  #include <string.h>
  #include <HelloComponent_h.h> 
  #include <HelloComponent_buf.h> 
  
  void h__init(void) {
  }
  
  void h_hello(void) {
          printf("This is the hello component saying: %s (data: %p, size: %d)\n", 
                  buf_data->data, 
                  buf_data->data, 
                  buf_data->size );
  }
  
  $

Building and running the example gives us:

  This is the hello component saying: hello world (data: 0x80409008, size: 11)
  This is the hello component saying: hello world (data: 0x80409008, size: 11)
  This is the hello component saying: hello world (data: 0x80409008, size: 11)
  This is the hello component saying: hello world (data: 0x80409008, size: 11)
  ...
  

More Components

There are several other example components included in the distribution.

fat_app is a more complex application that uses a compound component that provides a FAT filesystem. It also uses dataport interfaces.

Directory Structure Overview

The relevant directories in the OKL4 tree are

components/

Contains reusable components. These are components that are not specific to a single application but are general and meant to be reused by different applications. Functionality-wise they are a mix between Iguana servers and libraries.

Each component in this directory has its own subdirectory and that subdirectory has the following structure (with COMPONENT replaced by the component name):

  components/COMPONENT/
                  SConscript
                  camkes/
                          COMPONENT.camkes
                  include/                       
                          *.idl4
                  src/
                          *.c

The COMPONENT.camkes file contains the component definition. The files under include/ are interface files specific to the component. Note that interface files could also be stored under components/include if they are more general and not necessarily specific to this one type of component. The src/ directory contains the source files that implement the component's functionality.

components/ also has an include/ subdirectory. This directory holds any globally usable .camkes files or .idl4 files. For example, the std_connectors.camkes file, which specifies all the default connectors, lives here. IDL files which are used by multiple components can also be placed here.

iguana/apps/

Contains applications. Componentised applications have the following directory structure:

  iguana/apps/APPNAME/
          SConscript
          camkes/
                  APPNAME.camkes
          include/
                  *.idl4 
          COMPONENT/
                  SConscript
                  camkes/
                  include/
                          *.h
                  src/
                          *.c

where APPNAME is replaced by the application name and a COMPONENT directory exists for each application-specific component (with COMPONENT being replaced by the component name). APPNAME.camkes contains a CAmkES ADL description of the application. The include directory contains any interface defintions that are specific to this application (and not used anywhere outside the application). There is a COMPONENT directory for each application specific component used in the APPNAME.camkes file. Application-specific components cannot be used in other applications. The directory structure under here is similar to that of reusable components. An application-specific component can be defined in the application's .camkes file or in a separate .camkes file in that components camkes/ subdirectory.

Generated Files Overview

While building a CAmkES-based application there are a lot of files generated. These include header files that define what component code must implement and what it can call to invoke other components, as well as stubs to implement the connections between components, code required to implement compound components, and code containing service loops for components. The build process also generates code to load, create, and initialise the components and connections between them at system startup time.

All of the generated code is put in the build/components/ directory. Most of the generated code will be put in build/components/APPNAME. With each application having its own directory. The directory tree in there follows the component composition hierarchy. There will be a subdirectory for every top level component instance. If a component instance is a compound component, then its directory will contain subdirectories for every component instance that it contains. Each base component's subdirectory will contain the generated source files relevant to it. It will also include a SConscript file specifying how to build that component as an Iguana server -- this involves combining the programmer specified code and the generated code, compiling it, and linking it together into an object file that can be included in the boot image.

The Code

The code for all the tutorials can be found at camkes-apps-tutorial--release--1.0.tgz