CORBA Program Development, Part 3

J. Mark Shacklette

Jeff Illian

Issue #63, July 1999

A look at CORBA implementations in Java to provide interoperability between platforms.

Over the past couple of months we have endeavored to present an overview of distributed application development on Linux using CORBA. In the first article we dealt with the question “what is CORBA?” and covered the basics of using an ORB with a simple client and server. The second article introduced two of the most common OMG (Object Management Group)-supported services, the Naming Service and the Event Service, and provided an example using both. In this, our third and last article, we will be digging a little deeper into the approaches used so far by providing an introduction to “tie”, which is a delegatory method of binding to a remote object. To date, all of our examples have been implemented in C++, but we must remember that CORBA is designed around the concept of both platform and language independence. In order to further demonstrate the concepts of platform and language independence, this time we will be presenting an example using two different operating system platforms, one of which is Linux, and our implementation will be in Java instead of C++.

While we used a combination of Linux and Windows when writing this article, our code should run on any combination of Java-enabled platforms. That is to say, you should expect this code to run equally well in a Linux+Solaris environment, or an HPUX+IRIX environment. For part 3, we have chosen to implement our example using one of the most popular Java-based ORBs, VisiBroker from Inprise Corporation. Inprise kindly and officially sanctioned all the research we conducted.

As we discussed in the second part, the OMG specification describes how an ORB bootstraps itself in order to find the location of the Common Object Services (COS) such as the Naming Service, Trader Service or Implementation Repository. This bootstrapping process is hidden within each vendor's implementation of the resolve_initial_references method. The real trick in achieving interoperability between ORBs is to figure out how to bootstrap against another ORB. VisiBroker provides a simple proprietary naming service in the form of a binary executable called the osagent. The osagent provides this basic location service while at the same time supplying a measure of fault tolerance and load balancing of objects. In the case of VisiBroker, the implementation of resolve_initial_references has the ability to locate an osagent and then ask it for direct references to other COS services. Additionally, both VisiBroker's Naming Service and Event Service will try to find an osagent when started in order to register their IORs (interoperable object references). In our example, the osagent will supply our sample clients and servers with the references to VisiBroker's Naming and Event Services and any dependent objects.

Unfortunately, the osagent is a platform-specific binary application that has not yet been ported to Linux. The good news is only one osagent needs to be running on a local network for a single or group of VisiBroker-based applications to use. For our examples, we'll be running osagent on an Inprise-supported operating system. Quoting directly from VisiBroker's 3.4 release notes, “With the exception of the osagent, osfind and locserv executables, the VisiBroker for Java ORB is written entirely in Java and can run in any Java 1.1- or Java 2-compatible environment.” So we will run osagent on a supported platform and do all our other work on Linux. Since a simple ORB is also bundled with Java 2, be sure to read the release notes before trying to run VisiBroker using Java 2. The significant news is, according to James Ferguson, Senior Product Manager for VisiBroker, “Inprise has seen growing corporate demand for VisiBroker on Linux. This is just another indication of the rapid growth of Linux in corporations. To participate in this new growth market, Inprise will be making significant announcements regarding the availability of VisiBroker for Java and C++ on Linux.” If you'd like to register your support for that direction or find out more information, Inprise suggests you direct feedback to news://forums.inprise.com/inprise.public.visibroker.

The sample code in this article uses a popular OMG CORBA concept called “tie”. In part one, our server-based object was implemented by inheriting from the skeleton implementation _sk_InterfaceName, a class that was generated by OmniORB's IDL (Interface Definition Language) to C++ compiler. By inheriting from this base class, the developer can concentrate on implementing only the interfaces that have been defined in the IDL and not having to worry about all of the code that actually makes the CORBA communication possible. Multiple inheritance is sometimes used to allow for the inheritance of the skeleton provided by the IDL compiler, sometimes called the BOA (basic object adapter) implementation, as well as allowing the implementation to inherit from some other parent class. When the implementation is written in Java, which doesn't support multiple inheritance, this situation becomes problematic. Without multiple inheritance, it becomes impossible to inherit both a skeleton base class and another base class, such as an application framework.

