Implementing Distributed Objects

Ernest uses NeXT's PDO and Objective-C to implement a simple client-server application that packages a legacy application into an interoperable object and its client.


August 01, 1995
URL:http://www.drdobbs.com/parallel/implementing-distributed-objects/184409611

AUG95: Implementing Distributed Objects

Doing it the easy way with NeXT's PDO

Ernest is president of NextStep/OpenStep User Groups International, and currently working on his PhD in experimental particle physics at the California Institute of Technology. He can be reached at [email protected].


Creating distributed applications is generally considered difficult. While object-oriented programming promises to make the task more tractable, many programmers still shudder when subjects such as CORBA, OLE, SOM, and OpenDoc arise. However, programming with distributed objects does not have to be difficult, if you start with the right foundation.

My first distributed application--a client-server system developed from a small legacy C program--took just 45 minutes to write and debug. And that, using only a DDJ article for reference. Let me explain.

The recently published Dr. Dobb's Special Report on Interoperable Objects (Winter 1994/95) examined the major distributed-object technologies: Microsoft's Object Linking and Embedding (OLE), IBM's System Object Model (SOM), CI Labs' OpenDoc, Novell's AppWare Data Bus (ADB), Taligent's CommonPoint, and NeXT's Portable Distributed Objects (PDO). The issue ends with a challenge to vendors of distributed-object technologies to implement a simple client-server application which consists of packaging an existing C program (the "legacy app") into an interoperable object and its client. For whatever reasons, only Microsoft and IBM responded to this challenge. The results are presented in the article "Implementing Interoperable Objects," by Ray Valdés.

After puzzling over the pages of code from Microsoft and IBM, I read the article "Distributed Applications and NeXT's PDO," by Dennis Gentry. It seemed to me the job would be trivial with the PDO technology. Consequently, I created my version of the DDJ challenge--in about 20 minutes. Not having access to PDO at that time, my original version was written using the native Distributed Objects (DO) facility under NextStep 3.2. Thanks to Joakim Johansson ([email protected]), my application was then tested under PDO 2.0.

In this article, I'll describe how to use PDO and Objective-C to write distributed applications. I'll focus on concepts rather than on a detailed walk-through of my code. Once you understand the concepts, the code is trivial.

Three Steps to Distributed Objects

Applications that use distributed objects rely on an application-enabling foundation that provides the mechanisms for object distribution. This foundation is part of the system platform. To implement a distributed-object foundation, all that's needed are objects, a means for finding them, and a mechanism for distributing messages. While this may seem obvious, it is amazing how many companies attempt to create so-called interoperable-object frameworks without a proper object foundation. If you choose smart enough objects, you can even get a lot of the distribution thrown in for free.

Portable Distributed Objects (PDO) is a distributed-object facility intended to run on a number of platforms (such as Solaris and HP-UX) and interoperate with the native Distributed Objects (DO) facility in NextStep. First, NeXT starts with Objective-C, which adds Smalltalk-like objects and run time to C (see the accompanying text box entitled "Objective-C and Distributed Objects"). Second, to locate objects, NeXT uses the Mach nmserver (originally part of NextStep, but later shipped separately as part of PDO). Finally, to forward messages across a network, NeXT implements a proxy system. That's all it takes.

Implementing a Distributed Application

In writing my application, I spent most of my time creating ordinary Objective-C objects, a process I already knew how to do. After defining the ordinary object MyServer, you simply register it with the nmserver to make it distributed, as in Example 1.

When this program runs, it instantiates an object of class MyServer, registers this object under a particular name, then starts an event loop in that thread to service requests. Variants of this technique allow you to start up a server object as a separate thread, or even as a multithreaded object.

The client side is equally simple; see Example 2. Instead of explicitly allocating a server object, you merely ask for a connection to it. The server object can be on the same machine or anywhere on the network--the NXConnection facility will find it. This facility returns an NXProxy object, to which one can send messages, just as if it were actually an instance of MyServer. As you can see in the example, you connect to a computation server that multiplies two floating-point numbers and returns a result. The code also shows how a server vends other types of objects, such as strings and object references. Using the type id, you can take advantage of Objective-C's dynamic typing and send the proxy a message regardless of the object's actual class. The NXProxy takes care of all the work of translating and transporting the arguments and return types. It all just works!

