Extending C++ for Distributed Applications

One approach to implementing groupware

Patrick Suel

Patrick holds advanced degrees in theoretical physics and computer science. He works at ILOG Inc. and can be reached at suel@ilog.com.


In recent years, networking has completely revolutionized organizations by allowing workers in different locations to share and access information. The main problem now is to make sure that this valuable information stays consistent and can be easily manipulated by multiple applications. A groupware situation exists whenever a piece of information (that is, an object) is manipulated by two actors (applications, processes, or other objects) at the same time. This kind of application integration is only now gaining support in the form of development tools for groupware. In this article, we'll explore issues relating to groupware development and deployment, and describe ILOG Server, a tool that enables the development of dynamic servers of C++ objects. Given an appropriate object request broker, these servers can be distributed across a network in a transparent manner.

ILOG Server implements a system that, among other things, automatically manages object integrity, allows the programmer to define constrained values for structures, provides a facility for computing cross-references on structures, and provides a notification mechanism for structures that ensures the consistency of views. The major constraint in using ILOG Server is its restriction to C++ as the sole implementation language. This is because ILOG Server is implemented as an extension to the C++ language via a preprocessor that generates portable, standard C++ code. Presently, ILOG Server runs on a range of UNIX platforms, as well as Windows, Windows NT, and OS/2.

The Example Application

To illustrate the concept of distributed groupware, we're presenting an application that simulates the visualization of air traffic in the United States. This application can display various airlines, routes (which we call "lines"), flights, and airports. The application is partitioned into four types of processes:

In Figure 1, for example, a line (a route) can be represented simultaneously in three different ways: as an arrow between airports on graphical maps, as a textual entry in the table of lines of the airline, and as a whole table containing the line's flights.

Eliminating an arrow in the airline map will therefore trigger multiple actions: removal of the corresponding arrow from the agency map, removal of an entry from the table of airlines, closure of the list of flights for this line, and removal of these flights from airport departure and arrival boards.

This example illustrates a principal characteristic of groupware applications: Many clients are able to simultaneously see and manipulate the same information displayed locally under different representations. As soon as a modification of the structure occurs, all the clients viewing that information are notified within their own context.

The MVC Paradigm

Today, the language of choice for implementing real-world, object-oriented applications is C++. However, the earliest general-purpose approach to managing the consistency of applications can be found in Smalltalk, in the form of the well-known Model-View-Controller (MVC) paradigm for application architectures.

The MVC paradigm was developed at Xerox PARC in the late 1970s, and is used in the classic Smalltalk-80 system for presenting different graphical views of the same object. For example, an integer value such as Temperature can be displayed as a numerical digit in a text box, as the position of a needle in a gauge, or as a point in a graph. The MVC approach, in its initial form, applied only to objects within the same Smalltalk program. However, this paradigm can be extended to sharing objects across different applications, and to more-generic, nongraphical models.

In a distributed scheme, MVC can separate application objects (found in a server) from representation objects or views (generally found in clients). This is more than just good programming practice: It allows a single object to have multiple dynamic representations attached to it. In such a case, the creation and destruction of views is independent from the creation and destruction of application objects in the server.

An MVC architecture is best implemented in a language that provides dynamic binding, allowing for virtuality on method arguments. Unlike Smalltalk, this functionality is not available in the current form of the C++ language. Moreover, the MVC approach has some limitations. Its use in the context of structured objects is difficult. The notification mechanism that propagates update information cannot be made incremental (as would be done in a diffusion model). Going beyond fundamental datatypes with MVC can become complicated if you stick to standard C++.

One way to overcome the lack of dynamic binding in C++ is via code generation. A tractable way of generating code is to extend C++ with keywords that can be used to annotate code in header files. A preprocessor parses these annotated headers and automatically generates the appropriate C++ code. Example 1 illustrates the annotation technique: the annotated header is shown in Example 1(a) (specifically, the ILB_ENTRY keyword), while the corresponding preprocessor output is Example 1(b). The preprocessor generates C++ code, which declares and implements accessors and mutators on annotated data. In this example, the class Flight is updated via the generated mutator function: void Passengers(int).

Composition Versus Inheritance

Object-oriented languages, unlike object-oriented methodologies, place a strong emphasis on inheritance and have little direct support for expressing relationships. However, most applications seem to use the composition relation to a greater extent than inheritance. ILOG Server extends C++ to model this kind of relationship, which is crucial to implementing consistent object models.

In an object-oriented design, attaching a view corresponds to attaching a C++ class. Let's imagine that we need two views on the model:

To meet the first requirement, the program needs to access the arrival and departure airport for each line. In C++, this can be done by keeping two pointers to airports in each Line object. The second requirement is difficult since one needs to provide a return pointer from the airports to the lines. This can lead to situations where an airport may not be connected to the correct line.

