OSE - C++ Library User Guide

Graham Dumpleton
Dumpleton Software Consulting Pty Limited
PO BOX 3150
Parramatta, 2124
N.S.W, Australia
email: grahamd@nms.otc.com.au

Table of Contents

Event Driven Systems
OSE - C++ Library User GuideEvent Driven Systems

Event Driven Systems

1 Concurrent Tasks

For some applications, it is necessary for concurrent tasks to be executing. Due to the minimal amount of data associated with the tasks, or the need to share data between tasks, the use of separate heavyweight processes is not always viable. One solution is to use threads, a feature provided by the operating system for executing multiple tasks within the one process. The writing of code which executes safely within a threaded environment is difficult. Also, not all operating systems provide threads and where they do, the implementations often differ, making portability of code a problem.

A solution often adopted is to write the application in the style of an event driven system. That is, something happening and to which a task should respond, is modelled as an event. The event is put together by a central thread of control, with the event and thread of execution being handed over to the appropriate task. It is now the tasks job to deal with that event and subsequently return the thread of execution back to the executive or dispatcher.

In an event driven system, a task, when allowed to run, must return control when it no longer requires it or when it can not precede further. That is, the task cannot perform an operation which would cause execution to block within that task. If the task was half way through an operation and was waiting on more data, it would need to remember where it was and return. When the data it was waiting on arrived it would then continue from where it had previously stopped. The thread of execution must be given back to the dispatcher in order to allow other tasks to run and deal with their own events. The tasks therefore must cooperate with each other to ensure that all are able to get through their respective jobs.

2 Simulation Systems

At a lower level, an event driven system can also be viewed as being a form of simulation system. In simulation systems, there is a list of jobs to be performed. The central executive or dispatcher retrieves the first job from the list and executes the code associated with the job. The code must eventually return so that succeeding jobs in the list can be executed. If it was not possible to finish an operation, the code for that job would add a new job to the end of the list of jobs such that the operation could be completed at a later time.

The main difference with an event driven system is that the central executive or dispatcher must be aware of a number of primary event sources. When the dispatcher goes to retrieve the next job to execute, it must also check the event sources and if an event is waiting, create a job for the delivery of the event to the appropriate task.

In a simulation system, when there are no more jobs to execute the program would terminate. For an event driven system, while ever any of the primary event sources are active and tasks are interested in events from those sources, the process will continue to execute. This means that if there were no jobs to execute in the list of jobs, the process would block and wait for an event to arrive. As the process itself is blocked, those events will of necessity be the result of some stimulus from the operating system. For example, a timer or alarm expiring, a signal being directed at the process, or data being ready for the process to read.

3 Jobs and The Dispatcher

The framework provided in the library can be used to implement both simulation and event driven systems. To build a simulation system, the classes you need to be concerned with are OTC_Dispatcher and OTC_Job. The simplest example of a simulation system is one in which only one type of job exists. Take for example a job which is continually executed until a predefined condition is met.

  class Job1 : public OTC_Job
{
public:
void execute();
private:
static int count;
};

int Job1::count = 0;

void Job1::execute()
{
if (++count != 10)
OTC_Dispatcher::schedule(new Job1);
cout << count << endl;
}

main()
{
OTC_Dispatcher::initialise();
OTC_Dispatcher::schedule(new Job1);
OTC_Dispatcher::run();
return 0;
}
The first step which is necessary is to initialise the dispatcher by calling the static member function of OTC_Dispatcher called `initialise()'. This will create the internal job queue on which jobs are kept and set the state of the dispatcher to that of being idle. Note that the dispatcher should only be initialised once `main()' has started executing. The dispatcher should not be initialised as a result of code which is run due initialisation of a static object.

The next step is to schedule any jobs which are to be executed by calling the static member function of OTC_Dispatcher called `schedule()' with a pointer to the job object as argument. Jobs should only be scheduled after the dispatcher has been initialised. Any jobs scheduled should be created on the free store using `operator new()'. This is necessary as the dispatcher will take responsibility for ensuring that jobs are destroyed once they have finished executing. How to override this and control when jobs are destroyed will be detailed later.

Once all jobs to be executed have been scheduled, the dispatcher is run by calling the static member function `run()' of the OTC_Dispatcher class. While there are still jobs on the job queue, the dispatcher will take the first job from the queue and call the `execute()' member function. When there are no more jobs to run, the `run()' function will return, allowing the program to terminate.

4 Execution of Jobs