Well, not exactly. Up to this point, the code does work, but it is inefficient and leaks memory. To see how to make a distributed, heterogeneous system work properly, you must look inside NXProxy.

Protocols in Objective-C

The basic problem is how to deal with potential architectural differences between client and server machines. For example, you might be running an Intel (Little-endian) client, accessing a RISC (Big-endian) server. Converting between the two memory formats is simple, but how do you know when to do it? This is complicated by the fact that Objective-C selectors do not have the static typing of C++ member-function calls. That is, the add: selector can equally accept an integer argument for the Integer class, or a float argument for the Float class. Which method gets invoked is determined only at run time, by the actual object called.

To address this, NXProxy has to make a round-trip inquiry of the remote object to find out the effective signature (argument types) for the actual method. It then uses this information to package and send the arguments in a machine-independent format across an NXConnection, where they are unpacked and delivered to the object. Return values are handled in the same way.

This method involves a large amount of overhead, which is usually solved with static typing, but that is not appropriate to use with a proxy, since it has a different data type. Consequently, NeXT introduced the concept of protocols into the Objective-C language. Protocols are pure interface, similar to abstract base classes in C++. They follow their own hierarchy and can use multiple inheritance. This increased separation of interface from implementation is a powerful tool in its own right, but here I'm concerned only with its implications for distributed-object applications.

A protocol declaration looks just like an interface declaration in Objective-C, with inherited protocols in place of a superclass definition. Angle brackets indicate that a class or instance variable adopts a given protocol. When a message is sent to a proxy with a protocol indication, it uses the (local) protocol information to determine the method signature, rather than making a round trip to the remote object.

Protocols also provide a form of typechecking, in that any attempt to send a message to a variable that is not in the message's supported protocols results in a compile-time warning. The compiler will also warn you if you assign it an incompatibly typed object. You can also use the conformsTo: method to manually verify that a vended object supports the given protocol. Example 4(a) specifies a protocol; the application code that uses this protocol is in Example 4(b).

Memory Allocation

Since you obviously can't pass pointers across different address spaces, there has to be some mechanism to repackage out-of-line data for transmission across the network. By using the method signatures just described, NXProxy can determine when a pointer is being passed. If it is a pointer to an object, it simply sends another proxy instead. If it is a pointer to a normal variable, it just sends a copy of the information being pointed to over the network. The connection on the other side recreates the appropriate pointer, sends back any modifications, then destroys the remote copy after the remote-procedure call (RPC) finishes.

In general, this solution works pretty well. The system is smart enough to realize that not all information has to go both ways. For example, a const char* can't be modified by the remote process, and hence needn't be returned. To fine-tune the behavior, you can use the special keywords in, out, and inout in the protocol declaration. You can also use the keyword bycopy to indicate that an object that satisfies the NXEncoding and NXDecoding protocol should be encoded and passed over the wire, rather than sending a proxy. The specifier oneway is used for void methods that do not need to return, and hence can be called asynchronously.

The problem with this behavior is that sometimes you want a passed parameter to persist--say, when you're adding a string to a lookup table. Or sometimes you are returning a string to the calling routine, so there's no clearly appropriate lifetime. To allow for this, NeXT introduced a hack into their distributed object run time, preventing it from destroying strings created in an RPC process. Thus, using objects in the normal fashion results in a memory leak each time a string is passed back or forth.

Because of this, you need to know whether an object will be used locally or remotely, and be able to add the code to manually free any string pointers created in consequence. This is a rather ugly situation, destroying the otherwise beautiful symmetry of NeXT's Distributed Object system.

Rather than just undo the hack, NeXT solved the more-general problem of temporary objects in the FoundationKit (see the text box "NeXT's FoundationKit"), used in PDO 3.0. At the time of this writing, PDO 3.0 was just about to go into beta, so my example uses the shipping version, PDO 2.0.