To address this, the OMG specification defines a delegate class called the tie class. The IDL compiler for OmniORB generates an interface called _tie_InterfaceName to address this role, while VisiBroker's Java IDL compiler generates a Java interface called InterfaceNameOperations. Rather than inheriting from the base implementation class, the developer instead implements a generated interface called an operations interface, which contains no methods, attributes or properties other than those defined in the IDL. The operations class is then passed in the constructor to a wrapper tie class which implements each method in the interface by delegating to the operations interface object. The tie class implements the orb functionality supplied by the generated base implementation. The tie object is then the component that is actually bound to the orb. Since the implementation of our interface is no longer inheriting from the base implementation, this frees up the developer to inherit from another base class such as an application framework.

In order to better understand how to implement a CORBA solution in Java, let's compare a Java implementation to a C++ implementation, which readers of parts one and two should be familiar with. There are several differences in the way a CORBA application is implemented in Java versus C++. Concentrating on the VisiBroker for Java implementation and the VisiBroker for C++ implementation, the difference can most obviously be seen in the number of files generated by the Java idl2java compiler and the C++ idl2cpp compiler. Basically, the idl2java creates about twice as many files as the idl2cpp compiler. The idl2java compiler even creates a new subdirectory to hold all the new files it generates. Certain flags can help control the number and types of files generated. When you run idl2cpp without any flags on, this very simple IDL file (example.idl) is generated:

{
   interface SimpleInterface
   {
      void SimpleOperation(in short x);
   };
};

Also, the following files are created:

  • example_c.hh: contains the class definitions for SimpleInterface, along with supporting classes.

  • example_c.cc: contains stub code to be compiled and linked with the client, which provides support functions (such as _ptr and _var definitions).

  • example_s.hh: contains the definitions for the _sk_Account skeleton class for inheritance with the bind method, along with the tie classes for delegation in the tie method.

  • example_s.cc: contains the internal skeleton's marshaling code, etc.

The same IDL file will generate the following files when compiled by the idl2java compiler (note these files would be contained in a subdirectory called Example):
  • SimpleInterface.java: provides a simple public interface definition for the SimpleInterface declared in the IDL. This interface simply mimics, in Java, the interface defined in the IDL. The actual implementation of this Java interface is contained in the _SimpleInterfaceImplBase class (supplemented by the actual implementation you will write).

  • SimpleInterfaceHelper.java: provides helper methods for SimpleInterface clients. Among these helper methods are the ever-important bind method overloads, as well as a narrow function (for use in the tie method).

  • SimpleInterfaceHolder.java: provides a Java class that holds a public instance of a SimpleInterface object. It provides a wrapper class for a SimpleInterface object, which is necessary to allow the passing of SimpleInterface objects as out and inout parameters in function calls declared in an IDL interface.

  • SimpleInterfaceOperations.java: provides classes that assist in the implementation of the tie method. (Not created if the -no_tie flag is given).

  • _SimpleInterfaceImplBase.java: provides an abstract public Java class that implements the server-side skeleton for SimpleInterface. This base class itself extends org.omg.CORBA.protable.Skeleton, and implements Example.SimpleInterface. Your implementation inherits from this base when using the bind over the tie method.

  • _example_SimpleInterface.java: provides simple code that you can fill in to implement the SimpleInterface object on the server side.

  • _st_SimpleInterface.java: provides a Java class that implements the client-side stub, which proxies the SimpleInterface object on behalf of the client. The client makes calls on this proxy.

  • _tie_SimpleInterface.java: provides the delegation class used to implement the tie method on the server side.

Exactly twice as many files are generated by default by the idl2java compiler as compared to the idl2cpp compiler. This is partially because of language differences between the two languages. Java has no user support for pointers (the language has support only for pass-by-reference for objects), so holder classes are needed to support out and inout IDL parameter types. Every ORB-defined, as well as every user-defined, type has an associated holder class defined to support the IDL out and inout semantics. You can think of holder classes as pure wrapper classes containing a value that is an instance of the actual fundamental class. Holder classes implement the org.omg.CORBA.portable.Streamable interface, which allows for the reading and writing of objects and streams. Holder classes are named by simply taking the base class name and appending “Holder” to it.

The idl2java compiler also generates a helper class for every interface. Helper classes offer a number of static methods which provide clients with vital functionality. These include the bind and narrow methods, which allow clients to connect to server-based objects. They also provide read and write methods to assist the Holder classes in translating between I/O streams and native object types. They also supply type code information that is useful when it comes to Any types and the Dynamic Invocation and Dynamic Skeleton interfaces. Type codes provide for runtime detection of type mismatches, along with metadata support for runtime type information. Since Java is primarily an interpreted language, it must be careful of added memory constraints. Helper classes help by off-loading several rarely used methods, such as bind and narrow, so that the actual object implementations can avoid loading these methods. You might call the calculate method a hundred times a second, but you'll usually call bind only once.