When a job is run the `execute()' member function is called. To actually perform work, your derived version of OTC_Job must redefine the `execute()' function to do what you desire. As the `execute()' function is called in a totally different scope to where the job object is created, the job object will need to contain everything that it needs. Alternatively, what it needs must be accessible as global data.

As the job object is transient, that is, it will be deleted once the `execute()' function returns, it is unlikely that the job object will be the owner of the data being operated upon. Instead, the job object will generally act purely as a means of starting off a series of operations in some object having a lifetime much longer than that of the job object.

If the `execute()' function cannot complete the job and needs to continue with it at a later time, a new job will need to be scheduled with the dispatcher. The new job may need to be passed state information as to where the first job object stopped. An alternative is to reschedule the same job object. In this case though, it is necessary to prevent the dispatcher from destroying the job object while it is still required. This is done by overriding the `destroy()' member function of the OTC_Job class.

  class Job2 : public OTC_Job
{
public:
void execute();
void destroy();
private:
static int count;
};

int Job2::count = 0;

void Job2::execute()
{
if (++count != 10)
OTC_Dispatcher::schedule(this);
cout << count << endl;
}

void Job2::destroy()
{
if (count == 10)
delete this;
}
As is, there is no way to stop a job from executing once it has been scheduled. If this attribute is needed, your job object will need to keep a flag to indicate whether it is to be run. The `execute()' function should check this flag and subsequently run or not run.

  class Job3 : public OTC_Job
{
public:
Job3() : runnable(OTCLIB_TRUE) {}
void kill() { runnable = OTCLIB_FALSE; }
void execute();
void destroy();
private:
static int count;
OTC_Boolean runnable;
};

int Job3::count = 0;

void Job3::execute()
{
if (runnable)
{
if (++count != 10)
OTC_Dispatcher::schedule(this);
cout << count << endl;
}
}

void Job3::destroy()
{
if (!runnable || count == 10)
delete this;
}
The existence of the `kill()' member function presumes that something has kept a pointer to the job object in order that it can be called. To ensure that a pointer to the job object is not kept after it has been deleted, the `execute()' function would need to do whatever was necessary to ensure that whatever kept the pointer threw it away when the job had completed. It is presumed that if `kill()' was called, that the caller would throw away the pointer as it would know that it would no longer be valid.

Note that the code above will not result in the job object being destroyed when the `kill()' function being executed. Instead, the job object will remain in the job queue until it becomes the first job in the queue. At that point the dispatcher will execute the job by calling the `execute()' function, however nothing would be done and the function return. The job object will subsequently be destroyed when the dispatcher calls the `destroy()' member function.

Leaving the job object in the queue and only destroying it when it becomes the first job, avoids having to traverse a list of jobs to remove it. Leaving cancellation of jobs up to the user for those cases where it is necessary, also simplifies the code needed to implement jobs for the case where cancellation isn't required.

A further case of where `destroy()' could be overloaded, is to allow jobs to exist in extents other than on the free store, or to implement a reference counting mechanism for job objects. Both cases can be useful where a job is used many times and may even appear in the job queue more than once. If though a job object were to appear more than once in the queue, it would not be able to hold any data which needed to be different between each schedule job. You also would not be able to add the ability to cancel individual jobs.

  class Job4 : public OTC_Job
{
public:
void execute();
void destroy();
private:
static int count;
};

int Job4::count = 0;

void Job4::execute()
{
if (++count != 10)
OTC_Dispatcher::schedule(this);
cout << count << endl;
}

void Job4::destroy()
{
// Nothing to do.
}

main()
{
static OTC_Job4 job;
OTC_Dispatcher::initialise();
OTC_Dispatcher::schedule(&job);
OTC_Dispatcher::schedule(&job);
OTC_Dispatcher::schedule(&job);
OTC_Dispatcher::run();
return 0;
}

5 Stopping the Dispatcher

While the dispatcher is running, the static member function of OTC_Dispatcher called `isRunning()' will return `OTCLIB_TRUE'. If the dispatcher had run out of jobs and returned, a value of `OTCLIB_FALSE' would be returned if `isRunning()' were subsequently called.

If it is necessary to make the dispatcher return while there are still jobs in the queue to execute, the static member function `stop()' of the OTC_Dispatcher class should be called. This will cause the `run()' member function to return as soon as the job in which `stop()' was called had returned from the `execute()' function.