Bidirectional Smart Pointers

A way to solve this problem is to extend the C++ notion of pointer so that it is a reversible relation. ILOG Server provides annotations for specifying relations between classes via two keywords: ILB_USES and ILB_HAS; see Example 2. This relationship can also bear cardinalities that will automatically manage the maximum and minimum number of target objects for the relation. The ILB_HAS keyword expresses a notion of exclusive ownership: A given object (say, a Flight) is owned once by another object (an Airline). The ILB_USES keyword introduces the concept of utilization: The object Airline uses the object Departure. ILOG Server relies on "smart pointers," so that the developer does not have to explicitly destroy an object by calling the operator delete, which can be fatal when dealing with a large network of interrelated objects.

When the preprocessor encounters ILB_USES or ILB_HAS in a declaration, it generates functions and data members for class Airline. The member functions are generated with the degree of access current in the declaration (in this case, public). These member functions make it possible to access objects that are the target of the relations Departure, Arrival, and Flight, and thus manipulate them and the data members generated in the private part (and stored in the relation).

The member functions generated for Departure are shown in Example 3 and perform the following tasks:

Without creating an explicit symmetric relation from an Airport to an Airline, it is possible for the developer to implement an Airport function returning the arriving and departing Lines. This means that ILOG Server automatically generates bidirectional pointers that ensure coherence of the model.

Moreover, the inverse relations are used for referential integrity. For instance, if an Airport is removed, the related Lines no longer exist and disappear from their parent companies. Not only are the data structures automatically updated, but object destruction is also carried out automatically.

ILOG Server provides generic mechanisms to ensure referential integrity through annotation and also offers the possibility to locally adapt the relation behavior to fit specific needs. The referential integrity of a model is not predetermined but depends on the form of model itself. Unlike a garbage collector, which only reacts to local pointers, ILOG Server performs nonlocal operations on structures.

Information-Sharing Models

Once an object model has been designed and implemented, its objects can provide multiple views for different clients. In that case, the object model becomes an object server that can notify the various clients connected to it when the model changes. The notification mechanism that animates views is the heart of this groupware application.

Currently, there are three principal models for sharing object information in a groupware application: the facet, coupled, and diffusion models. In the facet model, each actor (a process or program) is aware of all the other actors with which it exchanges information. Adding a new view generally affects the implementation of all the other actors. This model does not scale well, resulting in a combinatorial explosion due to the lack of abstraction.

In the coupled model, application objects are clearly separated from representation objects. Each action performed on the application object incorporates the feedback to each view. Adding a new view requires modification of the actions and therefore of the model. This model also suffers from a combinatorial explosion.

The diffusion model is derived from the coupled model and decouples the feedback from the actions by propagating the very same application object to all views. Each view is then responsible for decoding the notification it receives. Performance is generally poor because the notification cannot be made incremental. When the application becomes distributed, network bandwidth becomes a scarce resource. Moreover, as in the previous cases, the server does not respect the client API. MVC-influenced implementations generally use a diffusion model.

To deal with these shortcomings, we introduce the object-server model. This model may or may not be distributed. In fact, the developer should not worry about this issue and can decide to distribute the system later without changes to the source. In our approach, an object model is created with a set of views attached to clients within the server. The notification is selective and adapted to each client's API. Traffic from the server to the client is incremental and reduced to selected service calls to the client's API (this is particularly interesting in the case of distributed objects). Each client is independent from the other, and adding a view does not require modifying the model. This architecture is shown in Figure 2.

A view is a class, separated from the object model, which contains a number of notification functions. In a view, the programmer specifies the classes of the model that will be notified. You can then define three different types of notification functions on the object of the model: creation, destruction, and modification.

The most important feature of the object-server architecture is that the server adapts itself to both the API and the logic of clients. If a client is a spreadsheet view, then its cells, upon modification of the objects in the server they represent, will receive a specific, spreadsheet-cell notification, not an abstract message they will need to decode.

An object server can work in both linked and distributed mode. For instance, one can implement a linked object server which enables different workstations to participate in a groupware application. In fact, the object-server architecture is invariant whether one uses a network or not.

Derived Attributes

In object-oriented programs, there is often the notion of an attribute (a data member). The annotation technique in ILOG Server can enable the notification of attributes. We distinguish two types of attributes: entry (using the keyword ILB_ENTRY) and derived (keyword ILB_DERIVED). To illustrate these attribute types, consider the case of a spreadsheet cell whose formula can be statically defined through C++ functions. Entry attributes are those not constrained by others but that still need to be notified. Derived attributes have values that are functions of other attributes.