Beyond the generated helper classes, the Java and C++ implementations using CORBA look very similar. For example, the only true difference between finding a naming context under C++ and Java is the use of the helper class to do the narrow.

Mico C++:
CORBA::Object_var nsobj =
  orb->resolve_initial_references ("NameService");
CosNaming::NamingContext_var nc =
  CosNaming::NamingContext::_narrow (nsobj);
VisiBroker Java:
org.omg.CORBA.Object objRef =
  orb.resolve_initial_references("NameService");
org.omg.CosNaming.NamingContext rootContext =
  org.omg.CosNaming.NamingContextHelper.narrow
      (objRef);

Our example demonstrates a simple logging facility that makes use of the VisiBroker for Java Naming and Event Service, as well as demonstrates the use of the tie mechanism in Java. The example offers a Log Service and two clients: one supplies events (messages) to the Log Service, the other consumes (reads) those messages or events.

Our example for this article is a simple message delivery service in the form of a logger, implemented using classes that interact with the VisiBroker for Java Event Service. A Supplier generates strings and then delivers them to a Log Service, a Java class that extends the Push Supplier interface. The Log Service publishes a function called send which allows one of its clients (a Supplier) to publish events (send messages) to the event queue. The send method forwards that event by pushing it onto the event queue. The PullConsumer, another client in the scene, binds to the event channel, then proceeds to pull the events issued by the Supplier from the queue. The example demonstrates both the use of the Naming and Event Services in VisiBroker for Java as well as the tie mechanism. (The LogService is implemented using the tie method.) The example has been kept simple in order to easily communicate the issues involved. Error handling, for example, has been kept to an absolute minimum so as to not obscure the foundational elements. Thus, the path a string travels through the system is as follows:

  1. Supplier creates string.

  2. Calls send on Log Service.

  3. Log Service forwards string to Event Channel via push.

  4. Event Channel buffers the string for Consumer.

  5. Consumer polls the Event Channel for a new string.

  6. Consumer retrieves the string from the Event Channel.

Listing 1. EventCannel.idl

The IDL for our logging facility is extremely simple (see Listing 1). It defines a module called logging and a single interface named LogService that implements a single function, called send, that accepts a single string parameter. This string is passed to the Log Service, which is then placed on the Event Channel, where it awaits being read by the Consumer. The Consumer polls the Event Channel periodically, checking to see if a new event (String message) has been delivered. If it has, it pulls that String from the Event Channel and prints it to STDOUT.

IDL modules are mapped, in CORBA's Java mapping, to Java packages. Therefore, the logging module in the IDL is mapped to a logging package that, by default in Java, is a subdirectory under the directory which contains the IDL file. It is the logging package (directory) that contains all the files generated by the idl2java compiler. When built, the logging directory contains eight Java files, generated by the idl2java compiler. The directory contains class definitions for the LogService interface, Helper and Holder classes which we mentioned above, and the tie and ImplBase classes for delegation and binding.

Listing 2. LJEventChannel.java