The `stop()' member function would be used where it was necessary to cleanly exit from a program when the desired result had been achieved, or some error had been encountered which prevented further processing. If the dispatcher is stopped prematurely, the `run()' member function will return a value of `-1'. On normal termination, the `run()' member function would have returned `0'.

6 Event Based Systems

The facilities of OTC_Dispatcher and OTC_Job are extended by the classes OTC_Event and OTC_EVAgent to provide support for event based systems. The class OTC_Event is the base class for all the different types of events which may be needed. The OTC_Event class provides an interface for implementing a minimal run time type identification system. This allows different types of events to be distinguished when they are received.

Events may be created and sent from anywhere within an application. The recipient of an event is referred to as an agent. To receive events, agents must derive from the class OTC_EVAgent. The OTC_EVAgent class provides a means of identifying each agent and an abstract interface for passing events to the derived class.

A range of predefined events are provided with the library. The simplest of these is the OTCEV_Action class. This event is used as a means of indicating to an agent that its turn to run has come. This event is useful where an operation is to be performed in steps which potentially must interleave with the delivery of other events. For example:

  class Agent1 : public OTC_EVAgent
{
public:
Agent1() : count(0) {}
void handle(OTC_Event* e);
private:
u_int count;
};

void Agent1::handle(OTC_Event* e)
{
if (e->type() == OTCEV_Action::typeId())
{
count++;
cout << count << endl;
if (count < 10)
{
e->queue(id());
return;
}
}
e->destroy();
}

main()
{
Agent1 a;
OTC_Dispatcher::initialise();
OTCEV_Action* e = new OTCEV_Action;
e->queue(a.id());
OTC_Dispatcher::run();
return 0;
}

7 Agents and Event Delivery

Any class which needs to be able to receive events must derive from the class OTC_EVAgent. Such derived classes are referred to as an agent. When an agent is created, it is allocated a unique ID. The agent is then added to a table consisting of all active agents. The table exists to allow a pointer to an agent to be retrieved using its unique ID as a key. When an agent is destroyed it will automatically be removed from the table of active agents.

Sending an event to an agent can be carried out in a number of ways. The first is to use the `queue()' member function of the OTC_Event class.

  Agent1 a;
OTC_Event *e;
e = new OTCEV_Action;
e->queue(a.id());
The `queue()' member function takes as argument the unique ID for the agent to which the event should be delivered. The result of the function is to create a job for the delivery of the event to the agent and add the job to the dispatchers job queue. The event will be delivered to the agent after any jobs already present in the dispatchers job queue have been run.

If it was necessary for the event to be delivered immediately to the agent, bypassing the dispatchers job queue, the `deliver()' member function should be used. The `deliver()' member function takes as argument either the unique ID for an agent, or a pointer to the agent. The latter type of argument is more efficient as it avoids the need to map the unique ID of the agent into a pointer to the agent. If using a pointer to the agent however, you must know that it still exists.

  Agent1 a;
OTC_Event* e;
e = new OTCEV_Action;
e->deliver(&a);
In all cases, to deliver an event to an agent, the request is made to the event object. The event object will use whatever mechanism is appropriate to ensure it arrives. When the event does arrive, it is passed to the agent through the `handle()' member function. An agent must override the `handle()' member function to interpret the events.

  void Agent1::handle(OTC_Event* e)
{
if (e->type() == OTCEV_Action::typeId())
{
count++;
cout << count << endl;
if (count < 10)
{
e->queue(id());
return;
}
}
e->destroy();
}
The job of the agent is to ascertain if an event it receives is an event which it was expecting. If the event was unexpected it should be destroyed by calling the `destroy()' member function. It may also be appropriate to print a diagnostic message for the case of an unexpected event.

To determine the type of an event the run time type identification system built into each event type should be used. To get the identifier for the type of an event, the `type()' member function is used. The type identifier should then be compared to that which was expected. The type identifier of the expected event is obtained by calling the static member function `typeId()' of the appropriate event class.

If an event contains data, it will be necessary to perform a downcast from the type OTC_Event to the true type of the event. As downcasting may be necessary, a type derived from OTC_Event should never use virtual inheritance.

When the event is no longer required it should be destroyed by calling the `destroy()' member function. If the event is to be reused, through forwarding it onto a different agent, or arranging for it to be delivered back to the same agent, it would not be destroyed. Only when the event is no longer needed should it be destroyed.

8 Identification of Events

When creating a new type of event, you must provide member functions to allow the type of the event to be determined. The first of these is a static member function named `typeId()'. This function must return a pointer of type `void*', the value of which is unique to that type of event. A suggested implementation is to return the address of a static member variable, or a static which is local to that function.

