CORBA Program Development
A D V E R T I S E M E N T
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:
- Supplier creates string.
- Calls send on Log Service.
- Log Service forwards string to Event Channel via
push.
- Event Channel buffers the string for Consumer.
- Consumer polls the Event Channel for a new string.
- Consumer retrieves the string from the Event Channel.
Listing 1.
module logging
{
interface LogService
{
void send(in string str);
};
};
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.
Listing 2. LJEventChannel.java
import org.omg.CosNaming.*;
import org.omg.CosEventComm.*;
import org.omg.CosEventChannelAdmin.*;
import com.visigenic.vbroker.services.CosEvent.*;
import java.io.DataInputStream;
import org.omg.CORBA.SystemException;
import java.util.*;
import logging.*;
class LogServiceImpl extends _PushSupplierImplBase
implements LogServiceOperations
{
private org.omg.CORBA.ORB orb;
private org.omg.CORBA.BOA boa;
private PushConsumer _pushConsumer;
public void disconnect_push_supplier() {
System.out.println(
"LogServiceImpl.disconnect_push_supplier() called");
try {
boa.deactivate_obj(this);
}
catch(org.omg.CORBA.SystemException e) {
e.printStackTrace();
}
}
LogServiceImpl()
{
System.out.println(
"in LogServiceImpl constructor, creating EventChannel");
String[] args = null;
try
{
orb = org.omg.CORBA.ORB.init(args, null);
boa = orb.BOA_init();
EventChannel channel = null;
ProxyPushConsumer pushConsumer = null;
try
{
String name = args[0];
System.out.println(
"In LogServiceImpl: binding to channel_server");
channel = EventChannelHelper.bind(orb,
"channel_server");
System.out.println(
"Created event channel: " + channel);
if(channel == null)
{
System.out.println(
"ERROR: Failed to bind to Event Channel ... bailing");
}
else
{
pushConsumer =
channel.for_suppliers().obtain_push_consumer();
System.out.println(
"Obtained push consumer: " + pushConsumer);
}
if(pushConsumer == null)
{
System.out.println(
"ERROR: failed to obtain push consumer");
}
else
{
PushConsumer clone =
PushConsumerHelper.narrow(pushConsumer._clone());
_pushConsumer = clone;
System.out.println(
"Connecting to Push Supplier ...");
pushConsumer.connect_push_supplier(this);
}
}
catch(org.omg.CORBA.SystemException e)
{
e.printStackTrace();
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
public void send(String str)
{
try
{
org.omg.CORBA.Any message = orb.create_any();
message.insert_string(str);
System.out.println(
"Supplier pushing: " + message);
_pushConsumer.push(message);
}
catch(Disconnected e)
{
System.out.println("Disconnected");
}
catch(SystemException e)
{
e.printStackTrace();
disconnect_push_supplier();
}
}
}
public class LJEventChannel
{
public static void main(String[] args)
{
org.omg.CORBA.ORB orb =
org.omg.CORBA.ORB.init(args,null);
org.omg.CORBA.BOA boa =
orb.BOA_init();
// create the logging server
LogServiceImpl new_service =
new LogServiceImpl();
LogService service =
new _tie_LogService(new_service,
"LogService");
if( service == null )
System.out.println(
"service is NULL!!!!!");
try
{
org.omg.CORBA.Object objRef =
orb.resolve_initial_references("NameService");
org.omg.CosNaming.NamingContext rootContext =
org.omg.CosNaming.NamingContextHelper.narrow(objRef);
NameComponent comp1 = new NameComponent(
"Linux Journal","");
NameComponent [] name = {comp1};
NamingContext ljcontext =
rootContext.bind_new_context(name);
NameComponent comp2 = new NameComponent(
"LJEventChannel","ec");
NameComponent [] name2 = {comp2};
ljcontext.rebind(name2,service);
}
catch(Exception e)
{
System.out.println("Exception: " + e);
}
// export the object reference
boa.obj_is_ready(service);
System.out.println(service + " is ready.");
// wait for requests
boa.impl_is_ready();
}
}
|