CORBA Program Development, Part 1
CORBA (Common Object Request Broker Architecture) is one of those acronyms for which most people have some “feel”, others have some interest, but very few have any real experience. This is the first article in a series of three in which we will attempt to increase the first, augment the second and remedy the third. We will be quick to point out CORBA's strengths, but will not shrink from disclosing some of CORBA's current shortcomings which, while not terribly numerous, nevertheless can present a stumbling block to the uninitiated.
Our goal is to help the Linux programmer new to CORBA get his feet wet through examination of a very simple CORBA application. Our simple distributed application is developed using OmniORB, a free ORB from Oracle-Olivetti Research in Cambridge, England. OmniORB is a fast, clean implementation of the basic CORBA 2.0 architecture. We have calculated its performance to be anywhere from 2 to 15 times faster measuring the same code as its commercial competitors—plus, it runs on Linux. It implements the CORBA standard's naming service, although it does not currently support some of the more popular CORBA services, such as the event service, the trader service or the life cycle service. (It has a life cycle service of sorts, but is nonstandard.) It also does not yet offer a dynamic invocation interface, a dynamic skeleton interface or an Interface Repository. It does, however, provide a fine thread abstraction for POSIX pThreads, one that is highly portable (one of the authors has ported it to HP's tenuous DCE threads implementation). The OMNIThread abstraction supports Linuxthreads 0.5 and above (POSIX 1003.1c-draft 10 and comes with Red Hat's glibc implementation) and MIT pThreads and is well worth the download. The simple example shown here does not require two separate networked machines, so even if you have only one machine, you can still get your feet wet. There is no requirement in CORBA that the objects actually be physically distributed. That design feature is completely optional.
We know that CORBA is an approach to distributed programming, but what exactly is it? CORBA defines a coordinated specification for the implementation of distributed object communication. CORBA is a specification managed by the Object Management Group (http://www.omg.org/), a consortium of over 800 different hardware and software vendors, whose diverse interests intersect around the concept of distributed computing. The OMG's goal with CORBA was to define a communication standard that would be platform-and-language independent, with a focus on the object-oriented paradigm.
The primary motivation for distributed programming is parallel processing, or actually distributing the activity of an application across several computers so that they are simultaneously working to solve a problem or a complex of problems. Resources are generally limited in any environment, and for applications that are stranded on one machine, the resource pool (memory, disk, I/O, etc.) can easily become strained. When that happens, overall throughput and performance of the application suffers. Distributed programming allows a developer to distribute the workload of an application across several different machines, each with its own set of resources. By doing so, the developer can greatly influence the overall throughput of an application in a very positive way.
CORBA provides developers with a framework in which to develop objects which can communicate with one another on a single machine or over a network, regardless of the hardware platform or the programming language. Using CORBA, it is possible for an object written in C++ on UNIX to communicate with another object written in Java on Windows or an object written in COBOL on a mainframe. Several technologies attempt to provide similar functionality: TCP/IP, Berkeley Sockets, DCOM, Remote Procedure Calls (ONC and DCE), Java's Remote Method Invocation, et al. System V IPC offers several inter-process communication capabilities such as shared memory and message queues, but they are single-machine solutions by default, whereas CORBA is centered around exposing objects on a network. CORBA, the implementations of which are often built on top of some of these components such as TCP/IP, sockets and DCE, offers a unique package of benefits over and above these alternatives.
CORBA allows distribution to take place almost entirely within the object model, abstracting the details of the object communication so that the developer has to worry about only the higher-level interfaces as opposed to the nuts and bolts of the communication layers. There is a cost in terms of network latency that any CORBA developer must be conscious of early in the game. In a sense, the resource limitations of a single computer are traded for the network limitations in bandwidth and performance. Remember, each call to a remote object is a network call. If your design calls for 100 set methods to be called on a remote object just to initialize it (a very bad CORBA design), you will soon see performance degradation in a whole new light.
CORBA objects can be implemented in many different programming languages running on many different platforms. In order for a specification to define a framework for such an environment, it is important to have a platform- and language-independent method of describing objects. In order to meet this need, CORBA defines a mapping language called IDL (Interface Definition Language) that is actually very similar to C++ in syntax. IDL is used to describe the way a remote object appears to the outside world, along with properties or methods that exist in the object. An IDL compiler is then used to translate the IDL into the source code of a particular implementation language, e.g., C++, Java, C, Ada, Smalltalk and COBOL. For the server, the IDL compiler creates the source code needed by the ORB to expose the object to the outside world and creates a skeleton that is then “filled in” with the actual implementation of the object by the developer. For the client, the IDL compiler creates stubs which allow the remote object to appear to be local to the client. In order to remain platform-and-language independent, IDL has its own variable types. The IDL compiler maps each of these variable types to a representative language construct in the native language for the client or server.
Now we can address the question of how CORBA actually works. The first and most important component to look at is the ORB. In the CORBA specification, the ORB (Object Request Broker) is the communication layer that resides between a CORBA object and the user of the CORBA object. Through the ORB, a client application can access properties, pass parameters, invoke methods and return results from a remote object. It is a common misconception that the ORB is a daemon or a service that implements CORBA—some ethereal middleman that floats around out on the network. Actually, the ORB is a communication layer that resides partially in the client and partially in the server at the same time. The ORB is responsible for intercepting a call to a remote object, locating the remote implementation of the object and facilitating communication with the remote object. Thus, when we talk about “the ORB”, we are talking about the communication capabilities provided to a client and a server through their respective stubs and skeletons, as well as through calls those stubs and skeletons make to the ORB implementation's runtime libraries, which provide low-level communication and marshaling capabilities.
Given that an ORB is a communication layer and is responsible for locating an implementation, it needs some method by which to find the remote object. CORBA achieves this by assigning all remote objects a unique IOR (Interoperable Object Reference). The IOR is like a telephone number by which the client application can call upon a specific remote object. In order for the client application to access the remote object server, it must first be able to obtain an IOR. There are several different methods by which a client can obtain an IOR, the easiest and least practical being to pass it on the command line. Many CORBA implementations (such as VisiBroker and Orbix) have simple proprietary IOR lookup mechanisms that allow the IOR to be passed to the client using a “bind” call. The OMG defines the Naming Service as the preferred way for a client to obtain an IOR of a remote object. In part three of this series, we will go into the Naming Service in detail. For our first example, however, we will pass the IOR of the server's object to the client in a common file that will be read by the client during initialization.
We deliberately created a simple example so that the code can be easily followed, and we provided a Makefile and Make.rules because omniORB uses a rather complicated make scenario that is difficult to follow. This sample code was developed and tested using omniORB 2.5.0 running on Red Hat Linux 5.1 (kernel 2.0.35). The code was compiled using g++ version egcs-2.90.27 980315 (the egcs-1.0.2 default C++ compiler with Red Hat). (We also compiled and ran the code using the latest omniORB 2.7.0 and egcs 1.1.1.)
To build and run this example, download omniORB 2.5.0 from http://www.orl.co.uk/omniORB/omniORB.html. Fill out the form, and download the correct version of omniORB for your version of Linux—it is free. You might want to get down the binary version that comes with complete source code as well. The binary version expects you to be using the g++ that comes with gcc 2.7.2. If you're running egcs (for example, Red Hat 5.x), you will need to go ahead and recompile omniORB, so it will work with the egcs g++ compiler (choose i586_linux_2.0_egcs in config.mk in the config directory). You can find out what g++ compiler you are running by typing g++ -v. Follow the instructions in the README* files for instructions on building and setting up the omniORB environment.
Once you have omniORB installed and built, you can download the sample code from ftp://ftp.linuxjournal.com/pub/lj/listings/issue61/3201.tgz. Then unpack the tar file. In order to build the sample, you must edit both the Make.rules and Makefile files. See the file README.build for information on what to change for your location and compiler. Once you've edited the appropriate files for your location, simply type make to build the samples.
Once you have built the example, run it by launching the server in one window (or virtual terminal) by typing server. Notice that the IOR for the server's object is printed to STDOUT. It is also written to a file called ior.out. Next, run the client in another window by typing client. This opens the IOR file, obtains the IOR for the server's object, then resolves that IOR to an object reference and makes a call on the remote object. For a remote connection, you will need to get the ior.out file from the server's directory to the directory the client is going to run in. You can do this by using FTP to transfer the file, after the server is up and running, to the client's directory on the other machine.
Like most CORBA applications, our simple CORBA example is made up of three items. First, a server application that instantiates a CORBA object, then basically blocks forever, thus exposing the CORBA object to potential clients. Second, the implementation of the CORBA object itself, which is run when a client obtains a reference to the server's object. Third, a client that binds to the CORBA object and proceeds to make calls against its interface as defined in the IDL for the CORBA object.
Let's begin with the IDL. Our example has a very simple interface defined, PushString. Listing 1 shows that interface PushString has a single function, called pushStr, which takes a single input parameter of IDL type string and returns an IDL boolean value. When this is compiled by the IDL compiler (omniidl2), the following files are created:
PushString.hh: stub/skeleton header
PushStringSK.cc: stub/skeleton code
In the omniORB implementation (the creation of stubs and skeletons is ORB-specific), both the stubs and the skeletons are defined in the same file for each IDL file processed. The skeleton for the implementation is called _sk_PushString and it is inherited by the implementation of PushString as follows (in PushString_i.h):
class PushString_i : public _sk_PushStringEvery function defined in an interface is declared as a pure virtual in the skeleton class (in PushString.hh):
virtual CORBA::Boolean pushStr ( const char * inStr ) = 0;When the implementation of PushString (PushString_i) states that it is inheriting from the skeleton (public _sk_PushString), it pledges to implement each single pure virtual function in the skeleton class. In this way, the pledge of an implementation fully supporting an interface is enforced. It is a compiler error to fail to implement one of the pure virtual functions declared in the skeleton.
In Listing 2 (Srv_Main.C), we see the server application that creates the CORBA object and presents it to potential clients via the ORB. The first thing the program does is open an output file called ior.out. This is where it is going to write the IOR for the object it is about to create. Then, it initializes the ORB:
CORBA::ORB_ptr orb = CORBA::ORB_init(argc,argv,"omniORB2");
This call takes in parameters to the orb which were passed in as command-line arguments, such as flags to turn on tracing, set the name of the server, etc., and uniquely identifies this initialization as expecting the omniORB2 ORB.
After the ORB has been initialized, it is the BOA's (Basic Object Adapter) turn. The BOA for the server is initialized with the following call:
CORBA::BOA_ptr BOA = orb->BOA_init(argc,argv,"omniORB2_BOA");
The BOA initialization is ORB-dependent. In omniORB, the name of the BOA is set to “omniORB2_BOA” and the user can specify certain flags to the BOA. A communication layer must be able to communicate with something, and one of the alternatives developed in the CORBA specification is the BOA. The BOA resides in a CORBA server and is responsible for initializing the remote object when a client requests access. The BOA then provides a translation layer between the remote representation of the object to which the ORB communicates and the actual physical implementation of the object.
After the BOA has been initialized, the implementation of a CORBA interface is created, in our case, the PushString interface. Once the implementation has been created, we register the newly created implementation with the BOA object by calling the object's _obj_is_ready function with the object reference of the BOA itself, which was returned by the BOA_init call. The main purpose for registering object implementations with the BOA is to let it know the implementation is running so it can dispatch calls to the object made by clients.
Finally, we call impl_is_ready on the BOA. We do this to let the BOA know it should now begin to listen for client requests on its designated port. Prior to this call, although the implementation is ready and waiting, no traffic will arrive because the BOA is not listening for it. It is the impl_is_ready call that tells the BOA to start listening for client connections on behalf of this object's implementation. Depending on the ORB implementation, the client may block on a function call to the remote object, or an exception may be thrown if impl_is_ready has not been called.
In Listing 3 (PushString_i.h), we see the implementation's header file which declares that this implementation will be implementing the pushStr function, but the class must also define its own constructor and destructor. The IDL compiler does not create the implementation header for you (some compilers, such as Orbix's idl2cpp compiler, will create a “shell” implementation header and cpp file if you request it); generally you have to do that on your own. Notice the class declaration line:
class PushString_i : public _sk_PushString
We are declaring that PushString_i will be implementing all the pure virtual functions defined in _sk_PushString (we have defined only one), by inheriting from the skeleton.
In Listing 4 (PushString_i.C), we actually go about the task of defining the implementation of the interface defined in the PushString.idl file. We will, of course, implement our constructor and destructor, but we will also give a real body to our virtual pushStr function. We do this with the definition of the function:
CORBA::Boolean PushString_i::pushStr( const char * inStr) throw(CORBA::SystemException) { int retval; cerr << "in PushStr\n"; char * m_str = new char[strlen(inStr)+1]; strcpy(m_str,inStr); // just for fun, mess with the boolean return if( strlen(m_str) > 5 ) retval = 1; else retval = 0; cout << "The string pushed was " << m_str << endl; delete [] m_str; cerr << "Implementation leaving PushStr..." << endl; return(retval); }
Here, when the client calls the pushStr function, passing it a string (notice the IDL string type has been mapped in C++ to a const char *), the function prints out a message letting us know we're in the implementation. It then copies the incoming string into a local buffer, checks to see if the length of the incoming string is greater than 5, and if so, sets the function's Boolean return value to 1; otherwise, it sets the return value to 0. Then, pushStr prints out the string that was copied, deletes it, and notifies us we are now leaving the implementation. At that point, we return to the client the Boolean retval created earlier.
In Listing 5 (Client.C), we see the client code. The first thing we do when we enter the Client.C code is open the ior.out file the server created, which contains the IOR of the server's object implementation. The client expects this file to be in its current working directory. Once that file has been opened and the IOR stored in the variable “IOR”, the client code begins to look reminiscent of the code in the Srv_Main.C file. Namely, the same calls to initialize the ORB and the BOA take place, with the same parameters.
Then, we create a variable of type PushString_var. The type PushString_var is essentially a helper type that provides the stub capabilities for marshaling and unmarshaling parameters. It also provides other essential functions such as releasing and duplicating object references, along with functions for determining whether a particular object reference is empty (null) or not. Essentially, the PushString_var type stands as a proxy within the Client's process space for the real implementation of the PushString object, which may be miles away across the network.
After pushStringVar is created, we enter a try/catch block that calls the ORB and asks it to translate the string IOR (which we obtained from the ior.out file created by the server) into an actual object reference. The object reference created here, however, is of type CORBA::Object_var, a generic type. In order to actually make a call against that object's interface, we must “downcast” it into the actual type of object it represents and for which we have an implementation. This is done through a call to a function named narrow, defined in the Abstract Base Class for the stub PushString (defined in PushStringSK.cc). Once the generic type has been resolved to an actual interface implementation, we can then make calls on that interface. This is done with the call:
pushStringVar->pushStr(src);
This makes a call to the remote object's implementation, passing in a string that contains simply the phrase “Hello World”. At that point, given no exceptions were thrown during the try block, the client notifies us it has completed the call without an exception and terminates.
Finally, you might be wondering whether it is always necessary to pass IORs around via files. The answer is certainly not, but because omniORB does not have its own proprietary bind mechanism (which is how a simple example like this would be implemented in VisiBroker or Orbix), the only way to get the client talking to the server object without using the Naming Service is through an IOR passed in to the client by the server. In a future article on CORBA services, we will show how the CORBA naming service can be used to obtain an object reference through nothing more than a name (which looks much like an absolute path name in UNIX). With the naming service in place, we will no longer need to pass IORs from the server to the client.
In our next article, we will be talking about CORBA on Linux using VisiBroker for Java, implementing in Java. We will also have a much more complicated example in our article on CORBA services, where we will offer an example that utilizes the Naming Service, as well as a factory which creates objects on behalf of the client.
Mark Shacklette is a principal with Pierce, Leverett & McIntyre in Chicago, specializing in distributed object technologies. He holds a degree from Harvard University and is currently finishing a Ph.D. in the Committee on Social Thought at the University of Chicago. He is an adjunct professor teaching UNIX at Oakton Community College. He lives in Des Plaines, Illinois with his wife, two sons and one cat. He can be reached at jmshackl@plm-inc.com.
Jeff Illian is a principal with Energy Mark, Inc. in Chicago, specializing in electric utility deregulation and distributed trading technologies. He holds a degree from Carnegie-Mellon University in Operations Research (Applied Mathematics). He lives in Cary, Illinois with his wife, son and daughter. He can be reached at jeff.illian@energymark.com.