Sharing data between programs on different machines, written in different languages, has always been a problem. CORBA makes it rather easier.
Introduction
The concept of distributed computing has been around for years, as evidenced by technologies such as remote shells, remote procedure calls (RPCs), and the ubiquitous web protocol HTTP/CGI. The Common Object Request Broker Architecture (CORBA) [1] takes this concept to a higher level by introducing object-oriented (OO) design principles (i.e., encapsulation, inheritance, and polymorphism) as well as platform and language independence. CORBA brings all this to distributed programming without making it any harder than the techniques you currently use for non-distributed programming.
This article briefly covers some CORBA basics and then guides you through the development of a simple client/server application in the following flavors:
1) a baseline non-distributed (single process) C++ application
2) a distributed C++ client/server application using CORBA
3) a distributed Java client and C++ server (above) using CORBA
CORBA in Brief
The Object Management Group (OMG) is an 800+ member consortium that develops the CORBA standard. This standard defines how distributed objects cooperate regardless of their location, platform, and programming language. CORBA version 2.0 introduces and defines the Internet Inter-ORB Protocol (IIOP), which insures vendor interoperability over TCP/IP (the lingua franca of the Internet) based networks. IIOP enables you, for example, to develop a PC-based Java client with one vendor's CORBA product and transparently communicate with a Unix-based C++ server built with another CORBA-compliant product. This interoperability combined with an OO design paradigm makes CORBA a very powerful integration technology. See the online resources listed at the end of this article for more introductory information on CORBA.
The OMG also specifies extensions to the core standard, called CORBA Services, that add the scalability and robustness required of real-world applications. Examples of standardized services are Naming, Event, Transaction, Persistence, and Security (the first two being the most widely available). The service specifications are freely available at the OMG website (see the online resources section) and designed by industry experts, saving you both time and expense.
CORBA Architecture
CORBA relies heavily on the Proxy design pattern [2] to achieve platform and language independence. A proxy is a surrogate for another object. In CORBA's case, a proxy acts as a client-side reference to a "real" server-side object. A client invokes a method on the local proxy object, which packs the input parameters for delivery to an Object Request Broker (ORB) (see Figure 1) . The ORB then locates the desired service and passes the input parameters to the appropriate Object Adapter (OA), which in turn invokes the actual method. Output parameters and return values, if any, follow the reverse path back to the client. From the client's perspective, the proxy acts just like the real object since it hides all the communication details within itself.
CORBA IDL
CORBA also achieves platform and language independence through its Interface Definition Language (IDL). As the name implies, you use IDL to define the interfaces for your distributed services. Defining interfaces is conceptually similar to defining abstract C++ classes with only pure virtual member functions. You then use an IDL compiler to generate the particular language mapping(s) for your development environment. Official mappings exist for C, C++, Smalltalk, Ada, COBOL, and Java as of this writing. Thus, a single IDL specification suffices for developing interoperable distributed objects in a heterogeneous environment.
IDL syntax closely resembles C++ syntax in that it supports the following:
- Basic types: short, long, float, double, char, boolean, and octet
- Complex types: structures, unions, arrays, enumerations, strings and typedefs
- Exception types: system and user-defined
- Interface type: the conceptual equivalent of a C++ abstract base class with pure virtual methods
- Modules: the equivalent of C++ namespaces
- (Interface) Inheritance: single and multiple
- C++ style comments and preprocessor directives
IDL adds support for the following:
- Object type: the root interface for all other interfaces
- Any type: a general purpose, type-safe container for any other data type
- Sequence type: the conceptual equivalent of STL vectors
- in, inout, and out specifiers: for specifying method parameter directionality
CORBA had to take a pragmatic approach to its language support since it had to map into many other, potentially non-OO, languages. As a result, IDL does not support the following:
- Pointers: all non-interface types are passed by value while interface types are passed by reference.
- Overloading: you cannot define two methods with the same name and different parameters.
- Overriding: you cannot re-declare a method in a derived interface
A "Fortune Teller" Example
Here's a concrete example using a simple "fortune teller" service. A client of the fortune service submits a month value to a fortune-teller factory [2] which in turn provides a fortune-teller server (object) for that specific month. Clients use the fortune-teller server to retrieve their fortune message and lucky number and print them to the screen.
I present this example in three different versions. The first develops a baseline (non-CORBA) client/server application as a single C++ process. The second creates individual C++ client and server programs that communicate via CORBA. The third develops a Java client that communicates with the existing C++ server via CORBA. Hopefully, these last two examples will show you how easy it is to produce distributed systems with CORBA.
The C++ programs were compiled on a Windows 95 machine using MS VC++ v5.0 and Object-Oriented Concepts' (OOC) OmniBroker/C++ v2.0.2 mapping. The Java client was compiled using Sun's JDK v1.1.5 and used OOC's v2.0.2 Java mapping. The source code should be portable to any other OmniBroker supported platform with very little modification. See page 3 for information on how to download the source for these examples.
Example 1 C++ without CORBA
Listings 1 through 3 cover the source code used for this example. In Listing 1, fortune.h declares the Teller_impl and Factory_impl classes. The Factory_impl::getFortune method takes an integer month value and returns a Teller_impl object created specifically for the given month. getFortune throws an outOfBounds exception, naturally enough, whenever the given month value is out of bounds (i.e., not between 1 and 12 inclusive). The Teller_impl class declares the methods to get a fortune message and a lucky number.
Listing 2 contains the source for fortune.cpp, which implements the above classes. The Factory_impl constructor creates a list of Teller_impl objects which are initialized from a static array of fortune messages. The Factory_impl::getFortune method either returns a pointer to the appropriate Teller_impl object in its list or throws an exception for invalid input. The Teller_impl constructor stores a copy of its string argument (the fortune message) and keeps a running count of Teller_impl objects, whose current value is used for the lucky number. The Teller_impl::getMessage and Teller_impl::getLuckyNmuber methods return their respective stored values. The destructors clean up any dynamically allocated memory.
Driver.cpp, shown in Listing 3, drives this version of the fortune-teller program. The driver checks that at least one command-line argument was passed and then instantiates a Factory_impl object. The program interprets the first command-line argument as a month value and uses it to obtain a pointer to the desired Teller_impl object. The program then uses this pointer to display the fortune message and lucky number to the screen. All the methods are wrapped in a try-catch block to handle possible exception conditions.
To run this program use the following command line:
C:\example1> driver <month value>Example 2 Developing with CORBA
CORBA development requires a few additional steps over the previous example. This process is summarized in Figure 2. The first step is to develop an IDL specification that describes the system. Step 2 passes this specification through the IDL compiler to generate client-side "stub" code and server-side "skeleton" code. Referring back to Figure 1, you can think of stub and skeleton code as the equivalents of the Proxy and OA components respectively. In steps 3 and 4 you develop the server implementation and driver code and link it with the generated skeleton code to produce a server executable. You then develop your client driver in step 5 and link it with the generated client stub code in step 6 to produce a client executable. You now have the components for a simple distributed system.
Writing the IDL Code
The IDL specification used for this example is shown in Listing 4. Fortune.idl is a direct translation of fortune.h (Listing 1) into CORBA IDL. I have made this direct translation for comparative purposes and not because of any imposed language limitation. A direct translation from an existing system interface into IDL is generally considered a poor design choice. The interfaces have been wrapped in a module block, which is not explicitly necessary but is good practice. (Note: OmniBroker prefixes the generated classes with the module name, which is an approved CORBA mapping alternative, since many compilers do not yet support namespaces.) Step 1 is now complete.
Step 2 is to run the (C++) IDL compiler on fortune.idl using the following command:
C:\example2> idl fortune.idlThe idl compiler will generate the following four files in the current directory:
- fortune.h, fortune.cpp: the client-side stub code
- fortune_skel.h, fortune_skel.cpp: the server-side skeleton code
The content of these files is not listed since the details are not particularly important to this discussion. Keep in mind that the particulars (e.g., generated file names and class names) in the examples below are specific to OmniBroker. Other vendors may use a different naming scheme but the procedure will be the same.
Implementing the Server in C++
Step three puts "meat" on the skeleton code by developing the implementation classes. Listing 5 (fortune_impl.h) shows the class declarations for Fortune_impl and Teller_impl. This is essentially the same source code shown in Listing 1. The main difference is that both classes now inherit from their respective "skel"-eton classes, which contain the server-side hooks into the ORB. (CORBA also defines a delegation model for implementation objects, but the inheritance model is the easiest to understand and will be used here.) I've also changed the return values for the Teller_impl::getLuckyNumber and Factory_impl::getFortune methods to CORBA_Long and FortuneModule_Teller_ptr (a generated convenience type) respectively for CORBA compliance. You can always check the method signatures in the IDL-generated files to find out what types you should use. The last difference is that the outOfBounds exception declaration is omitted in Listing 5 since it is already defined in the IDL specification.
The implementation code in Listing 6 (fortune_impl.cpp) is also very similar to the code shown in Listing 2. The method signatures have changed, as discussed above, and the source now #includes fortune_impl.h and CORBA.h instead of fortune.h. String management is so common that CORBA introduces its own methods, which allows vendors to optimize their allocation schemes without overriding the global new and delete operators. Therefore, string allocations that used new and delete in Listing 2 have been changed to use CORBA_string_alloc and CORBA_string_free methods in Listing 6. The CORBA_string_alloc method automatically allocates extra storage for the terminating null character, so the "+1" argument in Listing 2 is no longer necessary.
Memory management becomes a complicated issue in distributed systems. As a result, you need to be aware of client- and server-side responsibilities regarding storage associated with method parameters and return values (which should be spelled out in your ORB's documentation). In this case, string and object return values must be "duplicated" prior to returning any references from the server. As a result, the Teller_impl::getMessage and Factory_impl::getFortune methods use the appropriate "duplicate" methods before returning any references. By the same token, freeing object references requires the use of the appropriate "free" methods: CORBA_string_free for strings and CORBA_release for objects. CORBA uses a reference counting scheme for objects, which is managed via the "duplicate" and "release" methods. Using the delete operator would bypass this reference counting mechanism and potentially leave dangling references.
Listing 7 (server.cpp) shows the driver code for the server process. First, it includes the implementation header. Second, the server initializes the ORB and OA objects prior to creating any implementation objects. The server then creates a factory object and saves its interoperable object reference (IOR) to an external file so the client can subsequently obtain a handle to it. With everything now in order, the server enters the main loop. This source file completes step 3. Producing an executable from it completes step 4.
Besides the signature and memory management changes discussed above, nothing really appears in the source to indicate that the implementation code is distributed. All the communication details have been hidden inside the skeleton code; you write your implementation code just like before. If you have ever had to program using a low-level (and error prone) socket API, then you should already be in love with CORBA at this point.
Implementing the Client in C++
Steps 5 and 6 are very straightforward. The stub header is included in this case, as shown in Listing 8 (client.cpp). The client initializes the ORB prior to working with any CORBA objects (the OA is not required for client programs). The client then obtains the externalized reference generated by the server. This reference is returned as a generic Object type, the root of all interface types, and must be "narrowed" to the appropriate interface. A simple C++-style cast operator would not work here since the narrowing process may involve communications with the server. At this point, the object references can be used just like handles to the real objects themselves. The rest of the code is essentially the same as that shown in Listing 3. (The _var types used in Listing 8 are generated "smart pointer" convenience types that automatically release their objects when they go out of scope.)
The above client code is wrapped in a try-catch block, as before, but contains a new catch statement for CORBA_SystemException exceptions. Now that method invocations (can potentially) go across a network, there are more things that can go wrong than in the first example. In fact, with very few exceptions, every IDL method is capable of throwing a CORBA_SystemException (or a derivative) and should be handled. Compiling and linking this code produces a client executable and completes step 6.
Again, aside from obtaining the external object reference and handling an extra exception type, the source gives no indication that this code is distributed. All the communication details are hidden on the client side as well.
In order to run multiple clients simultaneously, use:
C:\example2> start server C:\example2> clientExample 3 Java Client with CORBA
CORBA/Java development follows the same steps shown in Figure 2. For this example, only the client portion will be developed since the server side is already built. To carry out step 2, run the IDL specification from the last example through the (Java) IDL compiler (jidl) using the following command:
C:\example3> jidl ..\example2\fortune.idlThis command generates Java stub, skeleton, and support classes in the Fortune subdirectory.
The client code for this example is shown in Listing 9. (Although I don't cover Java syntax here, it is similar enough to C++ that readers should be able to understand the code and following discussion.) First, the appropriate packages are imported at the top of the file. Listing 9 then defines a client class with a static main method. This method performs the same functions as the previous C++ client code: it initializes the ORB, obtains the external reference to the factory implementation, loops on user input and then displays the results to the screen. Writing this source completes step 5 and compiling it completes step 6.
To run this example, use the following commands:
C:\example3> start ..\example2\server C:\example3> java clientTo run multiple clients simultaneously use:
C:\example3> start java clientConclusion
I hope this article has shown how easy it is to develop distributed object systems with CORBA, and that CORBA delivers on its promise of platform and language independence. CORBA technology is mature, industry supported, and not necessarily expensive. There are plenty of quality CORBA ORBs available at little or no cost. In fact CORBA ORBs are currently being shipped with the latest versions of Netscape's browser and Sun's JDK.
There are many books and Internet sites that provide good information on CORBA. Check out the Online Resources and Further Reading sections (below) for some starting points. Also, be sure to read the sidebar on CORBA "Gotchas" before diving headlong into your own CORBA development. o
References
[1] Object Management Group. The Common Object Request Broker: Architecture and Specification, revision 2.1, Document 97-09-01 (Object Management Group, Framingham, MA, 1997).
[2] E. Gamma, R. Helm, R. Johnson, and J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, Reading, MA, 1995).
Online Resources
Source code for the examples used in this article
ftp://ftp.mfi.com/pub/cuj/1998/1604/resendes.zipOmniBroker CORBA 2.0 compliant ORB (free for non-commercial use)
http://www.ooc.com/ob/Java Development Kit. Sun's Java development environment for Win95/NT and Solaris (free)
http://www.javasoft.com/products/jdk/1.1/index.htmlMicrosoft Visual C++. Microsoft's C++ development environment
http://www.microsoft.com/products/prodref/197_ov.htmObject Management Group. The source for CORBA information and specifications
http://www.omg.org/CORBA 2.1 Specification
http://www.omg.org/corba/corbiiop.htmCORBA Services Specification
http://www.omg.org/corba/sectrans.htmFree CORBA Page. A great resource for free/trial/commercial CORBA ORBs
http://adams.patriot.net/~tvalesky/freecorba.htmlDoug Schmidt's homepage. A great resource for CORBA tutorials, articles, and source code.
http://siesta.cs.wustl.edu/~schmidt/CORBA FAQ
http://www.cerfnet.com/~mpcline/Corba-FAQ/index.htmlFurther Reading
Jon Siegel. CORBA Fundamentals and Programming (John Wiley & Sons, 1996). ISBN: 0471121487.
T.J. Mowbray and R. Zahavi. The Essential CORBA: Systems Integration Using Distributed Objects (John Wiley & Sons/OMG, 1995). ISBN: 0471106119.
Orfali, Harkey, and Edwards. The Essential Distributed Objects Survival Guide (John Wiley & Sons, 1996). ISBN: 0471129933.
Robert Resendes holds an MS in Electrical Engineering from the University of Massachusetts and currently works for the Naval Undersea Warfare Center. He is currently using CORBA, C++, and Java to build distributed object systems as part of an internal R&D project. He can be reached at resendes@cstlmail.npt.nuwc.navy.mil.
Marc Laukien is founder and president of Object-Oriented Concepts, Inc. He is the main author of OmniBroker, which is a CORBA-2.0 compliant ORB that supports C++ and Java mappings. His current technical interests involve distributed systems, object-oriented programming, and design patterns. He can be reached at ml@ooc.com.