The second member function which needs to be added, overrides the `type()' member function defined in the OTC_Event class. This function should call `typeId()' to return the unique identifier for that type of event.

  class Event1 : public OTC_Event
{
public:
void* type() const;
static void* typeId();
};

void* Event1::type() const
{
return typeId();
}

void* Event1::typeId()
{
static int dummy = 0;
return &dummy;
}
To identify this event from the `handle()' member function of an agent, you would now write:

  if (e->type() == Event1::typeId())
...
Note that this mechanism does not support having hierarchies of events. Each event class needs to inherit directly from OTC_Event. If you wish to create event hierarchies you will need to use some intermediate class which encapsulates a method for determining if an event is a derived version of some other event.

9 Non Delivery of Events

When the unique ID for an agent is used to queue an event for delivery to an agent, or used to immediately deliver an event to an agent, and the agent no longer exists, the event will not be able to be delivered. When this occurs, the delivery mechanism will destroy the event. Prior to destroying the event a diagnostic message will be displayed via the log facility.

The diagnostic message will contain a brief description giving the event type and the ID of the agent to which it was destined. The description of the event is provided by the `dump()' member function. When defining a new event, you will need to override this member function to display information appropriate to your event. For example:

  void OTCEV_IOEvent::dump(ostream& outs) const
{
outs << "<OTC> IOEVENT - fd = " << fd()
<< ", events = "<< events();
}
When an event cannot be delivered, the `cancelSource()' member function will also be called. The member function will be passed the ID of the agent to which the event was to be delivered. If necessary, this member function should be overridden to undertake whatever is necessary to ensure that the event source where the event originated, is notified that it shouldn't send further events to that agent.

  void OTCEV_IOEvent::cancelSource(int theAgentId)
{
OTCEV_IOEvent::unsubscribeAgent(theAgentId);
}

10 Cloning of Events

The recipient of an event may at times need to pass the event onto additional agents. If the event needs to be passed on to more than one agent the event will need to be cloned. An event can be cloned by calling the `clone()' member function.

  void Agent2::handle(OTC_Event* e)
{
for (int i = 0; i<nagents; i++)
{
OTC_Event* ce = e->clone();
ce->queue(agents[i]);
}
e->destroy();
}
By default. the `clone()' and `destroy()' member functions implement a reference counting mechanism. This ensures that the event will only be deleted when the number of calls to `destroy()' has exceeded the number of calls to `clone()'. The `destroy()' member function needs to be called an additional time to account for the original object.

If an event object contains data which is modifiable by the recipient of the event, it will be necessary to override the implementation of the `clone()' function such that it creates and returns a copy of the object. If this is not done, one agent could modify the data associated with an event before other agents had an opportunity to read it.

  OTC_Event* Event2::clone() const
{
return new Event2(*this);
}

11 Preventing Event Delivery

When the `queue()' member function is used to forward an event to an agent, it is not delivered straight away, but a job to carry out its delivery is added to the job queue of the dispatcher. The event will only be delivered after all existing jobs in the dispatchers job queue have been executed. Once queued, there is no way to prevent the delivery of that event.

If the delay in an event being delivered and the inability to prevent it from being delivered once queued is a problem, it may be more appropriate to use the `deliver()' member function. The `deliver()' member function ensures that the event is delivered immediately, avoiding the need to be able to prevent it from being delivered after it is queued.

Whether you can prevent delivery of an event will depend on whether the event is a stand alone object which you are responsible for creating, or whether it originates from an event source. As an example, the OTCEV_Action class provides an interface which negates you having to create an instance of the event in order to send it to an agent.

  Agent1 a;
OTCEV_Action::schedule(a->id());
The benefit of using the static member function `schedule()' is that creation of the event object is hidden. In addition, the ID returned by the `schedule()' function can be captured and used to later prevent the event from being delivered. It is also possible to prevent all instances of this type of event from being delivered to a particular agent.

  int id = OTCEV_Action::schedule(a->id());

// cancel single event
OTCEV_Action::cancel(id);

