| iMatix home page
| << | < | > | >>
SMT Logo SMT
Version 2.81

Writing A Native SMT Program

A native program is a agent. To build an agent you:

When you design an agent you must decide whether the it is single-threaded or multithreaded. What's the difference?

By convention, a single-threaded agent creates an unnamed thread when it initialises. A multithreaded agent, by contrast, typically create a new thread for each new connection, and uses a different name for each thread.

An example of a single-threaded agent is the operator console. This is a program that accepts error messages or warnings from other parts of the application, then does something useful with them. (The current implementation writes them to stderr.) The operator console has no need for multiple threads.

An example of a multithreaded agent is the logging agent. This is a program that manages log files on behalf of other application programs. It does this at a low priority, and without blocking, so that log file data is written without disturbing ongoing work. The logging agent can write to several log files in parallel: it does this by having one thread for each log file.

To specify that an agent is single-threaded, define SINGLE_THREADED as TRUE near the start of the program. For instance, this code comes from the SMTOPER agent:

/*- Definitions ------------------------------------------------*/

#define AGENT_NAME      SMT_OPERATOR  /*  Our public name       */
#define SINGLE_THREADED TRUE          /*  Single-threaded agent */

Initialising An agent

An agent program must be 'initialised' before it can do any useful work. For instance, to initialise the logging agent, the application must call the function smtlog init(). Generally, agents are initialised by the stub program, or by other agents. We generally recommend that an agent always try to initialise every agent it requires. It is safe to call initialisation function for an agent several times; only the first call has any effect.

The initialisation function is the only public function for a agent. Once an agent is initialised, it communicates with other programs only via events.

