Back to ObjectValue Logo articles

         Smalltalk-Like Message Passing in C++

Immo Hüneke

1          Introduction

Much has been written about the advantages of late binding in languages such as Smalltalk-80 and Objective-C. By contrast, C++ employs early binding. The compiler does most of the work of determining which class of object is the target of any function call (function calls in C++ take the place of messages in Smalltalk-80). This makes C++ more suitable for real-time embedded systems, but on occasion the message-passing model can usefully be applied to this kind of system as well.

This paper identifies a situation in which message-passing might be required, outlines a possible approach to the provision of a message-passing facility in C++, and sketches an example of its use.

2          Requirement

A system with a transaction processing flavour is being developed under Unix. A server process embodies a scheduler, which accepts requests over a network, and a variable number of transaction instances. The scheduler creates a new transaction object for each new incoming request. While a transaction proceeds, it may require accesses to databases or files. Depending on the type of transaction, it may send a reply to the requester, which in turn may result in continuation requests and further replies.

The transaction object will create further objects during its lifetime, which must all be deleted when the transaction completes (successfully or not). No transaction object is permitted to block the server process by waiting for the completion of any external action. Thus it may not perform synchronous input/output via the operating system, or await the next continuation request in its dialogue.

3          Analysis

Objects are treated as finite state automata. The set of member functions corresponds to the set of possible events driving the state machine. Every invocation of a member function causes an indivisible action and an associated change of state. An indivisible action is defined as a sequence of statements which can be guaranteed to run to completion. In other words, it requires no synchronisation with external resources.

In the course of an indivisible action, the object is permitted to invoke further functions (which in turn may be treated as events by other objects), provided that those functions themselves guarantee to run to completion. This rules out calls to Unix file I/O routines such as write(...), since they suspend the calling process until the operation is complete.

Instead, the result of an I/O call is treated as a new event coming into the object. The I/O call itself is encapsulated in an API object, which works together with the scheduler to make asynchronous Unix calls possible. This requires that the API object can identify the correct object to notify when an asynchronous I/O completes. Figure 1 illustrates this idea.

15,404)

Figure 1: Model of finite-state objects interacting with Unix I/O

Corollaries from this processing model are:

          Asynchronous forms of every OS call must be provided. Where this turns out to be impossible in native Unix, an array of I/O server processes communicating with clients via shared memory might do the trick.

          For each event coming into an object, the object must store the identity of the object originating the event, if it is eventually to notify that the requested action is complete.

          The standard invocation mechanism for member functions cannot be used in this case, as it does not inform the called object where it was called from.

4          Suggested Solution

Where an object needs to send an event to a second object in such a way that the second object can subsequently send an event back to the first, a message-passing style of function call can be used.

In order to support this message-passing style of invocation, the member function called by the first object must take an argument of class Event (or a subclass thereof specialised to contain the specific parameters needed for the function). Strictly speaking, a pointer to an Event would probably be used for efficiency.

The called object stores the Event pointer somewhere internally, which allows it to send another Event to the calling object later. This requires the calling object to be of a class known to the called object. The class FiniteStateObject is provided to act as the base class for the calling object, so that the called object can invoke its event-handling member functions.

Supposing that an error occurs immediately (such as invalid call arguments or no memory available). In this case, the called function would still have to return as before, but subsequently invoke an error event on the calling object. This can be done if we provide an “alarm clock” object which can be requested to send an Event to any calling object after a specified time (which could be “as soon as possible”). The object which first detects the error would set the alarm before returning, then on receipt of the timer event it calls the invoking object’s error handler.

In the case where an I/O call results in an error which is notified on completion, the invoking object’s error handler will be called in just the same way (unless the API object is able to sort matters out and re-submit the request – this could happen, for instance, if the process caught a signal during an OS call).

5          Example

First we declare the base class from which all events will be derived.

 

class Event {

 

#include <event.hpr> // private & protected ptns

 

public:

   Event (FiniteStateObject* caller);

   virtual ~Event () {}

   FiniteStateObject* caller () {return calledBy;}

};

 

Notice that only those members of Event which are intended to be used by other objects are shown. All others are contained in a header file called event.hpr, so that someone looking at the class declaration need not be concerned with them. However, it may be assumed that there must be a member variable called calledBy which is of type FiniteStateObject*.