// cancel all events for agent
OTCEV_Action::cancelAgent(a->id());
To ascertain if an event has been delivered, the static member function `active()' can be used.

  int id = OTCEV_Action::schedule(a->id());
if (OTCEV_Action::active(id))
...

12 Real Time Systems

The OTC_EVAgent and OTC_Event classes serve only to extend the ability of the dispatcher to implement simulation systems. In order to support what is commonly called a real time system, the dispatcher needs to be extended so as to be aware of event sources corresponding to real time aspects of the operating system. Examples of these event sources are alarms, timers, signals and i/o events.

Extending the functionality of the dispatcher is achieved by replacing the job queue. This is possible as the job queue is not just a list of jobs, but provides an encapsulation of the operations for adding a new job to the queue and retrieving the first job in the queue. The name of the default job queue is the class OTC_JobQueue.

The job queue is extended by overriding in a derived class the `next()' member function. This member function is used to obtain the next job to be executed. The dispatcher is then made to use the derived class instead of OTC_JobQueue. The name of the derived class which is provided and which is aware of the real time aspects of the UNIX operating system is OUX_JobQueue. To have the dispatcher use this class, it is initialised as follows.

  main()
{
OUX_JobQueue* queue;
queue = new OUX_JobQueue;
OTC_Dispatcher::initialise(queue);
...
OTC_Dispatcher::run();
return 0;
}
In the OUX_JobQueue class, when the `next()' member function is called, the various sources of real time events will be checked. If any events are available a job corresponding to the delivery of that event will be returned. If no events are available at that time, the job queue will go into a mode whereby it will return each of the jobs in the job queue held by the OTC_JobQueue base class.

When the job queue is empty, checking for real time events will recommence. If new jobs were added while in the mode where the job queue was being emptied, they would not be handled at that time, but would be returned the next time the job queue was being emptied. If there were no jobs in the job queue, the `next()' member function would block until a real time event occurs.

Note that the description above has been simplified. In reality, certain real time events will pre-empt the return of a job from the job queue when it is being emptied. Within the real time events, there also exists a priority as to which types of events are dealt with before others. Finally, due to the necessity to deal with real time events before other jobs, jobs in the job queue have been demoted to the point that they are only run when there is nothing else to do. Although, when run, they will all be run before returning to check for all real time events.

To be notified of any of the real time events, you need to subscribe to that type of event through its appropriate class. The event classes corresponding to the supported real time events are OTCEV_Alarm, OTCEV_Timeout, OTCEV_IOEvent and OUXEV_Signal.

13 Alarms and Timers

Alarms and timers provide a means of receiving an event notification at some time in the future. Subscribing to an alarm is carried out through the OTCEV_Alarm class. The OTCEV_Alarm class is also the class which will be received by an agent indicating that an alarm has been triggered. In subscribing to an alarm, you specify the absolute time at which you wish to receive the event. The time is expressed in seconds since January 1, 1970.

  class Agent2 : public OTC_Agent
{
public:
Agent2();
~Agent2();
private:
void handle(OTC_Event* e);
int aid;
};

Agent2::Agent2()
{
struct tm t;
t.tm_day = 25;
t.tm_mon = 12;
t.tm_year = 1994;
t.tm_sec = 0;
t.tm_min = 0;
t.tm_hour = 0;

time_t s = localtime(&t);
aid = OTCEV_Alarm::set(id(),s);
}

void Agent2::handle(OTC_Event* e)
{
if (e->type() == OTCEV_Alarm::typeId())
{
OTCEV_Alarm* ae = (OTCEV_Alarm*)e;
if (ae->alarm() == aid)
{
cout << "Merry Christmas" << endl;
aid = 0;
}
}
e->destroy();
}

Agent2::~Agent2()
{
if (aid != 0)
OTCEV_Alarm::cancel(aid);
}
When an alarm subscription is made, the unique ID for the agent to which the event should be delivered and the time the alarm is to expire, must be supplied. Subscriptions are made by calling the static member function `set()'. The value returned by `set()' is the ID for that alarm. This ID can be used to cancel the alarm by calling the static member function `cancel()'. All alarms destined for a specific agent can be cancelled by calling the static member function `cancelAgent()' with the ID of the agent.

A value of `0' is never returned as an alarm ID and therefore can be used to indicate that no alarm is pending. By calling the static member function `active()' you can determine if an alarm is still to be triggered. The argument to `active()' should be the alarm ID.

Alarms allow you to specify an absolute time, with the unit of time being a second. If you need to specify time periods of less than a second, a timer should be used. Subscription to timers is carried out through the OTCEV_Timeout class. For timers, the time at which event notification should occur is expressed as a relative time, the unit of time being milliseconds.

For example, to request that an event be sent to an agent half a second in the future, you would write:

  OTCEV_Timeout::start(a->id(),500);
As for alarms, an ID is returned by the static member function `start()'. This ID can be used to cancel the timer before it has expired. All timers associated with an agent may also be cancelled.

