| CORBA Program DevelopmentA 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 headerPushStringSK.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. |