Listing 2 shows the LJEventChannel.java source, which defines two classes. LogServiceImpl and LJEventChannel. The LogServiceImpl class extends the _PushSupplierImplBase base class, and implements LogServiceOperations. The LogServiceOperations class has the capabilities necessary for the tie mechanism, which we will use to connect to the LogServiceImpl object. The LogServiceImpl class provides the core functionality for binding to the proper VisiBroker Event Service channel. Since LogServiceImpl extends _PushSupplierImplBase, it is able to function in the role of a Push Supplier vis-à-vis the Event Service (for more information, see last month's article).

The meat of the LogServiceImpl class is in its constructor. When a new LogServiceImpl object is created, the constructor first binds to the ORB via an org.omg.CORBA.ORB.init call. Then, it connects to a particular event channel, “channel_server”, in order to pass strings via the Push Consumer proxy it creates. The actual process of connecting to the Event Channel was covered in detail last month; however, we will summarize the steps here briefly.

First, the bind method is called on the EventChannelHelper class that is part of VisiBroker's Event Service. The Naming service is not necessary to connect to the Event Service, because the osagent facilitates the binding by using its own simple naming service.

Once an EventChannel is bound, a Push Consumer proxy is obtained from the Event Channel, and then our LogServiceImpl object is connected to the proxy. This allows us to make calls on the proxy. From this point on, any supplier who calls the send method on our LogServiceImpl object will cause our implementation to call the push method on its Push Consumer proxy. This is done with the line _pushConsumer.push(message). Notice that, as usual, we've packaged our string to be sent in an Any type, which the Event Service requires for transmission.

Class LJEventChannel consists of a single main method which, after binding to the ORB and initializing the Basic Object Adapter for our object, creates a new LogServiceImpl object described above. The tie method is used in the binding process, which means we will be using delegation instead of inheritance in our communication with the object's implementation. After we have tied to our new LogServiceImpl delegate named “new_service”, we then bind that service object to the Naming Service under the component path Linux Journal:LJEventChannel. This allows any client object on any machine in the visible network to connect to our new_service object via this naming convention, without having to know the name or IP address of the hosting machine.

Once the new_service delegate has been bound to the Naming Service, the BOA is advised that the object is ready and available and the implementation of the server is complete.

At this point, an implementation of the LogService interface defined in the IDL has been created and published and is now available for calls from clients wishing to use its send method to post messages to the Event Channel.

Listing 3. PushSupplier.java

Listing 3 shows PushSupplier.java, the supplier in our application. Class PushSupplier consists entirely of a single main method, which after initializing the orb with org.mg.CORBA.ORB.init, stores the name of the supplier which was optionally entered on the command line when the supplier was started. This arbitrary name allows you to name your suppliers Supplier1, Supplier2, etc., so that you will know, on the consumer side, which supplier's string was obtained. After initializing the ORB, a LogService reference named logger is created. Then we enter a try block, which seeks to bind to the LogServiceImpl object already created using the Naming Service. The supplier calls the resolve_initial_references method on the orb object, obtains a root context, creates the appropriate name components, and calls resolve on the root context using the created name component array. The generic object reference returned is then narrowed by a call to narrow using the LogServiceHelper object.

Assuming the logger object is not null, we then enter a loop that continually sends a string to the LogServiceImpl object via its send method. This continues until the user interrupts the supplier with ctrl-C.

Listing 4. PullConsumer.java

Listing 4 shows the PullConsumer class, which extends the _PullConsumerImplBase base class of the VisiBroker Event Service. After initializing the ORB and BOA, the PullConsumer object attempts to bind to the Event Channel by calling the bind method on the EventChannelHelper object. Then a new PullConsumer object is created, which implements the disconnect_pull_consumer method required by a PullConsumer. The reason a new PullConsumer must be created is because we need an object reference to pass to the BOA's obj_is_ready and the proxy Pull Supplier's connect_pull_consumer method. Since we are in the main method which is static, no “implicit this” reference is available to us. Therefore, we need to create a new object in order to obtain a reference to pass. Once a new PullConsumer object is created, the BOA is advised that the object is ready. After this, a Pull Supplier proxy is created via a call to the bound channel's obtain_pull_supplier method. Once the proxy is created, the PullConsumer object is connected to the proxy by calling connect_pull_consumer on the proxy, passing it the PullConsumer object.

At this point, a while loop is entered, and the consumer continually calls try_pull on the Pull Supplier proxy. If the proxy finds an event, then that event is returned, the PullConsumer object prints that message to standard output and the loop restarts.

You can try out this application by first running a single PushSupplier and PullConsumer locally on the same Linux box. (See the README.install and README.run instructions that accompany the code for details on the setup, building and launching of the applications.) Then you might want to launch another PushSupplier and notice that the PullConsumer automatically begins to process events from the new PushSupplier as well. (You might want to name the PushSuppliers as they are launched—see the README.run instructions on how to do this.) Then launch a new PullConsumer over on the Windows (or other OS) box, and watch how events from the two suppliers are conveyed to the two consumers, one of which is now running on the Windows machine. Finally, launch another PushSupplier on the Windows machine and watch how two consumers process strings created and delivered to the same event channel by three separate suppliers. Even though the code is simple, the project implemented here is quite capable and has some broad implications which you should explore.

Conclusion

In these three articles, we have attempted to introduce you to CORBA programming on Linux. Linux is a robust platform for developing CORBA applications, and the CORBA is quite versatile in terms of its capabilities, services, platform independence and language independence. We hope these articles have spurred your interest in both CORBA and Linux and wish you success in exploring these issues more fully on your own. For those who wish to learn more, visit the Free CORBA Page at adams.patriot.net/~tvalesky/freecorba.html. It has continued to grow in terms of subject matter as well as quality, including some information on the CORBA implementation in Java 2.

Resources

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.