14 I/O Events

An i/o event is used when you wish to be notified that an event has occurred on a file descriptor. The types of i/o event you can subscribe to are input ready, output ready and the existance of priority data. The class for subscribing to i/o events is OTCEV_IOEvent. The static member function `subscribe()' is called to subscribe to the file descriptor. To subscribe to the different i/o event types, you use the values OTCLIB_POLLIN, OTCLIB_POLLOUT and OTCLIB_POLLPRI. These values are bit values and should be bit or'd together when you are interested in more than one type of i/o event. Alternatively, two separate subscriptions can be made. The agent to which the event should be delivered and the file descriptor must also be supplied.

  OTCEV_IOEvent::subscribe(a->id(),fd,OTCLIB_POLLIN);
OTCEV_IOEvent::subscribe(a->id(),fd,OTCLIB_POLLOUT);
To unsubscribe from a file descriptor you use the ID of the agent in conjunction with the file descriptor. You may optionally indicate which type of i/o event on the file descriptor you wish to unsubscribe.

  OTCEV_IOEvent::unsubscribe(a->id,fd,OTCLIB_POLLOUT);
It is also possible to unsubscribe from a file descriptor using just the file descriptor.

  OTCEV_IOEvent::unsubscribeFd(fd);
All file descriptors in which an agent is interested may be also be unsubscribed.

  OTCEV_IOEvent::unsubscribeAgent(a->id());
When an event for a file descriptor is delivered, the file descriptor and the type of i/o event will be encapsulated into the event. If more than one i/o event for a file descriptor occurs, only one event object will be delivered. You will need to decode the variable giving the type of i/o event to determine which occurred.

  void Agent3::handle(OTC_Event* e)
{
if (e->type() == OTCEV_IOEvent::typeId())
{
OTCEV_IOEvent* ioe = (OTCEV_IOEvent*)e;
cout << ioe->fd() << endl;
if (ioe->events() & OTCLIB_POLLPRI)
...
if (ioe->events() & OTCLIB_POLLIN)
...
}
e->destroy();
}
If dealing with multiple i/o events, a check may need to be done for succeeding i/o event within that object to determine if processing should occur. For example, one i/o event may result in the file descriptor being closed, obviating the need to process the other i/o events within that event object.

  int cont = 1;
if (ioe->events() & OTCLIB_POLLPRI)
{
...
if (...)
{
close(ioe->fd());
cont = 0;
}
}
if (cont && (ioe->events() & OTCLIB_POLLIN))
...

15 UNIX Signals

As a signal can interrupt a program at any point, it is generally not safe to try and access any data structures from a signal handler, or even attempt to create new objects. The OUXEV_Signal class makes handling signals easier as it converts signals into events. The event for a signal would then be dealt with outside the context of a signal handler and therefore you are not restricted as to what you can do in response to that signal. To register interest in a signal the static member function `subscribe()' is called with the ID of the agent and the signal number. Multiple agents may subscribe to the one signal. Signals may be unsubscribed using the ID for the agent and the signal number. All signal subscriptions for an agent may also be cancelled.

  Agent5::Agent5()
{
OUXEV_Signal::subscribe(id(),SIGNINT);
OUXEV_Signal::subscribe(id(),SIGUSR1);
}

void Agent5::handle(OTC_Event* e)
{
if (e->type() == OUXEV_Signal::typeId())
{
OUXEV_Signal* se = (OUXEV_Signal*)e;
if (se->signal() == SIGINT)
OTC_Dispatcher::stop();
else if (se->signal() == SIGUSR1)
...
}
e->destroy();
}

Agent5::~Agent5()
{
OUXEV_Signal::unsubscribeAgent(id());
}

16 Integration with GUI's

Graphical user interfaces typically expect you to be using the dispatcher supplied as part of the GUI. To use these GUI products a version of the OTC_JobQueue needs to be written which extracts information from the GUI about real time event sources it is interested in and use that in combination with interest expressed through the event classes in the library. Unfortunately GUI libraries tend not to make available the information required.

In order to use the dispatcher with the TK GUI library, patches are provided for the TK dispatcher so that the necessary information can be obtained. Provided the patches are applied and the OTKLIB library installed, a version of `wish' can be built which uses the OSE dispatcher. To do this, at the start of the `main()' for `wish' add:

  OTK_JobQueue* queue;