The constructor function (in this case there is only one) takes a compulsory argument which is a pointer to the calling object. See below for an example of how it is used. Within the constructor function (which would be defined in another file) it may be possible to insert a check that the object was allocated from the free store (the exact implications of this have not been investigated).

The function caller() can be used to return a pointer to the calling object. The destructor function, ~Event(), is virtual: this means that, although it does nothing, it is called through a function table when invoked. Thus a pointer to any subclass of Event can be stored in a pointer to class Event, and its destructor will still be properly invoked when the pointer is used in a delete call.

Next we make a useable subclass from Event. Subclasses of Event can be built in such a way as to make it easy to provide lots of defaultable parameters. This is a convenience feature which has already been written about at great length (see for instance the X toolkit programmer’s manual, or the SunView programmer’s manual). This is illustrated by the interval parameter for setting a timeout:

 

class SetTimer: public Event {

 

#include <settimer.hpr> // private & protected ptns

 

public:

   SetTimer (FiniteStateObject* caller) : (caller)

     {delay = 0;}

   void interval (Interval dt) {delay = dt;}

   Interval interval () {return delay;}

};

 

Note how the constructor function (which is inline this time) first initialises its superclass (this is the meaning of the argument list directly following the colon), then sets up a default value for the protected member variables. The type Interval is used in this example to measure time, and is probably just a typedef for long int. Otherwise it could be a class which has a conversion defined from integer values. In either case, the assignment of the value 0 to an Interval is syntactically correct.

There are two interval(...) functions, which set and retrieve the timeout value. C++ allows overloading of the function name.

Now let’s assume there is some class MyClass in the system, which is a subclass of FiniteStateObject. Within one of its member functions it needs to set a timer to go off after 15 seconds:

 

MyClass::doSomethingOrOther (...)

{

   ...

   /*

   Now create a SetTimer event initialised with a

   pointer to this object:

   */

   SetTimer* crash_p = new SetTimer (this);

   crash_p->interval (15 * SECONDS);

 

   // Now send the event to the system alarm object:

   theAlarmClock.set (crash_p);

   ...

}

 

And that’s it!

FiniteStateObject would provide a function called (for instance) timeout(...), which theAlarmClock would call when the time was up. Because theAlarmClock does not expect to get called in turn, this could be an ordinary member function not requiring an Event as its argument.

[However, in practice it would be a good idea to make timeout(...) take an Event argument, because then the original SetTimer object could be sent back to the calling object. This in turn would allow the caller to store additional information about the timed-out operation in the SetTimer.]

A slightly different approach would be to provide a single event-handling function in FiniteStateObject, which would use an op-code held within the Event to decide what type of event it was. Although this would correspond more closely to the Smalltalk-80 message-passing mechanism, it would be messy and inefficient, and not in the spirit of C++ at all.

6          Conclusion

The message-passing model can be a useful complement to the more usual member function call model in C++. In the circumstances described, it confers the following advantages:

          It relieves transaction programmers of the need to check for error conditions after every I/O call. This would be done once and for all inside the API object.

          The “normal” flow of control within the code for a class is obvious.

          Cleaning up after errors is localised, which means that

          it is easy to ensure that all the objects involved in a transaction are correctly terminated following an error; and

          the code for handling errors does not intrude into the normal flow of control.

          Its application to a transaction-based server process effectively allows multitasking within a Unix process. This is important for several reasons:

          In a real-time system, the time taken to start and stop Unix processes for each transaction can be prohibitive.

          Access to data common to all transactions is much more straightforward.

          The final routing of request messages can be handled inside the server process. Thus knowledge about the meaning of application-dependent request fields can be hidden within the server.

          Many functions require lots of parameters. The Event class forces defaults to be specified for all of them, which can be overridden by the programmer. The programmer need not be concerned with any parameters whose values are defaulted, while the significance of the values which are specified is always clear. An additional parameter introduced to a library function does not require changes to all the code which calls it.



Created Thu Oct 23 21:55:26 2003

Copyright ObjectValue Ltd.

Back to ObjectValue Logo articles