The PhoneDir Example

At this point, you should be able to read and understand the code for the DDJ interoperable-object challenge. As you may recall, the requested application was the simplest possible client-server application, an example called the "One-Minute Phone Directory" (so-called because it is so small, implemented in less than 200 lines of C). Listings One through Four present my implementation of the PhoneDir example. I won't repeat the original C code here, but it is available electronically; see "Availability," page 3.

The goal of the exercise is not to show off application functionality, but to focus on the machinery needed to turn a piece of legacy C code into an application that uses distributed objects. To this end, I did not rewrite the legacy code, even though it would have been trivial. Rather, the legacy code is called from inside PhoneDir, an object I defined that adopts the PhoneDirectory protocol. A simple server application vends the remote object, and the client application is virtually identical to the nonobject case. The total Objective-C code is the same size as the nondistributed C program. A prototype version written entirely using the FoundationKit was actually shorter than the C version.

Objective-C and Distributed Objects

Objective-C is a hybrid, object-oriented programming language (OOPL) originally developed by Brad Cox of Stepstone. It consists of a few Smalltalk features layered on top of ANSI C: messaging, class definition, and the id data type. Smalltalk syntax and semantics make it much simpler and easier to learn than C++, at the price of a look-and-feel that is slightly foreign (at least to C programmers).

Objective-C is a dynamically bound, single-inheritance OOPL using both static and dynamic typing. Its principal difference from C++ is that Objective-C handles most decisions at run time, rather than compile time. This requires a great deal of run-time information, which, while incurring some overhead, allows the use of more-powerful programming paradigms.

A good caching strategy results in messaging overhead around three times that of a function call. Of course, that is still far slower than an inline function call or pointer dereference; for time-critical sections, you can drop back into straight C.

Objective-C objects and messages are "self-conscious," that is, you can ask an object its name, whether it responds to certain messages, and what its method signatures are (that is, what are the types of its arguments). Objects can also manipulate messages as first-class objects, using the @selector() directive. For example, a List object can use the -perform: method with a message argument to make its constituent objects perform the appropriate method. It can even check first to send the message only to objects that know how to respond to it!

The key feature that makes Objective-C useful for distributed objects is its ability to "forward" messages; see Example 3. Dynamic typing allows you to send any message to an object, even a message that's not part of the object's defined interface. While this ability should be used sparingly (it is optional in Objective-C), it allows for a great deal of flexibility when prototyping, or dealing with objects from an outside source. If the object cannot respond to a message, the default behavior is to return a run-time error. However, if you implement a forward:: method, you can choose to forward that message to another object, known as a "delegate."

Objective-C has been extended by NeXT in a variety of ways, including protocols and better integration with C++. These extensions are tracked by the Free Software Foundation in the GNU Objective-C compiler, part of the current GCC distribution. Sun is also integrating Objective-C with its C++ compiler as part of OpenStep for Solaris, scheduled for release later this year.

--E.N.P.

NeXT's FoundationKit

NeXT first introduced Distributed Objects (DO) as part of NextStep 3.0 in 1992, followed a year later by Portable Distributed Objects (PDO) for HP-UX. Since then, it has become clear that one of the main motivations for the use of object-oriented technology is to simplify distributed client/server computing. NeXT has therefore extended the foundations of Objective-C to optimize its support for object distribution over a network.

The resulting technology is called "FoundationKit" (FK) and consists of numerous classes, plus a new paradigm for memory allocation called "autorelease." Since most action takes place in response to an event (user interaction or RPC), this provides a natural lifetime for temporary objects. Instead of being freed, an object can be autoreleased, meaning that it will be cleaned up the next time through the event loop. If you wish to hold on to an object, you send it a retain message; when you no longer need it, you send a release message.