queue = new OTK_JobQueue;
OTC_Dispatcher::initialise(queue);
Instead of invoking `TkMainLoop()' the following call should be made.

  OTC_Dispatcher::run();
Full integration with TK has been achieved. You are not limited as to the features of TK which you can use.

For the X Intrinsic library, it is not realistic to expect that the library be patched. For this reason full integration with Xt is not possible. As many GUI packages such as Motif rely on the Xt dispatcher those libraries are also affected.

What is possible is to create an agent representing the connection to the X display. The agent will subscribe to the file descriptor used for the X display and whenever there is data waiting on the file descriptor it will call the appropriate Xt routines to process the data and flush out any message queues. The code for this agent is as follows.

  class XtAgent : public OTC_Agent
{
public:
XtAgent(
XtAppContext theAppContext,
Display* theDisplay
);
~XtAgent();
protected:
void handle(OTC_Event* theEvent);
private:
XtAppContext myXtAppContext;
Display* myDisplay;
};

XtAgent::XtAgent(
XtAppContext theAppContext,
Display* theDisplay
)
: myXtAppContext(theAppContext),
myDisplay(theDisplay)
{
OTCEV_IOEvent::subscribe(id(),
myDisplay->fd,OTCLIB_POLLIN);
}

XtAgent::~XtAgent()
{
OTCEV_IOEvent::unsubscribe(id(),myDisplay->fd);
}

void XtAgent::handle(OTC_Event* theEvent)
{
if (theEvent->type() == OTCEV_IOEvent::typeId())
{
OTCEV_IOEvent* theIOEvent = (OTCEV_IOEvent*)theEvent;
if (theIOEvent->fd() == myDisplay->fd
&& theIOEvent->events() == OTCLIB_POLLIN)
{
while (XtAppPending(myXtAppContext) != 0)
XtAppProcessEvent(myXtAppContext, XtIMAll);
XFlush(myDisplay);
}
}
theEvent->destroy();
}
As full integration cannot be accomplished, any interest in timers and file descriptors registered through the routines `XtAddInput()' and `XtAddTimeout()' are not handled correctly. Although the Xt events for these event sources will get delivered, they will not always be delivered at the time they occur. The reason for this is that the events will only be processed and delivered when the Xt routines are called to handle data ready for reading from the X display.

The only known consequence of this is that in Motif, holding down a button for an extended time on a scroll down arrow, will not result in continuous scrolling. Moving the mouse around while the button is held down will result in scrolling occurring as the mouse motion will result in X events being sent from the X display to the application, triggering the XtAgent class and the processing of all Xt events which are pending.

17 Custom Interfaces

The interfaces comprising the dispatcher and the means of subscribing to real time event sources are spread across numerous classes. This approach makes it more open and allows for easy extension, however the number of classes and style of interface may not be to personal preferences.

The benefit of the open style of interfaces is that customised interfaces can easily be built on top. This has allowed in the past, software from two different groups which each provided their own object oriented framework for handling real time events to be relatively easily integrated. One groups was using a modified version of the InterViews dispatcher, the other group was using a home grown dispatcher.

The solution was that the internal implementations of each dispatcher were removed and the interfaces bolted on top of the dispatcher provided in the library. This allowed each group to continue to use their own interfaces for subscribing and responding to real time events.

