Next: 8 Error checking - Up: GNUstep Distributed Objects Previous: 6 Putting them together

7 A modified client which accesses both local and remote files

The previous sections introduced you to the black magic of DO: we replaced a local object with a remote one, and we still called a method of the object in the usual way. We didn't need to use a special syntax to send messages to the remote object: we just did as if it still were our old little object in the local process; the language and the libraries managed silently all the rest. Now we want to push it even further: we do the replacement at execution time.

We want to extend the client so that it can access both local and remote files; it will decide what to do depending on the command line arguments which are used to call it. When called with a single argument, for example README,

Client README
it will display the file README from the local machine; when called with a filename and a host name, for example README and didone.gnustep.it,
Client README didone.gnustep.it
it will display the file README fetched from the server running on the host called didone.gnustep.it.

The server is the same; we only change the client. We declare the usual FileReader protocol because we now want to have a reader object which might be local or remote, and the only thing we know is that we can send the getFile: message to it, which is expressed in Objective-C by saying that the reader object conforms to the FileReader protocol.

Then, we implement a LocalFileReader class, which reads a file (from the local machine). I called it LocalFileReader rather than FileReader to avoid confusion, but technically there is nothing preventing you from calling it FileReader. It is the usual class, which implements the facility of reading a local file. But the class declaration has an interesting modification:

@interface LocalFileReader : NSObject <FileReader>
- (NSString *)getFile: (NSString *)fileName;
@end
in this declaration, LocalFileReader inherits from NSObject, and conforms to the FileReader protocol. We declare that the class implements the protocol because that allows us to cast any LocalFileReader object to id <FileReader>.

When the client is run, it examines its arguments to decide whether it needs to create a local or a remote reader object:

/* Get program arguments */
args = [[NSProcessInfo processInfo] arguments];
  
/* If there is a second argument, read it as a hostname 
   to fetch files from */
if ([args count] > 2)
  {
    NSString *host = [args objectAtIndex: 2];

    /* Create our remote FileReader object */
    reader = (id <FileReader>)
      [NSConnection
        rootProxyForConnectionWithRegisteredName: @"FileReader" 
        host: host];
      
    if (reader == nil)
      {
        NSLog (@"Error: could not connect to server on host %@",
               host);
        exit (1);
      }
  }
else /* No second argument - read local file */
  {
    reader = [LocalFileReader new];
  }
and that's it; the code which follows is the usual one. Notice how both the remote and the local object conform to the FileReader protocol, which makes it possible for the code to contact the local and the remote object in an opaque way.

The full client source code is:

#include <Foundation/Foundation.h>


/* This tells us how the reader object behaves */

@protocol FileReader
- (NSString *)getFile: (NSString *)fileName;
@end

/* A local file reader conforms to the FileReader protocol 
   and reads files locally */
@interface LocalFileReader : NSObject <FileReader>
- (NSString *)getFile: (NSString *)fileName;
@end

@implementation LocalFileReader
- (NSString *)getFile: (NSString *)fileName
{
  return [NSString stringWithContentsOfFile: fileName];  
}
@end


int 
main (void)
{
  NSAutoreleasePool *pool;
  NSArray *args;
  int count;
  id <FileReader> reader;
  NSString *filename;
  NSString *file;

  pool = [NSAutoreleasePool new];
  
  /* Get program arguments */
  args = [[NSProcessInfo processInfo] arguments];
  
  /* If there is a second argument, read it as a hostname 
     to fetch files from */
  if ([args count] > 2)
    {
      NSString *host = [args objectAtIndex: 2];

      /* Create our remote FileReader object */
      reader = (id <FileReader>)
        [NSConnection
          rootProxyForConnectionWithRegisteredName: 
                @"FileReader" 
          host: host];
      
      if (reader == nil)
        {
          NSLog 
            (@"Error: could not connect to server on host %@",
             host);
          exit (1);
        }
    }
  else /* Local file */
    {
      reader = [LocalFileReader new];
    }

  /* From now on the code is the same, whether reader is 
     in the local process or in a remote one */

  /* the first string in args is the program name; 
     get the second one if any */
  if ([args count] == 1)
    {
      NSLog (@"Error: you should specify a filename");
      exit (1);
    }
  
  filename = [args objectAtIndex: 1];

  /* Ask the reader object to get the file */
  file = [reader getFile: filename];

  /* If the reader object could get the file, show it */
  if (file != nil)
    {
      printf ("%s\n", [file lossyCString]);
    }
  else
    {
      NSLog (@"Error: could not read file `%@'", filename);
      exit (1);
     }

  return 0;
}
NB: If you play with this client, and if you want to pass an empty string as host name to have it look on the local host, make sure you quote the empty string:
./obj/Client Client.m ''

At this point we probably need to say a few words about why and how this magic is possible. In other words, it's time for a bit of sane religious praise of our beloved Objective-C language :-).

The whole magic is in the method invocation

file = [reader getFile: filename];
which invokes the method getFile: of the object reader, no matter if reader is a local or a remote object; the type of the reader object is determined only at execution time, depending on the command line arguments which were passed to the tool. The example is impressive because it shows that the language allows you to replace any local object in your application with a remote object at execution time, without recompiling or restarting your code, and everything will still work, assuming of course that the remote object can respond to the methods the local object could. This is possible because Objective-C provides dynamic binding of method invocations, which means that the method invocation is bound to the method implementation only at runtime. This allows many powerful object oriented designs which wouldn't be possible otherwise (Distributed Objects are an example of such a design); typically designs in which objects are dynamically and cleanly replaced with other objects at runtime, or designs in which an object encapsulates some functionality, but the actual class or implementation of the object is not known till execution time.

Objective-C is the fastest language available which supports this kind of advanced object oriented designs; Objective-C code is normally as fast as C because it is C code, except in method invocations (which C doesn't have) which on average take three times more than the time required by a function invocation. It's practically impossible for a fully object oriented language to go faster than that; Objective-C performs nearly as fast as C++, but yet provides you with dynamic binding and many other very advanced and flexible object oriented features (such as categories, or access to the runtime internals) which C++ doesn't provide. Some of these features are missing even in Java.


Next: 8 Error checking - Up: GNUstep Distributed Objects Previous: 6 Putting them together
2011-02-09