Autorelease is based on reference counting and is not as powerful as true garbage collection: It requires programmer intervention and does not handle cyclic references. However, it has lower overhead than any other distributed garbage-collection scheme and the overriding virtue of being easy to implement over a network. Most importantly, it provides a uniform solution to the temporary-object problem, which crops up whenever you pass pointers across a network or return an object from within a method. The freeing that takes place between user events also helps minimize its impact on response time.

FoundationKit provides numerous other classes to aid in developing applications and frameworks. Network-savvy object wrappers for primitive data types (numbers, points, buffers, and collections) aid in passing them over the network. Objects manage Objective-C information (method signatures and exceptions). Most importantly, a Unicode-supporting NSString object allows NeXT to merge the release of NextStep for European and Asian languages, which were previously separate. However, there is still no official word on a revised Text object which would support alternate layouts (right-to-left, vertical).

FoundationKit ships as part of NextStep 3.3 and will be in PDO 3.0, slated for the middle of this year. NextStep 3.3 is the first release of NextStep (and possibly any other operating system) that will run transparently on four different CPU architectures: Motorola, Intel, PA-RISC, and SPARC. Applications compiled on any one platform using NeXT's MAB (Multiple Architecture Binary) technology, which uses GNU's cross-compilation tools, will run identically on any of the others. FoundationKit also forms the basis of OpenStep, NeXT's OS-independent API, which will be available on Sun's Solaris and Digital's OSF/1 for Alpha near the end of 1995. NeXT recently confirmed that OpenStep will be ported to Windows 95 and Windows NT. FoundationKit is also considered the starting point for "Mecca," NeXT's next-generation technology, which is intended to compete against Taligent and Cairo in 1996 or 1997.

--E.N.P.

Example 1: A simple server application.

main()
{
        static const char* serverName = "ServerName";
        // create and initialize the Server object
        MyServer* mine = [[MyServer alloc] init];
        // register the Server with the nameserver facility
        NXConnection* conn = [NXConnection
               registerRoot:mine
               withName:serverName];
        // start servicing requests             
        [conn run];  
}

Example 2: Client for the simple server.

main()
{
        static const char* serverName = "ServerName";
        // Get the proxy for the server - scan the entire subnet
        id serverProxy = [NXConnection
                             connectToName:serverName
                             onHost:"*"];
        // Pass two floating-point numbers to server and get a result
        float result = [serverProxy multiply:7.0 with:5.0];
        // Get a proxy for another object vended by the server
        id objectFromServer = [serverProxy getAnotherObject];
        // Pass a string, and get one back
        char* string = [serverProxy sendAndReturnString:"string pointer"];
}

Example 3: Forwarding in Objective-C.

//  Called when this object receives a message that it cannot respond to.
//  Messages consist of a "selector" (i.e. "forward::") plus arguments.
//  If forward:: is not defined, then "doesNotRecognize:" is automatically
//  called. The variable "delegate" is an instance variable of this class.
- forward:(SEL)aSelector :(marg_list)argFrame
{
    // if the delegate can handle it, let it.
    if ( [delegate respondsTo:aSelector] )
        return [delegate performv:aSelector :argFrame];
    // otherwise, raise the error
    [self doesNotRecognize:aSelector];
}

Example 4: (a) Specifying a protocol; (b) using the specified protocol.

(a)
@protocol ServerProtocol : InheritedProtocol

- multiply:(float) a  with:(float) b;

- (char*) sendAndReturnString:(const char*) ptr;

- (id<AnotherProtocol>) getAnotherObject;

@end

@class MyServer: Object <ServerProtocol> 

//...

@end
(b)
// Get the proxy for the server

id<ServerProtocol> serverProxy = [NXConnection connectToName:server];

// Get a proxy for another object vended by the server

id<AnotherObject>  another = [serverProxy getAnotherObject];

Listing One