In the case of the InterViews style of interfaces, the user only had to know about two classes. The first class was IOHandler. This class derived from OTC_EVAgent and mapped events received by the `handle()' member function to individual virtual function calls. An implementation similar to that which was used is given below.

  class IOHandler : private OTC_Agent
{
friend class Dispatcher;
public:
virtual int inputReady(int fd);
virtual int outputReady(int fd);
virtual int exceptionRaised(int fd);
virtual int errorRaised(int fd);
virtual int signalRaised(int sig);
virtual void timerExpired(int id);
virtual void alarmTriggered(int id);
protected:
IOHandler() {}
void handle(OTC_Event* theEvent);
};

int IOHandler::inputReady(int fd) { return -1; }
int IOHandler::outputReady(int fd) { return -1; }
int IOHandler::exceptionRaised(int fd) { return -1; }
int IOHandler::errorRaised(int fd) { return -1; }
int IOHandler::signalRaised(int sig) { return -1; }
void IOHandler::timerExpired(int id) { }
void IOHandler::alarmTriggered(int id) { }

void IOHandler::handle(OTC_Event* theEvent)
{
if (theEvent->type() == OTCEV_IOEvent::typeId())
{
OTCEV_IOEvent* theIOEvent;
theIOEvent = (OTCEV_IOEvent*)theEvent;
int fd = theIOEvent->fd();
int events = theIOEvent->events();
int result = 0;

if (events & OTCLIB_POLLIN)
result = inputReady(fd);
if (result != -1 && events & OTCLIB_POLLOUT)
result = outputReady(fd);
if (result != -1 && events & OTCLIB_POLLPRI)
result = exceptionRaised(fd);
if (result != -1 && events &
(OTCLIB_POLLERR | OTCLIB_POLLHUP | OTCLIB_POLLNVAL))
result = errorRaised(fd);
if (result == -1)
OTCEV_IOEvent::unsubscribe(id(),theIOEvent->fd());
}
else if (theEvent->type() == OTCEV_Timeout::typeId())
{
OTCEV_Timeout* theTimeout;
theTimeout = (OTCEV_Timeout*)theEvent;
timerExpired(theTimeout->timer());
}
else if (theEvent->type() == OTCEV_Alarm::typeId())
{
OTCEV_Alarm* theAlarm;
theAlarm = (OTCEV_Alarm*)theEvent;
alarmTriggered(theAlarm->alarm());
}
else if (theEvent->type() == OUXEV_Signal::typeId())
{
OUXEV_Signal* theSignal;
theSignal = (OUXEV_Signal*)theEvent;
int result;
result = signalRaised(theSignal->signal());
if (result == -1)
OUXEV_Signal::unsubscribe(id(),theSignal->signal());
}
theEvent->destroy();
}
The second class was the Dispatcher class. This class was the interface for subscribing to all the different types of real time events. It also provided functions for starting the dispatcher.

  class Dispatcher
{
public:
void link(
int theFd,
the theEvent,
IOHandler* theHandler
);
void unlink(int theFd);
void unlink(IOHandler* theHandler);
void subscribe(int theSig, IOHandler* theHandler);
void unsubscribe(int theSig, IOHandler* theHandler);
int startTimer(
long theSec,
long theUSec,
IOHandler* theHandler
);
void stopTimer(int id);
int setAlarm(long theTime, IOHandler* theHandler);
void cancelAlarm(int id);

void dispatch();
static Dispatcher& instance();

private:
static Dispatcher* _instance;
};

Dispatcher* Dispatcher::_instance = 0;

void Dispatcher::link(
int theFd,
int theEvent,
IOHandler* theHandler
)
{
OTCEV_IOEvent::subscribe(theHandler->id(),theFd,theEvent);
}

void Dispatcher::unlink(int theFd)
{
OTCEV_IOEvent::unsubscribeFd(theFd);
}

void Dispatcher::unlink(EX_IOHandler* theHandler)
{
OTCEV_IOEvent::unsubscribeAgent(theHandler->id());
}

void Dispatcher::subscribe(
int theSig,
IOHandler* theHandler
)
{
OUXEV_Signal::subscribe(theHandler->id(),theSig);
}

void Dispatcher::unsubscribe(
int theSig,
IOHandler* theHandler
)
{
OUXEV_Signal::unsubscribe(theHandler->id(),theSig);
}

int Dispatcher::startTimer(
long theSec,
long theUSec,
IOHandler* theHandler
)
{
long period = theSec*1000 + theUSec/1000;
return OTCEV_Timeout::start(theHandler->id(),period);
}

void Dispatcher::stopTimer(int id)
{
OTCEV_Timeout::cancel(id);
} int Dispatcher::setAlarm(
long theTime,
IOHandler* theHandler
)
{
return OTCEV_Alarm::set(theHandler->id(),theTime);
}

void Dispatcher::cancelAlarm(int id)
{
OTCEV_Alarm::cancel(id);
}

void Dispatcher::dispatch()
{
OTC_Dispatcher::run();
}

Dispatcher& Dispatcher::instance()
{
if (_instance == 0)
{
OTC_JobQueue* theJobQueue;
theJobQueue = new OUX_JobQueue;
OTC_Dispatcher::initialise(theJobQueue);

_instance = new EX_Dispatcher;
}
return *_instance;
}
One benefit of this interface was that because the static member function `instance()' had to be used to get at the dispatcher in order to perform subscriptions, the dispatcher could be created and initialised the first time it was used. This avoided the restriction of the lower level interface that the dispatcher couldn't be initialised and certain subscriptions made until after `main()' had started executing.