Going back to the airline example, let's add a data member to the class Airline. This new data member will count the number of passengers on the line at a given time. The number of passengers is the sum of all the passengers traveling on all the flights of that airline. We will assume that the number of passengers on a flight is a data member of that flight as well. As shown in Example 4(a), the data member Passengers, annotated by the keyword ILB_DERIVED, constitutes the declaration of the active value. The member function countPassengers() runs through the list of flights and sums up the passengers from each flight. To define the rule for computing the passengers, we simply have to define the function ILB_EVALUATE(Airline, Passengers), as shown in Example 4(b).

Once a derived attribute has been declared and defined, the data member it controls will be recomputed automatically based on various updates in the model, such as adding/removing passengers from a flight on the line, or adding/removing entire flights on the line. Derived data members are simultaneously sensitive to modifications of other data members, even those remote in the structure, and are also sensitive to establishing or breaking off relations among objects.

Handling Complex Updates

After designing an object server, the development of the views can be done in parallel with the development of the clients, since only their APIs have to be known. Attaching the notification mechanisms is a simple step that can be performed at the end without any surprises.

Consider a relatively complex application scenario. In the airline application, routes (lines) belong to airline companies and are displayed in multiple views (tables, graphs). Similarly, one can view arriving and departing flights for a given airport. This view is a cross-section of the model, compared to the views by company or line. One application function that may be needed is the transferring of an entire line from one company to another. This operation impacts all the views opened on the model. First, the line needs to disappear from the original company; then, it needs to appear on the map of the target company with the correct color; lastly, the different company line and airport tables must be updated. Using ILOG Server, adding this functionality is a matter of the five lines of code shown in Example 5.

The cut() function is automatically generated by ILOG Server and performs a cut operation on the object--that is, it is removed from its owner (Airline). The cut object must then be attached to a new owner by adding it to an internal list. ILOG Server adapts itself to the locality of the view that the client has on the information. In the case of Line::Transfer(), one destruction and one creation operation are triggered in completely different representations, while the object just moved from one structure to another.

Since the model has been modified, all relevant structures will be automatically notified and updated. This kind of operation is difficult to do using the facet or coupled models.

From Groupware to Systems Integration

Deploying groupware technology in an existing enterprise is only successful if existing heterogeneous systems can be integrated. With the object-server architecture, it is possible to create a server of C++ objects to which different applications, even legacy systems, can connect and access common services. Instead of drastically modifying existing applications, you can extend them, as long as they offer a C++-compatible API. Each application becomes a client of the newly created object server when connecting to a view.

Such integration can extend to databases. A database can be considered a client of an object server through its API. Doing so enables the server to selectively notify the database, in real time, of object updates. This can transform any standard relational database into a persistent repository for C++ objects.

Going back to the airline-management example, this application has to manage a common repository stored in a database, offer multiple views of the same information under different representations (tables, graphs, maps, lists), and ensure consistency between views. If any of the clients already existed as a separate application, one would only have to create a small API around it and add the corresponding view in the object server.

Example 1: (a) A simple class, as annotated for the preprocessor; (b) the corresponding preprocessor output.

(a)
class Flight
{
    public:
        ILB_ENTRY int Passengers;   // data member subject to notification
};
(b)
class Flight
{
    public:                          // these are generated functions
        int    Passengers();         // an accessor function (to get value)
        void   Passengers(int);      // a mutator function (to set value)
    private:
        int    _Passengers;          //  the real data member is private
};

Figure 1 Multiple views in the Airline application. Figure 2 The architecture of the object server model and its clients.

Example 2: Specifying relationships between classes via annotations.

class Airline
{
    public:
        ILB_USES   Airport  *Departure;       // Departing airport
        ILB_USES   Airport  *Arrival;         // Arriving airport
        ILB_HAS    Flight   *Flight {0, ...}; // Flight with cardinality unlimited
};

Example 3: Member functions generated for Departure relation.

ILB_SMART(Airport)    Departure ();
ILB_SMART(Airport)    Departure ( ILB_SMART(Airport) target);

Example 4: (a) A derived attribute for class Airline; (b) function that calculates the derived attribute.

(a)
class Airline
{
    public:
        // ...other data members...
        ILB_DERIVED    int    Passengers;        // passenger count
                       int    countPassengers(); // evaluation function
};
(b)
int ILB_EVALUATE(Airline, Passengers) ()
{
    return    owner().countPassengers();
}

Example 5: Transferring a route (line) from one company to another.

void  Line::Transfer(char* new_co_name)
{
    Airline *new_co = Airline::get(new_co_name);    // Get the company
    if(new_co)
    {
        cut();                                     // cut the Airline.
        new_co->Lines().cons(this);                // paste Line into the new Airline.
    }
}

Copyright © 1995, Dr. Dobb's Journal