C-Kernel - An Example

Here we provide a brief tutorial on how to write components for the C version of the RUNES middleware through a sample application. First we outline the process involved in developing a component, then we build on this to develop a (simple) multi-component application…

Developing a Component

In the C/Linux environment, components are represented as Linux .so files. In the C source for the .so file, the developer implements the set of functions that appear in all of the interfaces supported by the component. Each function takes Component * as its first argument - this refers to the component instance being invoked. In each component, the developer implements two standard C functions as follows:

As an example, we now develop an Adder component that has a single interface as follows:

/* IDL */
Interface Adder {
  int add(in int a, in int b);
  int increment(void);
}



The header file for this component is as follows:

/* adder.h - Adder Component with a single Interface */
#include “rcm.h”

/* Interface struct for the Adder interface */
typedef struct adder {
  int (*add)(Component *comp, int x, int y);
  int (*increment)(Component *comp);
} Adder;

/* State struct for the 'adder' component */
typedef struct as {
  int current; /* for use by increment() */
} Adder_State;

Now the .c file. In the contruct() function there are 5 steps outlined in the below. Actually, these are quite generic to any contructor implementation:

/* adder.c - adder component with a single interface
   of type Adder */
#include "rcm.h“
#include "adder.h“

int construct(Component *comp)
{
  Adder *ai;
  Adder_State *as;

  /* STEP 1: Allocate memory for function pointer structs
     (n.b. VEFIFY() is a simple macro that makes its enclosing
     function return the macro’s second arg if the first arg
     evaluates to 0 - in such cases the list of statements
     comprising the third arg is executed before returning) */   
  VERIFY(ai = (Adder *)malloc(sizeof(Adder)),
    ERR, "adder.construct(): malloc",); 

  /* STEP 2: Allocate the memory for the state struct */
  VERIFY(as = (Adder_State *)malloc(sizeof(Adder_State)),
    ERR, "adder.construct(): malloc", free((void *)ai)); 

  /* STEP 3: Create and register the receptacles. As there
     are no receptacles in this component we have nothing to
     do here. If there were, we would use rcm_regreceptacle()
     in a similar way to that illustrated below for
     rcm_reginterface(). */ 

  /* STEP 4: Create and register the interfaces */   
    ai->add = add ;      /* assign function pointers... */
    ai->increment = increment ;
    VERIFY(rcm_reginterface(comp, "Adder", ai, sizeof(Adder)),
      ERR, "adder.construct(): rcm_reginterface",
      free((void *)ai); free((void *)as););

  /* STEP 5: Create and register the state */
    as->current = 0;
    rcm_regstate(comp, (void *)as);
    return OK;
}

int destruct(Component *comp)
{
  /* free any non-top-level state-related dynamic
     data - none here */
  return OK;
}    

int add(Component *comp, int a, int b)
{
  /* carry out the application specific job; in this case we
     can ignore the comp argument as the add operation doesn't
     involve state */
  return a + b;
}

int increment(Component *comp)
{
  Adder_State *state;
  /* call a utility function to obtain the state for this
     component */
  rcm_getstate(comp, (void **)&state);
  return ++(state->current);
}

Developing a Client for our Adder Component

Now, here is a simple client for the Adder component we have just developed:

/* IDL */
Interface Client {
  int run(void);
}

/* client.h */
#include "rcm.h“

typedef struct client {
  int (*run) (Component *comp);
} Client;

/* client.c */
#include "client.h“
#include "adder.h“

int construct(Component *c); {...}
int destruct(Component *c); {...}
int run(Component *comp)
{
  Receptacle *r;
  int i, t;

  VERIFY(rcm_getreceptacle(comp, "Adder", &r),
    ERR, "client.run(): get_receptacle",);
  for (i=0; i<5; i++) {
    t = INVOKE_RECEPTACLE(r, Adder, add, i, i+1);
    printf("Adder.add(%d, %d): %d\n", i, i+1, t);
    t = INVOKE_RECEPTACLE(r, Adder, increment);
    printf("Adder.increment(): %d\n", t);
  }
  return OK;
}

Note that run()calls rcm_getreceptacle() to obtain the client component's receptacle, and then uses the INVOKE_RECEPTACLE() macro to perform an invocation on that receptacle. The receptacle is assumed here to be already bound to the Adder component's interface! How does this happen? To see how let's now look at an application that puts it all together...

Putting it all Together

Here is a main() function that instantiates and binds instances of our Adder and Client components and then sets things off by calling the Client's run() operation. Note that the call of run() is achieved using a macro called INVOKE_INTERFACE(). This allows us to call an interface directly - i.e. an interface that has not necessarily been bound to a receptacle. This should only be used for bootstrapping purposes as in the case here - somehow we need to get the 'first' component running given that a C main() function is not itself a component.

main()
{
  ComponentType *adderCT, *clientCT;
  Component *adder, *client;
  Receptacle *rAdder;
  Interface *iAdder, *iClient;
  Component *con;

  printf("Starting...\n");   
 
  /* initialise */
  rcm_init();

  /* load the “adder” and “client” ComponentTypes */
  VERIFY(rcm_load("adder.so", &adderCT) != ERR,
    ERR, "main: rcm_load adder",);
  VERIFY(rcm_load("client.so", &clientCT) != ERR,
    ERR, "main: rcm_load client",);

  /* instantiate adder and client components */
  VERIFY(rcm_instantiate(adderCT, &adder, 0,
    (char **)0) != ERR, ERR, "main: rcm_instantiate adder",);
  VERIFY(rcm_instantiate(clientCT, &client, 0,
    (char **)0) != ERR, ERR, "main: rcm_instantiate client",);

  /* get the Adder Receptacle from the client instance */
  VERIFY(rcm_getreceptacle(client, "Adder", &rAdder),
    ERR, "main: get_receptacle adder",);

  /* get the Adder Interface from the adder instance */
  VERIFY(rcm_getinterface(adder, "Adder", &iAdder),
    ERR, "main: get_interface adder",);

  /* connect them using the default Connector */
  VERIFY(rcm_connect(rAdder, iAdder,
    "default_connector.so", &con), ERR, "main: rcm_connect",);

  /* get the Client interface from client and call
     the “run” method on it */
  VERIFY(rcm_getinterface(client, "Client", &iClient),
    ERR, "main: get_interface client",);
  INVOKE_INTERFACE(iClient, Client, run);

  /* when we get back here, client has done making
     calls on adder; so clean up...  */
  VERIFY(rcm_destroy(con) != ERR,
    ERR, "main: rcm_destroy dummy Conn",);
  VERIFY(rcm_destroy(adder) != ERR,
    ERR, "main: rcm_destroy adder",);
  VERIFY(rcm_destroy(client) != ERR,
    ERR, "main: rcm_destroy client",);
  VERIFY(rcm_unload(adderCT) != ERR,
    ERR, "main: rcm_unload adderCT",);
  VERIFY(rcm_unload(clientCT) != ERR,
    ERR, "main: rcm_unload clientCT",);  
  printf("Done\n");
}