//---------------------------------------------------------------
//  PhoneClient.m                       by Ernest Prabhakar, 1995
//  Transliteration of test suite into Objective-C
//  The only addition is to free strings for the remote case
//---------------------------------------------------------------
#import "PhoneDir.h"
#ifdef REMOTE
#import <remote/NXProxy.h>
#endif
main(int argc, char* argv[])
{
        const char* name;
        const char* number;
#ifdef REMOTE // scan subnet for object
        id<PhoneDirectory> theDir = 
                [NXConnection connectToName:dirName onHost:"*"];
        if (!theDir) {
                printf("Server not present.\n");
                exit(1);
        } else {
                printf("Connected to remote server.\n");
        }
#else
        PhoneDir<PhoneDirectory>* theDir = [[PhoneDir alloc] init];
#endif
        // Call server to get number for name, if any.
        name = "John Doe";
        number = [theDir number:name];
        // Print out result
        if (number) {
                printf("%s's number is %s.\n", name, number);
#ifdef REMOTE   
                // For remote case, free returned string
                free((char*)number);
#endif
        } else {
                printf("%s does not have a number listed.\n",name);
        }
        // do a lookup by number
        number = "408-555-1212";
        name   = [theDir name:number];
        if (name) {
                printf("%s's number is %s.\n", name, number);
#ifdef REMOTE   
                // For remote case, free returned string
                free((char*)name);
#endif
        }        else {
                printf("The phone number %s has not been assigned.\n",number);
        }
        exit(0);
}

Listing Two

//---------------------------------------------------------------
//  ** PhoneDir.h **                by Ernest Prabhakar, 1995
// The protocol and interface declaration for the PhoneDir class.
//---------------------------------------------------------------
#import <objc/Object.h>
static const char* dirName = "PhoneDirectory";
//--------------------Protocol Declaration----------------------
@protocol PhoneDirectory
        // Return number given name
        - (const char*) number:(const char*) name;
        // Return name given number
        - (const char*) name:(const char*) number;
@end
//-------------------Interface declaration----------------------
// Inherits name: and number: from protocol
@interface PhoneDir : Object <PhoneDirectory>
        {
        }
        // initialization and destruction
        - init;
        - free;
@end

Listing Three

//----------------------------------------------------------------------------
//  PhoneDir.m                          by Ernest Prabhakar, 1995
//  Wrapper class for the C phonedir functions. It uses the original C code
//  unchanged to prove a point. Otherwise, the code for the PDO implementation
//  would be about the same size as original non-distributed code!
//----------------------------------------------------------------------------
#import        "PhoneDir.h"
#include "phonedir.h"
@implementation PhoneDir : Object
{
}
//---------------------------------------------------------------
- init
{
        [super init];
        phonedir_Initialize();
        return self;
}
//---------------------------------------------------------------
- free
{
        phonedir_Terminate();
        return [super free];
}
//---------------------------------------------------------------------------
//  When the object is invoked over the wire with a string argument, a copy of
//  the passed string is made. In that case, we have to explicity free the 
//  string. On a local invocation, the string would be a reference from the 
//  caller, and we should not free it. This dichotomy between local and remote
//  calls--and the different treatment of char* and other pointers--is due
//  to the limitations of PDO 2.0, and will not be necessary in PDO 3.0.
//---------------------------------------------------------------------------
//
// Return the phone number, given a customer name.
- (const char*) number:(const char*) name
{
        const char* result = phonedir_LookupByName(name);
#ifdef REMOTE
        free(name);
#endif
        return result;
}
//---------------------------------------------------------------
// Return the customer name, given the phone number.
- (const char*) name:(const char*) number
{
        const char* result = phonedir_LookupByNumber(number);
#ifdef REMOTE
        free(number);
#endif
        return result;
}
@end

Listing Four

//---------------------------------------------------------------
//  PhoneServer.m                       by Ernest Prabhakar, 1995
//  Creates the PhoneDir object & sets it up to service requests.
//---------------------------------------------------------------
#import "PhoneDir.h"
#import <remote/NXProxy.h>
main(int argc, char* argv[])
{
        id myDir = [[PhoneDir alloc] init];
        id myConn = [NXConnection registerRoot:myDir withName:dirName];
        printf("Starting server...\n");
        [myConn run];
}


Copyright © 1995, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.