Bipolar CORBA Objects in Java

  Ronald Mak is a software developer, architect, and instructor in Silicon Valley. He can be contacted at [email protected].

JAVA AND CORBA are a powerful combination for developing client-server software. But CORBA objects, even ones written in Java, can be a challenge to debug. Many Java debuggers have difficulty single-stepping from local application code into code that is running in a remote object.

A bipolar CORBA object is one written in Java in such a way that a Java client application can use it either remotely or locally. The client application can bind to the object on the CORBA bus, or it can use the new operator to instantiate the object in the local address space. Once the application has a reference to a bipolar object, it is transparent to the application, whether the object is remote or local. The exact same method code in the object will run in either case.

By making the remote objects bipolar, we can ease program development. A locally instantiated bipolar object does not require CORBA runtime code. Not only will this simplify debugging the object, but we've also temporarily eliminated one major domain where a program can fail.

Figure 1.

Figure 1. The inheritance approach.

Another justification is performance and flexibility. Multiple client applications can share remote objects that run on a server. There may also be server applications that want to use those objects, and if they run on the same machine as the server objects, we can improve performance if the server applications create the shared objects locally on the server and thus bypass CORBA.

For a bipolar object to be useful, it must meet several criteria:

  1. It should be easy to make a remote object bipolar, and we must not violate any CORBA programming conventions.

  2. Once a client application has a reference to a bipolar object, it shouldn't need to know whether the object is running remotely or locally. We don't want to sprinkle if(local) and if(remote) tests throughout the program.

  3. The exact same code in the bipolar object must run whether the object is running remotely or locally. We don't want to debug two separate sets of code.
Let's look at a simple Hello object that would normally be accessed remotely via the CORBA bus. Its implementation is class HelloImpl in the hello package. When we run it from the command line on the server, we pass it two string arguments, a hello string and a goodbye string, such as:

java hello.HelloImpl "Guten Tag" "Auf Wiedersehen"

(It shall be a very remote object, indeed.) The object includes two public methods:

public String sayHelloTo(String name)
public String sayGoodbyeTo(String name)
When a client application calls the first method with a name string, such as sayHelloTo("Ron"), the method returns the hello string concatenated with the name, as in "Guten Tag, Ron!". Similarly, the second method returns the goodbye string concatenated with the name, as in "Auf Wiedersehen, Ron!"
There is a third public method:

public void initServer(
String helloString, String goodbyeString)
As we'll soon see, this method helps in making the object bipolar. Listing 1 shows the IDL file hello.idl for this example.