An agent program is based on a Libero dialog, and is 'driven' by a chunk of code generated by Libero. This code (defined as an #include file) handles the initialisation of the agent. The code looks something like this (we explain each part):

if (agent lookup (AGENT_NAME))
    return (0);                 /*  Agent already declared     */

The agent lookup() function returns NULL if an agent object with the specified name already exists. Otherwise it returns a pointer to the agent object. Here we check that the agent has not already been declared. The generated code assumes that AGENT_NAME has been defined to hold the name of the agent. AGENT_NAME can be a variable or a pre-processor macro (the generated skeleton program defines it as a macro).

if ((agent = agent declare (AGENT_NAME)) == NULL)
    return (-1);                /*  Could not declare agent    */

The agent declare() function returns a pointer to the newly-created agent object. If there was an error (e.g. not enough memory), it returns NULL.

#if (defined (SINGLE_THREADED))
agent-> tcb_size    = 0;    /*  No context block            */
agent-> max_threads = 1;    /*    and max. 1 thread         */
#else
agent-> tcb_size    = sizeof (TCB);
#endif

Once the agent has been created, the generated code sets the thread context block (TCB) size depending on whether the agent is single threaded or multithreaded. A single-threaded agent does not need TCBs, so the size is zero. The code assumes that SINGLE_THREADED has been defined as a macro if required. The generated code then sets a variety of fields in the agent block. This allows the SMT kernel to 'drive' the agent program correctly.

This is how a typical multithreaded agent program initialises (this code is taken from smtlog.c):

#define AGENT_NAME  SMT_LOGGING    /*  Our public name         */

The SMT_LOGGING symbol is defined in the standard SMT header file, smtlib.h, since this agent is part of the standard package. For your own agents you would define AGENT_NAME as a string literal. Note that agent names must be unique within the application.

int smtlog_init (void)
{
    AGENT   *agent;             /*  Handle for our agent        */
    THREAD  *thread;            /*  Handle to console thread    */
#   include "smtlog.i"          /*  Include dialog interpreter  */

The initialisation code in smtlog.i assumes that a variable 'agent' is defined. Then, you can refer to the agent in following code using this 'handle'. The first thing we do is to declare each method:

/*                           Method     Event value  Priority  */
    method declare (agent, "SHUTDOWN", shutdown_event,
                                             SMT_PRIORITY_MAX);
    method declare (agent, "OPEN",     open_event,         0);
    method declare (agent, "PUT",      put_event,          0);
    method declare (agent, "CLOSE",    close_event,        0);

Methods names are not case-sensitive, but by convention we specify them in uppercase. Every agent must support the SHUTDOWN method; this is sent to each agent when the SMT kernel terminates (for instance when interrupted). SHUTDOWN gets a high priority, so that an agent will handle shutdown events before any other waiting events. The other events get a normal priority (0 means 'default').

You can define several methods for the same event. The SMT kernel uses the set of methods to translate an incoming external event into an internal dialog event.

    /*  Ensure that operator console is running, else start it  */
    smtoper init ();
    if ((thread = thread lookup (SMT_OPERATOR, "")) != NULL)
        console = thread-> queue-> qid;
    else
        return (-1);

This agent sends error messages to the operator console agent, which is generally a good idea. It initialises the agent (with no effect if the agent is already initialised) and then gets the console queue id, so it can send events to the operator console. Note how we do a thread lookup() with an empty thread name. The operator console is single threaded, and that single thread has no name.

    /*  Signal okay to caller that we initialised okay          */
    return (0);
}

Finally, if everything went as expected, we return 0 to signal that to the calling program. This is a convention, although you can write the initialisation function any way you like, accepting any arguments and returning any value that is appropriate.

The Thread Context Block

Threads can share data: any global data in the agent program is de-facto shared by all threads. Since threads also need 'private' data, each thread owns a block of memory called the Thread Context Block, or TCB. The SMT kernel allocates such a block when a thread is created.

The TCB is a structure that contains arbitrary fields. You define this structure at the start of your program. All code modules in the program receive a pointer to this structure: they use the pointer to reference private data. For instance, this is how the smtlog.c agent declares its TCB:

typedef struct                  /*  Thread context block:       */
{
    int handle;                 /*    Handle for i/o            */
} TCB;
This is how the smtlog.c agent opens a file:
/*******************   OPEN THREAD LOGFILE   ********************/

MODULE open_thread_logfile (THREAD *thread)
{
    char
        *logfile_name;

    tcb = thread-> tcb;         /*  Point to thread's context   */

    /*  Event body or thread name supplies name for log file    */
    logfile_name = (strused (thread-> event-> body)?
                    thread-> event-> body:
                    thread-> name);
    tcb-> handle = lazy creat (logfile_name, S_IREAD | S_IWRITE);
    if (io_completed)
      {
        if (tcb-> handle < 0)   /*  If open failed, send error  */
          {                     /*    to console, and terminate */
            sendfmt (&console, "ERROR",
                     "Could not open %s for output",
                      logfile_name);
            senderr (&console);
            raise exception (exception_event);
          }
      }
}

Choosing Event Names

We generally use the same name for the method as for the event. E.g. CLOSE and close_event. This is not obligatory, and in some cases not appropriate, but it does make the program easier to understand when dialog event names correspond to methods.

Mechanics Of Event Delivery

The agent kernel delivers events to threads when required. This happens at a precise moment: when the thread moves to a new dialog state -- after executing the action module list -- and no internal event was provided.

When a thread moves into a state, the set of possible events is those events defined in that state, plus the events defined in the Defaults state, if any.

The SMT kernel takes the following event (or the event with the highest priority) and tries to match it to a method name. If the event does not match a method, the event is rejected. Otherwise it is accepted and translated into an internal event number. If the internal event is illegal at that moment in the dialog, this causes a fatal dialog error (the thread rejects the event).

Waiting For An External Event

Normally the SMT kernel delivers an external event when the dialog moves to a new state, and no event was specified. In some cases this can make a dialog rather large, since you need to break each step up into states. The event wait() function causes the dialog to halt until an event can be taken from the queue. When several threads are executable, this function also switches execution to the next thread.

The event wait() call sets the variable "the_external_event". This should be the last statement in a dialog module. When used in the last module in a list, it has strictly no effect.

Sending Structured Event Data

Events can optionally have a body to carry additional information. When you send textual data - for instance a string - the event body can be transferred between programs without any type of conversion. (We ignore problems of character-set conversion at present.)

When you need to send several items in one event body, we speak of sending structured data. Structured data consists of a mixture of data items of these types:

An example of events with structured data are those accepted by the socket i/o agent SMTSOCK.

In C, we can group the data items in a structure, hence the term. We cannot, however, simply copy the structure into the message and send that. We cannot copy the address of the structure. Both these methods will work today, but an event may (in the future) be sent to an agent in a different process, perhaps running on a different machine.

Our solution is to take each data field in turn, and pack the structure into a machine-independent stream. We transmit this stream, then do the reverse unpacking in the target program.

To do the conversion we use the SFL functions exdr_read() and exdr_write().

This is more work than just sending the complete structure, but is the only way to ensure that data can safely be sent between two programs that may be running on separate systems.

Sending Events Within An Agent

Within one agent, you do not need to use the EXDR functions. It is quite acceptable to pass data in a structure. To do this,

Ignoring External Events

In some cases, you may want to ignore reply events sent by an agent. This can be useful to simplify a dialog. This is how we declare a method to ignore some specific event:

    method declare (agent, "SOME_EVENT", SMT_NULL_EVENT, 0);

Non-Blocking File Access

The SMT kernel provides a minimum file access layer that is safe to use in multithreaded programs. To understand what this means, first understand what is 'unsafe'.

On some systems, like UNIX and Digital VMS, file access may need resources that are not always available - like memory for buffers. If you ask to read some data from a file, and there is a problem, the operating system may loop a few times - waiting and then trying again - before finally returning to the calling program. In the meantime your program and all threads are blocked.

The general solution is to request non-blocking file access. Then, in the case of a resource problem, the operating will not loop, but will return at once with an error code that means 'try again'. The SMT kernel integrates this solution with its thread management, so that a thread waiting for file access will loop slowly, allowing other threads to continue to run.

To make this work, you should not call the open(), creat(), read(), or write() functions directly in your program. Instead, call the SMT kernel functions lazy open(), lazy creat(), lazy read(), lazy write(), and lazy close(). Furthermore, construct your code like this:

rc = lazy write (tcb-> handle, formatted, fmtsize);
if (io_completed && rc < 0)     /*  If write failed send error  */
  {                             /*    to console and terminate  */
    sendfmt (&console, "ERROR",
             "Could not write to %s", thread-> name);
    senderr (&console);
    raise exception (exception_event);
  }

If io_completed is FALSE, then the code module should do no further work. In this case the SMT kernel automatically re- executes the entire code module.

This is simple and sufficient for sequential file access. If you need heavy database access, where one SELECT statement may take a long time to complete, you'll find that your program responds slowly. A better architecture in such cases is to handle database requests in a separate process, which talks to your application program using sockets. The requests will take the same time to complete, but in the meantime other threads - e.g. handling new connections - can run normally.

While we generally recommend you use the non-blocking i/o functions, there are instances where this is not really necessary. Most obviously, when an application is initialising (e.g. reading configuration files) or terminating (dumping data to a log file), there is no need to avoid blocking i/o. In such cases, you can access sequential files directly.

Real-time Programming

When you call a function like lazy read() and it detects a 'busy' condition, it sets io_completed to FALSE, and automatically re-executes the current code module. You may want to manage this yourself, however. It can also be useful to have a similar looping when you access a socket and receive the EAGAIN error code.

The recycle module() function lets you control this looping explicitly. For example, we can decide to abort a file access after more than RETRY_MAX retries:

typedef struct                  /*  Thread context block:       */
{
    int handle;                 /*    Handle for i/o            */
    int retries;                /*    Number of retries so far  */
} TCB;
Let's assume that 'retries' has been set to zero, either during thread initialisation (okay), or by a specific dialog module (better). We can then try to open a file like this, calling recycle module() with a FALSE argument to prevent looping when we have retried too often:
/********************   OPEN THREAD LOGFILE   *******************/

MODULE open_thread_logfile (THREAD *thread)
{
    tcb = thread-> tcb;         /*  Point to thread's context   */

    /*  Our thread's name is the name for the log file          */
    tcb-> handle = lazy creat
                      (thread-> name, S_IREAD | S_IWRITE);
    if (io_completed)
      {
        /*  If open failed, send error to console, and end      */
        if (tcb-> handle < 0)
          {
            sendfmt (&console, "ERROR",
                     "Could not open %s for output", thread-> name);
            senderr (&console);
            raise exception (exception_event);
          }
      }
    else
      {
        if (++tcb-> retries == RETRY_MAX)
          {
            sendfmt (&console, "ERROR",
                     "Could not open %s for output", thread-> name);
            sendfmt (&console, "ERROR", "Too many retries.")
            raise exception (exception_event);
            recycle module (FALSE);
          }
      }
}

Using Semaphores

A semaphore is an object that you can use to synchronise threads, lock resources, etc. Semaphores are widely- used in multithreaded and parallel computing, and the SMT kernel implements semaphores in a conventional manner. Semaphores have these characteristics:

Replacing The Standard Agents

In realistic projects you will want to replace the operator console agent and perhaps the logging agent with your own, customised versions. One way to do this is to modify the code of the programs we supply. However, this just causes maintenance problems. A better way is to use the existing code as a basis for new agents that use the same agent name (but written as a different source file). Initialise the replacement agent from your main() function. Then, the standard agent will never be initialised. All events normally sent to the standard agent will be sent to your agent instead. Do not try to change the form or meaning of events sent to the standard agents.


| << | < | > | >> iMatix Copyright © 1996-99 iMatix Corporation