Hello RCS World

Don McLane
University of Washington Tacoma
GPLD © 2001 by Don McLane

1.  Taking the Plunge.

Kernighan and Ritchie popularized an approach to learning a programming language which began by writing the smallest possible program that would do something [1].  Their example was a program that simply printed out "hello world."  That’'s the approach we take here.  There is nothing here that could not be found elsewhere--just a gentler introduction.

You might ask; "Why bother when there’'s an RCS Design Tool available?".  Even with the design tool, life will be easier if you understand the basic components.  Also, with the RCS Design Tool, you might get the impression that nothing can be done with RCS in fewer than a dozen files.  That may be true for any sizeable application.  But simple, small examples are possible and usually preferable when you are learning.

Maybe you’ve tried to make sense of the documentation.  Your head is swimming with new acronyms.  Isn’t there something simple we could start with--like a hello world program?  Yes!  We can write exactly a hello world program.

We begin with the assumumption that the RCS software is installed and you have basic knowledge of C++.

First here’s an overview: to use RCS all you need to do is include rcs.hh and learn about three classes:

  1. NMLmsg
  2. NML
  3. NML_MODULE

How hard can that be  (these must be someone’s famous last words)?

We will write hello world examples with each class, but we start by using none of them. Here is our simplest possible RCS program:

#include "rcs.hh"
void main(int argc,     char *argv[])
{
      rcs_print("hello RCS world\n");
}
Yes, that’s it!  Of course; it’s not quite that simple.  You need to get some paths and libraries set up first.  Here’s how it’s done with Microsoft Visual C++.

Now, compile and run.  While the result isn’t earth shattering, getting the environment set up correctly is often times no mean feat. Now you’re ready to rock-and-roll.

2.  Making a Message from NMLmsg

Let’s start with the NMLmsg class which is the base class for messages we’ll be sending around.  What’s the simplest thing we can do with it?

A class is normally declared in a header file, then the functions are implemented in a different file, and the class is created and used in yet another file.  But the simple, quick and dirty way is to just define the class in-line and in one file.  Don’t do this for production code; this is sandbox code for learning only.  Below, we derive a class  from the NMLmsg base class which we’ll call HELLO_MESSAGE.  When a class is derived from a base class, usually something is added.  We’ll add some data, and an array of characters which the constructor initializes to "hello RCS world." Here is the entire file:

#include rcs.hh
#define HELLO_MESSAGE_TYPE 123
class HELLO_MESSAGE : public NMLmsg
{
public:
      HELLO_MESSAGE() : NMLmsg(HELLO_MESSAGE_TYPE,
                               sizeof(HELLO_MESSAGE))
      {
        strcpy(msg, "hello RCS world");
      }
      char msg[32];
};
void main(int argc, char* argv[])
{
      HELLO_MESSAGE hello;
      rcs_print("%s", hello.msg);
}
It almost seems too simple.  But consider, you have:  a) derived a new message class from NMLmsg, b) customized it by adding some data, c) written a constructor that initializes the data, d) made an instance of the class, and e) referenced the data. 

3.  Channeling with NML

Now that we have a message, we’d like to send it somewhere.  All messages get sent through a communication channel, which is a class derived from NML.  So we need to make a channel.  But look at all the weird stuff that the NML constructor takes:

NML(NML_FORMAT_PTR f_ptr, char *buf, char *proc, char *file);

What is all that stuff?  Well, the short answer is:

f_ptr ...... a format function
buff ....... name of buffer
proc ....... name of process
file ....... name of nml file

Unfortunately, the short answer won’t suffice.

The format function is called when the channel receives a message, and it typically calls update() for the particular type of message.  If you study the examples, you’ll probably end up with a function like this:

int helloFormat(NMLTYPE type, void *buf, CMS *cms)
{
    switch(type)
    {
    case HELLO_INIT_TYPE:
          ((HELLO_INIT *)buf)->update(cms);
         break;
    case HELLO_HALT_TYPE:
          ((HELLO_HALT *)buf)->update(cms);
          break;
    case HELLO_MESSAGE_TYPE:
          ((HELLO_MESSAGE *)buf)->update(cms);
          break;
    default:
          rcs_print("helloFormat: Invalid message.\n");
          return 0;
    }
    return 1;
}

The design here seems odd.  Notice that all this function does is to cast buf to the particular type of message, and then call update(…).  Aren’t virtual functions designed to avoid such things?  Couldn’t we just dispense with the format function altogether and instead call a function like this:

(NMLmsg *)buf->update(cms);

Polymorphism was invented to eliminate such constructs.  But it’s done now, so we’ll just have to humor those wild and crazy guys at NIST.

However, we’ll continue our minimalist style and use the world’s simplest format function:

int dummyFormat(NMLTYPE type, void *buf, CMS *cms)
{
    return 1;
}
What about the remaining three parameters?  It’s easiest to start at the end.  The last parameter is the name of an NML configuration file, usually with the extension .nml. That file contains definitions of the previous two parameters buf and proc. For example:

 


 

An NML configuration file has only two types of entries: 1) lines that start with a B, which describe a buffer, and 2) lines which start with a P, which describe a process that uses one of the buffers.  Let’s accept defaults for most items in the file and concentrate on understanding just three: buffer name, process name, and master.

Each communication channel is associated with exactly one buffer and one process.  If two modules want to send messages to each other they each need a channel to a common buffer.  One channel must be a master.  The process name distinguishes the master from the non-master.  For instance, here two modules wish to communicate through the common buffer hello_buf:

 


Now we want to write the world’s simplest NML configuration file.  So, here is rcshello.nml:

# Buffers
# Name        Type   Host      size  neut?  RPC#      buffer# MP   . . .
B hello_buf   LOCMEM localhost  2048  0    x2010bbe0   1      8 38440  TCP=9681 xdr

# Processes
# Name        Buffer    Type   Host       Ops server?  timeout  master?     cnum
# hello(0)
P hello_proc  hello_buf  LOCAL  localhost  RW    0       0.1      1      0

The lines starting with a ‘#’ are comments and are included because the column headings are helpful.  One process must be a master and since we only have one process hello_proc is a master.  Here is hello world for an NML channel.

void main(int argc, char* argv[])
{
      HELLO_MESSAGE hello;
      NML channel(helloFormat, "hello_buf", "hello_proc",
                                     "rcshello.nml");
      channel.write( &hello );
      if (channel.read() > 0)   // there's an unread message
      {
            HELLO_MESSAGE *hello_ptr =
                    (HELLO_MESSAGE *)channel.get_address();
            rcs_print("%s", hello_ptr->msg);
      }
      else
            rcs_print("something's not right here\n");
}

Now that we have made an instance of a channel, we want to use it.

 

4.  NML_MODULE, The Final Frontier.

4.1 Make an instance.

The whole point is to make modules.  RCS is designed for problems that can be decomposed into modules that can be implemented separately, connected by messages.  What’s the simplest possible control module we can write?  Normally, each control module would run in its own process.  And normally, it would run in an endless loop.  We’ll, of course, do everything in one process and just loop a few times since that will serve our purpose.  Consider this:

class HELLO_MODULE : public NML_MODULE
{
public:
      void PRE_PROCESS() rcs_print("hello there again\n"); }
};
main(int argc, char* argv[])
{
  RCS_TIMER *timer = new RCS_TIMER(1.0);
  *mod = new HELLO_MODULE(); for (int i = 1; <= 5; i++)    // usually loop forever
      mod->controller(); timer->wait();
}

The module should be periodically called, spitting out "hello there again" each time.

Unfortunately, this program crashes at the call controller(), so maybe it’s a little too simple (or maybe we found a bug).  Some experimentation finds what’s needed.  All control modules have special status and command channels.  They’re special because they are automatically read and written by the module.  The RCS library has special message and channel classes for the purpose, derived, of course, from NMLmsg and NML.  Here is an inheritance diagram, by convention, the arrow point to the base class.

 


Initializing the channels with calls to setStatChannel(…) and setCmdChannel(…) solves the problem.  Although before we call setStatChannel we need an instance of an RCS_STAT_MSG, or something derived from RCS_STAT_MSG.  Despite our usual minimalist approach we’ll derive a class, since it’s such a common occurrence.

#define HELLO_STATUS_TYPE  123
class HELLO_STATUS : public RCS_STAT_MSG
{
public:
      HELLO_STATUS() : RCS_STAT_MSG(HELLO_STATUS_TYPE,
                             sizeof(HELLO_STATUS)) {}
};

So here’s our class definition with the status and command channels initialized in the constructor:

class HELLO_MODULE : public NML_MODULE
{
public:
  HELLO_MODULE()
  {
    HELLO_STATUS *statmsg = new HELLO_STATUS();
    setStatChannel(new RCS_STAT_CHANNEL(dummyFormat,
                  "hello_sts_buf", "hello_proc",
                  "rcshello.nml"), statmsg);
    setCmdChannel(new RCS_CMD_CHANNEL (dummyFormat,
                 "hello_cmd_buf", "hello_proc", "rcshello.nml"));
  }
  void PRE_PROCESS() { rcs_print("hello there again\n"); }
};

Of course the buffers must be defined in the nml file so add the following to rcshello.nml:

B hello_cmd_buf  LOCMEM  localhost  2048  0  0x2010bbe0  1  8  38440  TCP=9681 xdr
B hello_sts_buf  LOCMEM  localhost  2048  0  0x2010bbe0  2  8  38441  TCP=9681 xdr
P hello_proc   hello_cmd_buf   LOCAL   localhost   RW   0   0.1   1   0
P hello_proc   hello_sts_buf   LOCAL   localhost   RW   0   0.1   1   0

Success! a running control module albeit not a particularly useful one.  But, we really can’t say we’ve covered everything until we’ve sent messages to the module and received responses.

4.2  Send a message.

What’s the simplest example of sending a message to a module?  Recall that modules have a command channel that is automatically read at the beginning of each cycle, so we don’t have to do much work if we use that.  But our main function needs a channel into the same buffer.  So we add the following declaration to main(…), but place it after the module is constructed, since the channels in HELLO_MODULE are masters and must be made first.

RCS_CMD_CHANNEL  *cmd  = new RCS_CMD_CHANNEL (dummyFormat, "hello_cmd_buf",
    "main_proc", "rcshello.nml");

We need some nml configuration as well.  It has to be in have a different process name, because there should only be one master process.  So, "process name" isn’t necessarily related to the operating system process.  Usually channels will be in different operating system processes, so the term "process name" is reasonable in the  typical case.  Add the following to rcshello.nml:

P main_proc   hello_cmd_buf   LOCAL   localhost   RW   0   0.1   0   0

We’ll also derive a new message class named VAR_CMD which can do something different each time if we put it in our loop, so we can see something happening.

class VAR_CMD : public RCS_CMD_MSG
{
public:
      VAR_CMD(int i) : RCS_CMD_MSG(i, sizeof(VAR_CMD)) {};
};

While the command channel is read automatically each cycle, to handle it, we must override the DECISION_PROCESS() function.  Here’s our quick and dirty function:

void DECISION_PROCESS()
{
    rcs_print("I got a type %d message\n", commandInData->type);
}

Now let’s write each time on our main loop:

for (int i = 1; i <= 5; i++)
{
    mod->controller();
    timer->wait();
    cmd->write( new VAR_CMD(i));
}

The output is getting a bit messy, but should convince you that, indeed ,you are sending a message to a control module.

4.3   Read a message.

A status message is automatically updated each cycle by a module.  So that’s the easiest thing to read.  A channel to the buffer is needed in main().

RCS_STAT_CHANNEL *stat = new RCS_STAT_CHANNEL(dummyFormat,
                   "hello_sts_buf", "main_proc", "rcshello.nml");

and associated nml needs to be added to our NML configuration file:

P main_proc   hello_sts_buf   LOCAL   localhost   RW   0   0.1   0   0

All we want to do is see some information returned--enough to convince ourselves that we are actually getting messages sent from the module.  What’s the simplest thing we can do?  There is a function called msg2str that converts the contents of a message to a string.  It can’t get much easier than that.

And finally, our main loop:

RCS_STAT_MSG *address;
for (int i = 1; i <= 5; i++)
{
    mod->controller();
    timer->wait();
    if (stat->read() > 0)      // is there an unread message?
    {
        address = stat->get_address();
        rcs_print("%s\n", stat->msg2str(address) );
    }
    cmd->write( new VAR_CMD(i));
}
The output is even messier.  One of the members of a status message is the last command received, and the module updates it automatically for us.  Notice how it is incremented, as we increment the type of command message sent.  We are receiving messages.

We are running a control module, sending commands to it and reading its status.  It’s basic, but it’s running.  Certainly, we committed various programming sins (such as making a bunch of VAR_CMD instances without deleting any).  So … fix it!  Try some experiments on your own.  You’re guaranteed to get a better feel for how all of these parts fit together.

Bibliography:

[1] Kernighan and Ritchie, "The C Programming Language", Prentice Hall