The key to making a CORBA object bipolar is the "tie" or delegation approach. To understand this, the UML diagram in Figure 1 displays the class hierarchy when using the standard inheritance approach. The diagram distinguishes three types of classes and interfaces:

  1. Fixed classes and interfaces that never change. These come from Java (e.g., java.lang.Object) or from CORBA (e.g., org.omg.CORBA.Object).

  2. Classes and interfaces that the idl2java compiler generates from the hello.idl file (e.g., _HelloImplBase; for this article, we use the compiler from Inprise's Visibroker.)

  3. Classes that we must write (e.g., HelloImpl).
We write the public class HelloImpl to implement the Hello object. This class must extend the generated abstract class _HelloImplBase, which contains code to do CORBA operations specific to the Hello object, such as marshaling the string parameters. The abstract class implements the generated Hello interface. This interface, shown in Listing 2, specifies the signatures of the three public methods initServer(), sayHelloTo(), and sayGoodbyeTo(), and it extends the CORBA interface org.omg.CORBA.Object.

Figure 2.

Figure 2. The "tie" approach.

Figure 2 shows the class hierarchy when we use the tie approach. In both figures, the shaded portion highlights the classes and interfaces that are common to both the inheritance and tie approaches.

The class we write, HelloImpl, now only needs to implement the generated interface HelloOperations, which, like the Hello interface, specifies the signatures of the three public methods. But unlike the Hello interface, HelloOperations does not extend org.omg. CORBA.Object. Listing 3 shows source file for the tie approach.

The tie approach generates an intermediary class _tie_Hello. This tie class extends _HelloImplBase (see Listing 4). The tie constructors set the private member variable _delegate to refer to the actual HelloImpl object. Each of the public methods initServer(), sayHelloTo(), and sayGoodbyeTo() invokes the corresponding HelloImpl method via _delegate.

The main() method of HelloImpl (Listing 3) is called whenever we start the program on a remote server. It first creates a HelloImpl object and calls its initServer() method to set the hello and goodbye strings that were passed in as command-line parameters. Then it creates a _tie_Hello object, passing the HelloImpl object to the constructor to set up its delegate reference. Finally, main() initializes the object request broker and puts the _tie_Hello object (using the instance name "HelloInstance") onto the CORBA bus to await requests from the client applications.

The primary advantage of the tie method is that it allows HelloImpl to be extended from another class, if necessary. A secondary advantage is that it enables us to make our Hello object bipolar.

THE LocalHello CLASS
To make our Hello object bipolar, we write a new class, LocalHello, that mimics the generated tie class _tie_Hello. In fact, we can see in Listing 5 that we copy code from _tie_Hello into the body of LocalHello.

In the constructor, we set the private variable _delegate to refer to a local instance of the Hello object created with the new operator.

The latter part of LocalHello "stubs out" the methods of org.omg.CORBA.Object. These stubs will never be called, but they do allow the class to implement the generated Hello interface, as shown in Figure 2.

The crucial fact that allows bipolarity is that both the _tie_Hello class and the LocalHello class implement the Hello interface.

Listing 6 shows a sample client application ClientApp that can access our Hello object either remotely via the CORBA bus or locally in the same address space. The program takes two command-line parameters. The first parameter is the letter "r," to indicate that we want to access the Hello object remotely; anything else says we want local access. The second parameter is a person's name.
For example, for remote access, we type:

java hello.ClientApp r Ron
and for local access, we type:
java hello.ClientApp x Ron
If the first command-line parameter of ClientApp is "r", then main() initializes the object request broker and sets variable helloObject by binding to the remote _tie_Hello object using the instance name "HelloInstance." However, if the first program parameter is not "r," then main() instead sets helloObject to a local instance of the Hello object with new LocalHello().

When we started the Hello object as a remote CORBA object, we passed it the hello and goodbye strings on the command line. In the local case, the client application calls the Hello object's public initServer() method to pass it these strings. In Listing 6, they are "Howdy" and "Bye-bye" (in a very local dialect).

The first command line below tells ClientApp to bind to the remote Hello object, and we see the two output lines from the remote object. The second command line below tells ClientApp to bind to a local Hello object, and we see the two output lines from the local object.

java hello.ClientApp r Ron
Guten Tag, Ron!
Auf Wiedersehen, Ron!

java hello.ClientApp x Ron
Howdy, Ron!
Bye-bye, Ron!
In the client, helloObject.sayHelloTo() and helloObject.sayGoodbyeTo() work whether variable helloObject refers to a remote or to a local Hello object. In either case, the exact same code in HelloImpl runs. Of course, if we only want local access to the Hello object, there is no need to run hello.HelloImpl from the command line to put an instance of the remote object on the CORBA bus.

Let us review how our example bipolar object helps us during program development. We want to debug the interactions between ClientApp and the Hello object.

We begin testing with a local Hello object. If there are any problems, we know the bug must be either in ClientApp or in HelloImpl because CORBA is not yet in the picture. A debugger should have no difficulty single-stepping between the two objects which are both running in the same address space.

After we've debugged the interactions between the two objects, we start up the Hello object remotely. Now, if testing reveals any problems, we can concentrate our debugging efforts on the CORBA connection. We've already debugged HelloImpl when it was running locally, and the same code runs even when we start it remotely.

The way we wrote ClientApp makes it easy to switch between running the Hello object locally and remotely. If we find a problem when the object is running remotely, we simply switch to running it locally for debugging. After fixing the bug, we start the new version of HelloImpl on the server and we can quickly switch back to running the object remotely for further testing.

Suppose we have another application, ServerApp, for example, that needs to use the Hello object, and the application happens to run on the same machine as the object. Then ServerApp can bind locally to the object and the two will interact without the performance penalties of CORBA.

The benefits of creating bipolar objects outweigh the costs, but we should consider what we have to pay.

Bipolar objects force us to employ the tie approach, which means a bit more development work and a few more source files for a project that could otherwise use the inheritance approach. The use of delegates exacts a small runtime performance penalty. Of course, the tie approach, as described by the CORBA documentation, has its own advantages independent of the creation of bipolar objects.

We need to maintain an extra source file, the one we called in our example. To create this file, we copy lines from the source files of the tie class (_tie_Hello in our example) and the class org.omg.CORBA.Object. This can be done with a script that automatically runs whenever either source file changes.

Debugging client-server applications is difficult if bugs can occur in the client code, the server code, or in the CORBA connections. We've seen how simple it is to make the remote objects bipolar, so that we can debug them initially in the local address space without CORBA getting in the way. Only after we've debugged the client-server interactions do we switch to running the server objects remotely on the CORBA bus.