CORBA Program Development
A D V E R T I S E M E N T
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.ssc.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.
// This is a very simple IDL file. It defines
// a single interface, called PushString, which
// contains a single function that takes a single
// input paramter of type string, which is mapped
// in c++ to a const char * (see PushString_i.h,
// for pushStr declaration in the implementation).
interface PushString
{
boolean pushStr(in string inStr);
};
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_PushString
Every 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.
Listing 2. C++ in Srv_Main.C File
#include
#include
#include "PushString_i.h"
int main(int argc, char **argv)
{
ofstream f_out;
f_out.open("ior.out");
if(!f_out)
{
cerr <<
"\nCould not open output file ... Disk full?"
<< endl;
exit(-1);
}
cout << "Server starting, creating "
<< "PushString\n";
CORBA::ORB_ptr orb =
CORBA::ORB_init(argc,argv,"omniORB2");
CORBA::BOA_ptr boa =
orb->BOA_init(argc,argv,"omniORB2_BOA");
PushString_i *myPushString =
new PushString_i("hi");
myPushString->_obj_is_ready(boa);
{
PushString_var myobjRef =
myPushString->_this();
CORBA::String_var p =
orb->object_to_string(myobjRef);
cerr << "'" << (char *) p << "'" << endl;
f_out << (char *) p;
f_out << flush;
}
boa->impl_is_ready();
cout << "Server terminating." << endl;
return(0);
}
(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.
Listing 3. C++ in PushString_i.h File
#include "PushString.hh"
// *.hh files are generated by the idl compiler
class PushString_i : public _sk_PushString
//_sk_PushString is the skeleton
{
public:
PushString_i(const char * theName);
// the constructor
virtual ~PushString_i();
// the destructor<\n>
// here we define the implementation
// of pushStr, which was declared as a
// Pure Virtual Function in PushString.hh.
// All PVFs in the abstract base class must be
// declared here and defined in the implementaion.
virtual CORBA::Boolean pushStr(const char * inStr)
throw(CORBA::SystemException);
};
(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.
Listing 4. C++ in PushString_i.C File
(This is the CORBA object's implementation.)
#include "PushString_i.h"
#include
//define the constructor
PushString_i::PushString_i(const char * theName) {
cerr <<
"PushString_i implementation is being created"
<< endl;
}
//define the destructor
PushString_i::~PushString_i()
{
cerr <<
"PushString_i implementation is being"
<< "destroyed" << endl;"
}
// Here is the actual implementation of pushStr.
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);
if( strlen(m_str) > 5 )
// just for fun, play around with the boolean
// return
retval = 1;
else
retval = 0;
cerr << "The string pushed was " << m_str
<< endl;
delete [] m_str;
cerr << "Implementation leaving PushStr..."
<< endl;
return(retval);
}
(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.
Listing 5. C++ in Client.C File
#include
#include
#include "PushString.hh"
int main(int argc, char ** argv)
{
char tmpFilePath[1024];
char *IOR = new char[1024];
ifstream f_in("ior.out");
if(!f_in)
{
cerr <<
"\nCould not open ior.out for reading: "
<< "Things to check:" << endl;
cerr << "1. Is server running?" << endl;
cerr << "2. If so, is its ior.out file"
<< "accessible from the" << endl;
cerr << "directory that client is running "
<< "from currently?" << endl;
exit(-1);
}
f_in >> IOR;
CORBA::ORB_ptr orb =
CORBA::ORB_init(argc,argv,"omniORB2");
CORBA::BOA_ptr boa =
orb->BOA_init(argc,argv,"omniORB2_BOA");
PushString_var pushStringVar;
try {
CORBA::Object_var obj =
orb->string_to_object(IOR);
pushStringVar = PushString::_narrow(obj);
CORBA::String_var src =
(const char *) "Hello World";
CORBA::String_var dest;
cerr << "client callint PushStr with string: "
<< src << endl;
pushStringVar->pushStr(src);
// Call the remote object's pushStr function
cerr << "client returned from PushStr call "
<< "without an exception" << endl;
}
catch( CORBA::COMM_FAILURE & ex) {
cerr << "Caught system exception COMM_FAILURE"
<< endl;
cerr << "We seem to be missing a server "
<< "object" << endl;
cerr << "Make sure that (1) the server is "
<< "running and" << endl;
cerr << "(2) that the ior.out file that "
<< "server writes" << endl;
cerr << "out is accessible from this "
<< "client's" << endl;
cerr << "present working directory"
<< endl;
}
catch( omniORB::fatalException & ex) {
cerr << "Caught omniORB2 fatalException. "
<< "This is a bug in omniORB" << endl;
}
}
(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.
|