April 02, 2009 - By Charles Calkins, OCI Senior Software Engineer
Middleware News Brief (MNB) features news and technical information about Open Source middleware technologies.
Introduction
The most significant benefit of the Common Object Request Broker Architecture (CORBA) is interoperability — applications can be written in a multitude of languages, on distributed platforms with varying physical architectures. Over a dozen mappings from CORBA's Interface Definition Language (IDL) to common programming languages are defined by the Object Management Group (OMG) [1], plus additional custom mappings also exist. Interoperability at the data transmission level is also defined by the OMG, as provided by the General Inter-ORB Protocol (GIOP) [2,§9], a syntax, called Common Data Representation (CDR), for encoding IDL types and CORBA messages as octet streams for transfer across system and network boundaries, and the Internet Inter-ORB Protocol (IIOP), a mapping of GIOP to TCP/IP.
This article will illustrate multilanguage development with CORBA by implementing a simple client-server system written in each of four separate languages: C++, Java, Perl and C#, using four different open source middleware products: TAO, JacORB, opalORB, and IIOP.NET. These particular products have been selected as they are implemented fully in the languages indicated, and are not wrappers around modules written in other languages. Source code for the examples presented are available in the code archive associated with this article.
CORBA Application Development
A simple client-server system is developed in three stages: describe the service that a server will provide, implement the server, and then implement a client that uses the service. These stages are detailed below, with further discussion found in the referenced sections of the CORBA specification. [3] Readers who are already familiar with the basics of CORBA may skip this section and proceed directly to the sections that describe the details of implementing our simple client-server application using TAO (C++), JacORB (Java), opalORB (Perl), and IIOP.NET (C#).
- Describe the Service
Conceptually, the service is represented by one or more CORBA objects [§5.2.1], where an object is a "self-contained entity" that encompasses an aspect of the service's behavior. The functionality of each object is invoked via function calls known as operations [§5.1.8] on the object, and invoking an operation is known as making a request [§5.2.2]. An interface [§5.1.5] is a set of operations that a given object provides. CORBA defines an Interface Definition Language (IDL) [§7] to describe interfaces and operations.
The service that we create in this article is a simple one — it provides a means to sum two integers. We define an interface,
Math
, with a single operation,Add
, in IDL. We choose to operate on 32-bit integers (the CORBAlong
type), and we indicate that the values are passed into the method via thein
syntax. The intention of this operation is to addx
andy
and return the result. We name this file with the same name as the interface, and with an.idl
extension, thusMath.idl
. Defining theMath
interface in its own CORBA module reduces clutter at the global namespace level. We will use the same IDL file for all of our examples, but it will be copied into anIDL
subdirectory in each language directory in order to keep the examples self-contained.
// Math.idl
module MathModule {
interface Math
{
long Add(in long x, in long y);
};
};
Each CORBA product provides an application known as an IDL compiler to convert IDL files into source code in the appropriate target language. Code that is generated includes client stubs [§6.1.7] which allow operations on interfaces to be called in a straightforward way. Other code that is generated includes a server skeleton [§6.1.9], a framework for the implementation of the object which had its interface described in IDL. Code can be written that does not rely on stubs and skeletons, but instead calls methods dynamically, however that is beyond the scope of this article.
- Implement the Server
After describing the service, we must implement a server that hosts it and provides a means for the outside world to access it.
- Implement a servant
We must create an entity, known as a servant [§15.2.1] written in the target language, which performs the behavior of the service. While CORBA allows a servant to be associated with more than one CORBA object, as well as the life of the servant being independent of the life of the CORBA object, we will keep this example as simple as possible. We will represent our service as a single CORBA object that will live for the life of the server process, with an associated servant that will do the same. That is, we will create an entity that provides an operation named
Add
that returns the sum of its arguments. - Initialize the ORB
In order to start using CORBA functionality, a process must initialize the CORBA environment by calling
ORB_init()
[§8.5.1]. This initializes an Object Request Broker, and returns a reference to it. The application uses this reference to further interact with the CORBA environment. - Obtain a reference to the root POA
An object adapter manages a collection of CORBA objects, and the Portable Object Adapter (POA) [§15] is the standard object adapter that is defined in the CORBA specification. Multiple object adapters, each with different properties, can be associated with a single ORB in a hierarchical relationship, with the POA at the base of the hierarchy known as the root POA. Our server needs only the root POA with its default set of policies.
As all ORBs have at least the root POA, a reference to it can be obtained by calling
resolve_initial_references()
on the ORB reference with the well-known name,"RootPOA"
. - Activate the manager of the root POA
A POA manager manages the state of one or more POAs. A POA manager can be in one of four states: [§15.3.2.1]
State Description Holding The POAs will queue incoming requests. Active The POAs will process incoming requests, assuming that suitable system resources are available. Discarding The POAs will discard incoming requests, indicating to requestors that the operation should be tried again at a later time. This state is intended to be temporary, and used as a form of flow-control. Inactive The POAs are shutting down, and incoming requests will be rejected. -
Initially, the manager of the root POA is in the Holding state, so must be switched to the Active state for request processing to begin. This is accomplished by first obtaining a reference to the root POA's manager from the root POA, and then calling
activate()
on the POA manager reference.
- Instantiate the servant
As the first step in implementing the server process was to implement the servant, we must now instantiate the servant so it becomes an entity in the system. For instance, in C++, the servant would be written as a C++ class, and instantiated by creating a C++ object of that class on the stack or on the heap.
- Activate the object in the root POA
Given that we are using the default policies of the POA, a servant must be explicitly registered with a POA for it to be used. We do this by calling
activate_object()
[§15.3.9.15] on the root POA reference, passing a pointer to the instantiated servant. The registration process returns an opaque identifier called an Object Id which uniquely identifies the object within that POA. - Publish a reference to the object
In order for the outside world to perform requests on the CORBA object, a reference to the object must be made available. This can be done in several ways — for instance, by using the OMG-defined Naming Service or Trading Service, or by the
-ORBInitRef
command-line option — but for simplicity we will write the reference to a text file. The Object Id returned in the previous step is converted to a string [§8.2.2] via the ORB methodobject_to_string()
and written to a file on disk. - Wait for incoming client requests
The server is now ready to process incoming requests. Two methods are available. [§8.2.5] The first allows the server to perform other work — on the ORB reference, the server can repeatedly call
work_pending()
, and, if true,perform_work()
. If no work is pending, the server can perform other tasks. Alternatively, the server can callrun()
, which will not return until the server terminates via a call toshutdown()
. As our server has no additional work to perform, we will callrun()
. - Upon termination, free the resources used by the ORB
When the server has completed processing, the resources used by the ORB must be freed. This is accomplished by calling
destroy()
[§8.2.5.5] on the ORB reference.
- Implement a servant
- Implement a Client
With the server complete, we can now build a client of the server.
- Initialize the ORB
As before, to start using CORBA functionality, we must call
ORB_init()
. - Obtain an object reference to the Math object in the server
As the client is to invoke the
Add()
method, we must obtain a reference to the CORBA object that implements it. As the server represented the object reference as a string and wrote it to a file, we can do the reverse. We read the stringified reference from the file, and callstring_to_object()
to convert it into a usable reference. - Invoke the Add() operation
Now that a reference to the Math object has been obtained, we can invoke the
Add()
method. - Upon termination, free the resources used by the ORB
As before, to free ORB resources, we must call
destroy()
on the ORB reference.
- Initialize the ORB
Now that we have seen, in general terms, how to write a CORBA client and server, we will now show details of their implementation in CORBA products.
The ACE ORB (TAO)
The first product that we will use is TAO v1.6a [4], a CORBA v3.0 compliant ORB, written in C++. Although many C++ compilers are suitable, we will use Microsoft Visual Studio 2005. We will also use the Makefile, Project and Workspace Creator (MPC) [5] as part of the build system. As per the standard build instructions, we will presume that the ACE_ROOT
, TAO_ROOT
and MPC_ROOT
environment variables are set appropriately, and that the execution and library paths are updated to include ACE's bin
and lib
directories. For more information on the installation of TAO, please see the TAO FAQ. [6]
To begin, we will create a directory hierarchy for project organization. As this example is in C++, create a directory, CPP
, with subdirectories IDL
, server
and client
. Copy Math.idl
into the IDL
subdirectory.
IDL
In order to create the client stubs and server skeletons from Math.idl
, the TAO IDL compiler, tao_idl
, must be run. Although it can be run from the command prompt, MPC makes it easy to incorporate into the build system. In the IDL
subdirectory, create a file named IDL.mpc
as follows:
// CPP/IDL/IDL.mpc
project : taoidldefaults {
IDL_Files {
Math.idl
}
custom_only = 1
}
MPC allows project definitions to inherit from other project definitions. Here, taoidldefaults.mpb
is a base project that is bundled with ACE which creates a special project type named IDL that provides appropriate arguments to tao_idl
based on IDL files and other parameters that are specified in the IDL_Files
section of the MPC file. Here, only the one IDL file needs to be specified as the defaults set for the IDL compiler are acceptable. We do need to indicate that the project does not produce an executable or a library via the custom_only
flag.
Executing the IDL compiler on the Math.idl
file generates these six files: MathS.cpp
, MathS.h
, MathS.inl
, MathC.cpp
, MathC.h
, and MathC.inl
. The MathS
files contain the server skeleton, and the MathC
files contain the client stub. For more information on these files, please see chapter 4 of the the TAO Developer's Guide. [7]
Server
In the server
subdirectory, create the file server.cpp
, as described below.
Start by including several system and ACE headers. As the server will implement a servant for the Math
object, it must also include the Math
skeleton, MathS.h
.
// CPP/server/server.cpp
#include
#include
#include
#include
#include "MathS.h"
We now create the Math
servant as MathImpl
. The skeleton consists of a class, POA_MathModule::Math
, with each operation as a virtual method that must be implemented by a concrete subclass. As the Math
interface has only one operation, Add()
, POA_MathModule::Math
has only the one corresponding method that must be implemented. Note that the CORBA long
type as expressed in IDL is mapped to the C++ type ::CORBA::Long
.
class MathImpl : public virtual POA_MathModule::Math {
public:
MathImpl() {}
virtual ~MathImpl() {}
virtual ::CORBA::Long Add(::CORBA::Long x, ::CORBA::Long y) {
return x+y;
}
};
As we wish to provide the name of the file used to store the object reference, we need a way to pass the filename on the command line. A convenient way to process command line arguments is to use the ACE class ACE_Arg_Shifter
. Here, we search for an argument, -ior
, and return its parameter as the filename to use.
void GetArgs(int argc, char *argv[], std::string &ior_file) {
ACE_Arg_Shifter arg_shifter(argc, argv);
while (arg_shifter.is_anything_left()) {
const ACE_TCHAR *currentArg = 0;
if ((currentArg = arg_shifter.get_the_parameter(ACE_TEXT("-ior"))) != 0) {
ior_file = currentArg;
arg_shifter.consume_arg();
}
else
arg_shifter.ignore_arg();
}
}
We now come the main server functionality, implemented in the main()
function. First, we find the file that is to contain the object reference.
int main(int argc, char *argv[]) {
try {
// obtain arguments
std::string ior_file;
GetArgs(argc, argv, ior_file);
Next, we initialize the ORB.
// initialize the ORB
CORBA::ORB_var orb = CORBA::ORB_init(argc, argv);
Now, obtain a reference to the root POA. The reference is returned as a generic object reference, so must be cast (narrowed) to a POA reference.
// obtain a reference to the RootPOA
CORBA::Object_var obj =
orb->resolve_initial_references("RootPOA");
PortableServer::POA_var poa =
PortableServer::POA::_narrow(obj.in());
Next, we activate the POA manager of the root POA.
// activate the POAManager
PortableServer::POAManager_var mgr = poa->the_POAManager();
mgr->activate();
As this is C++, we can instantiate an instance of MathImpl
on the stack. We then activate it in the POA.
// create the Math servant
MathImpl servant;
PortableServer::ObjectId_var oid =
poa->activate_object(&servant);
We obtained the Object Id of the registered object from the activate_object()
call, and now convert it to an object reference via id_to_reference().
We then stringify the object reference and write it out to the file.
CORBA::Object_var math_obj = poa->id_to_reference(oid.in());
// write the object reference to a file
CORBA::String_var ior = orb->object_to_string(math_obj.in());
std::ofstream out(ior_file.c_str());
out << ior;
out.close();
As the outside world now can obtain a reference to the Math object, we can wait for incoming requests. Note that the server will continue to process requests until explicitly killed.
// accept requests from clients
orb->run();
At application exit, we destroy the ORB.
// cleanup
orb->destroy();
}
catch (CORBA::Exception& ex) {
std::cerr << "CORBA exception: " << ex << std::endl;
return 1;
}
return 0;
}
With the server complete, we create an MPC file for it, server.mpc
, also in the server
directory.
// CPP/server/server.mpc
project : taoserver {
after += IDL
includes += ../IDL
Source_Files {
server.cpp
../IDL/MathC.cpp
../IDL/MathS.cpp
}
}
As this is an application that uses TAO, the project inherits from taoserver
which will set proper libraries and other build attributes. As server.cpp
relies on files created by the IDL compiler, the IDL project must be built first, so the after
clause is used to establish the dependency — this project is to be built after the IDL project. Header files needed by the project reside in the IDL directory, so the includes
clause is used to add the IDL directory to the includes list. Finally, the C++ files to compile are specified in the Source_Files
section.
Client
The development of the client starts as that of the server — include system headers as well as headers from ACE and TAO. The header for the client stubs, generated by the IDL compiler, is also is included.
// CPP/client/client.cpp
#include
#include
#include
#include
#include
#include "MathC.h"
The client supports the -ior
argument as does the server, though here, rather than returning just the filename, the object reference as returned from the file is obtained. Although string_to_object()
supports the file://
syntax allowing a file containing an object reference to be passed, instead of requiring the contents to be read and passed as a string, we will read the contents ourselves to maintain consistency across all examples.
The client also supports an -add
argument to specify the two integers to sum.
void GetArgs(int argc, char *argv[],
std::string &ior, int &x, int &y) {
ACE_Arg_Shifter arg_shifter(argc, argv);
while (arg_shifter.is_anything_left()) {
const ACE_TCHAR *currentArg = 0;
if ((currentArg = arg_shifter.get_the_parameter(ACE_TEXT("-add"))) != 0) {
x = ACE_OS::atoi(currentArg);
arg_shifter.consume_arg();
y = ACE_OS::atoi(arg_shifter.get_current());
arg_shifter.consume_arg();
}
if ((currentArg = arg_shifter.get_the_parameter(ACE_TEXT("-ior"))) != 0) {
std::ifstream in(currentArg);
if (!in)
throw std::exception((std::string("Cannot open ") + currentArg).c_str());
std::ostringstream ss;
ss << in.rdbuf();
if (!in && !in.eof())
throw std::exception((std::string("Cannot read IOR from ") + currentArg).c_str());
ior = ss.str();
arg_shifter.consume_arg();
}
else
arg_shifter.ignore_arg();
}
}
We now write the client's main()
by first processing the command-line arguments.
int main(int argc, char *argv[]) {
try {
// obtain arguments
std::string ior;
int x=2, y=2;
GetArgs(argc, argv, ior, x, y);
Next, initialize the ORB.
// initialize the ORB
CORBA::ORB_var orb = CORBA::ORB_init(argc, argv);
As we have the object reference in string form, we convert it with string_to_object()
, and narrow it to obtain the Math
interface.
// obtain an object reference
CORBA::Object_var obj = orb->string_to_object(ior.c_str());
MathModule::Math_var math = MathModule::Math::_narrow(obj.in());
if (CORBA::is_nil(math.in()))
throw std::exception("IOR was not a Math object reference");
We can now invoke the Add()
operation, and display the result.
// invoke the method
std::cout << "Sum: " << math->Add(x,y) << std::endl;
The client now destroys the ORB and exits.
// cleanup
orb->destroy();
}
catch (CORBA::Exception& ex) {
std::cerr << "CORBA exception: " << ex << std::endl;
return 1;
}
catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
return 1;
}
return 0;
}
We create an MPC file for the client as we did with the server. The main difference is the additional base projects that this project inherits from, due to the IDL compiler defaults.
// CPP/client/client.mpc
project : taoexe, anytypecode {
after += IDL
includes += ../IDL
Source_Files {
client.cpp
../IDL/MathC.cpp
}
}
Build and Run
The last component in the build system is to create a workspace (MWC) file, collecting together the individual projects. In the CPP
directory, create the file CPP.mwc
with the following contents:
// CPP/CPP.mwc
workspace {
IDL
server
client
}
From a command prompt, while in the CPP directory, executing:
%ace_root%\bin\mwc.pl -type vc8 CPP.mwc
will generate the Visual Studio 2005 (vc8) solution file CPP.sln
. That file can now be opened in Visual Studio and the projects compiled. Opening a command prompt and executing
server -ior my.ior
from the server directory, and opening another command prompt and executing
client -ior ..\server\my.ior -add 5 6
from the client directory will display the expected sum of 11.
JacORB
The second product that we will use is JacORB v2.3.1 [8], a CORBA v2.3 compliant ORB, written in Java. We will presume that the following environment variables are set correctly: JACORB_HOME
and JAVA_HOME
to the top level directories of the JacORB distribution and Java Developer Kit, CLASSPATH
updated to include all of the JAR files in JacORB's lib
, JAVA_PLATFORM
has been set to the correct architecture (e.g., WIN32
in our case), and that JacORB's bin
directory is in the execution path. For more information on the installation of JacORB, please see the JacORB Programming Guide. [9]
For this example, we will be using GNU Make to execute the build commands, and other GNU core utilities (such as cp
) will also be required. While already available on most UNIX systems, Windows users will need to obtain distributions from sources such as [10].
As we did in the C++ example, we will create a similar directory hierarchy for project organization. Create a directory named Java
as a sibling to the CPP
directory, and also create IDL
, server
and client
subdirectories of the Java
directory, analagous as to what had been done before. Also as before, copy Math.idl
into the IDL
subdirectory.
IDL
Running the JacORB IDL compiler on an IDL file produces a number of Java source files for Math.idl
: Math.java
, MathHelper.java
, MathHolder.java
, MathOperations.java
, MathPOA.java
, MathPOATie.java
and _MathStub.java
are produced. In particular, MathPOA.java
contains the server skeleton, Math.java
the Math
interface, and _MathStub.java
the client stub. These files are generated from the single Math interface — a similar set of files would be generated for each IDL interface that is compiled.
Once produced, the .java
files must be compiled into .class
files. We shall use MPC to structure this process. Create a file, IDL.mpc,
, in the Java/IDL
directory as follows:
// Java/IDL/IDL.mpc
project {
Define_Custom(JAVA_IDL) {
command = $(JACORB_HOME)/bin/idl
inputext = .idl
}
JAVA_IDL_Files {
Math.idl >> MathModule/Math.java \
MathModule/MathHelper.java \
MathModule/MathHolder.java \
MathModule/MathOperations.java \
MathModule/MathPOA.java \
MathModule/MathPOATie.java \
MathModule/_MathStub.java
}
specific {
// to provide more info on unchecked calls in generated code
compile_option += -Xlint:unchecked
}
}
MPC allows custom file types to be defined. We define a custom type named JAVA_IDL that runs the JacORB IDL compiler and processes files with an .idl
input extension. The Java_IDL_Files
section lists the input IDL files, and the files that will be produced when the custom command is executed.
As MPC has built-in support for Java, the existence of .java
files in the directory will cause the Java compiler, javac
, to be invoked. If no main()
function is found, as in this case, the output is a Java JAR file. The compile_option
entry allows that additional option to be passed to javac
when the compilation is performed. In this case, it shows more information related to warnings in the code generated by the IDL compiler.
Server
The development of the JacORB server follows the CORBA development path described above. First, we write a class MathImpl
, in the file server/MathImpl.java
, to implement the servant.
// Java/server/MathImpl.java
public class MathImpl extends MathModule.MathPOA {
public int Add(int x, int y)
{
return x+y;
}
}
We create server/Server.java
in the same way we created the C++ server. First, we process command-line arguments to retrieve the object reference filename.
// Java/server/Server.java
import java.io.*;
public class Server {
public static void main(String[] args) {
try {
// obtain arguments
String ior_file = "";
for (int i=0; i<args.length; i++)
if (args[i].equals("-ior"))
ior_file = args[i+1];
Next, initialize the ORB. JacORB functionality is in the org.omg
namespace.
// initialize the ORB
org.omg.CORBA.ORB orb = org.omg.CORBA.ORB.init(args, null);
We now obtain a reference to the root POA, narrowing it as necessary.
// obtain a reference to the RootPOA
org.omg.PortableServer.POA poa =
org.omg.PortableServer.POAHelper.narrow(orb.resolve_initial_references("RootPOA"));
Next, activate the POA manager.
// activate the POAManager
poa.the_POAManager().activate();
Now, instantiate the MathImpl
servant and register it with the root POA.
// create the Math servant
org.omg.CORBA.Object o = poa.servant_to_reference(new MathImpl());
Next, stringify the object reference and write it to the file given as the -ior
argument.
// write the object reference to a file
PrintWriter ps = new PrintWriter(new FileOutputStream(new File(ior_file)));
ps.println(orb.object_to_string(o));
ps.close();
We are done, so start processing requests.
// accept requests from clients
orb.run();
Finally, clean up on exit by destroying the ORB.
// cleanup
orb.destroy();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
With the server written, we now configure an MPC file to compile it. As the IDL project must be built before this one, a dependency is created on it with the after
clause. As the Java compiler needs to reference the code generated by the IDL compiler, IDL.jar
(in addition to the current directory) is set as the -classpath
argument for javac
.
// Java/server/server.mpc
project {
after += IDL
specific {
compile_option += -classpath ../IDL/IDL.jar;.
}
}
Client
The client follows the same process as described above. As we need to retrieve three command-line arguments, we create a class to store them temporarily.
// Java/client/Args.java
public class Args {
int x;
int y;
String ior;
Args(int x, int y, String ior) {
this.x = x;
this.y = y;
this.ior = ior;
}
int getX() { return x; }
int getY() { return y; }
String getIOR() { return ior; }
}
We next parse the command-line arguments for -ior
and -add
, and retrieve the object reference from the supplied file.
// Java/client/Client.java
import java.io.*;
public class Client {
public static Args GetArgs(String[] args) throws FileNotFoundException, IOException {
int x=2,y=2;
String ior = "";
int i=0;
while (i<args.length) {
if (args[i].equals("-add")) {
x = Integer.parseInt(args[++i]);
y = Integer.parseInt(args[++i]);
}
else
if (args[i].equals("-ior")) {
String ior_file = args[++i];
BufferedReader in = new BufferedReader(new FileReader(ior_file));
ior = in.readLine();
in.close();
}
++i;
}
return new Args(x,y, ior);
}
main()
is straightforward. First, decode the command-line arguments.
public static void main(String[] args) {
try {
// obtain arguments
Args a = GetArgs(args);
Next, initialize the ORB.
// initialize the ORB
org.omg.CORBA.ORB orb = org.omg.CORBA.ORB.init(args, null);
As we have the object reference in string form, convert it back to an object and narrow it to the Math
interface.
// obtain an object reference
MathModule.Math math = MathModule.MathHelper.narrow(orb.string_to_object(a.getIOR()));
Call Add()
, and print the result.
// invoke the method
System.out.println("Sum: " + math.Add(a.getX(),a.getY()));
Perform clean up, and exit.
// cleanup
orb.destroy();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
With the client code complete, we create an MPC file to compile it. The MPC file has the same structure as the one for the server.
// Java/client/client.mpc
project {
after += IDL
specific {
compile_option += -classpath ../IDL/IDL.jar;.
}
}
Build and Run
We now create a workspace file to build the Java example. As all three projects compile Java code, they must be grouped together and -language java
passed to MPC.
// Java/Java.mwc
workspace {
specific {
cmdline += -language java
client
server
IDL
}
}
From a command prompt, while in the Java directory, executing
%mpc_root%\mwc.pl -type make Java.mwc
will generate makefiles suitable for use by GNU Make. After the files are built, we open a command prompt and change to the Java server directory. After setting the CLASSPATH
to include ..\IDL\idl.jar
, running
jaco -I../IDL Server -ior my.ior
will start the server. We open another command prompt, change directory to the Java client directory, set the CLASSPATH
again, and execute
jaco Client -ior ..\server\my.ior -add 5 6
displays the expected sum of 11, in addition to JacORB startup messages.
opalORB
The third product that we will use is opalORB v0.1.6 [11], a CORBA v3.0 compliant ORB ("CORBA/e Micro Profile" subset, but supporting additional features such as dynamic invocation), written in Perl. We will presume that the OPALORB_ROOT
environment variable is set to the opalORB directory, and, as per opalORB instructions, that Error.pm
has also been installed in Perl's include path.
For our Perl example, we will use GNU Make, and we will create a directory structure as before. We create a directory named Perl
as a sibling of CPP
and Java
, with subdirectories IDL
, server
, and client
. Also as before, copy Math.idl
into the IDL directory.
IDL
Once again, we must run an IDL compiler on Math.idl
to generate the client and server code. With opalORB's IDL compiler, only two files are generated: Math.pm
and POA_Math.pm
. We can use MPC to automate the process of the execution of the IDL compiler as we have done before by creating a custom project.
// Perl/IDL/IDL.mpc
project {
Define_Custom(PERL_IDL) {
command = perl $(OPALORB_ROOT)/idl/idl.pl
inputext = .idl
}
PERL_IDL_Files {
Math.idl >> MathModule/Math.pm \
MathModule/POA_Math.pm
}
}
Server
The process that we have seen for C++ and Java is once again repeated here. To implement the server, we first implement the servant as a Perl package MathImpl
in the file server/MathImpl.pm
.
First, we declare a package
to start the implementation of MathImpl.
# Perl/server/MathImpl.pm
package MathImpl;
use strict;
As this class inherits from POA_Math
, we add it to the @ISA
array.
use MathModule::POA_Math;
our @ISA = qw(MathModule::POA_Math);
We implement a constructor that calls new()
on its base class, stores the ORB argument passed to it in its hash, and returns a reference to the newly-created object.
sub new {
my $class = shift;
my $self = $class->SUPER::new();
$self->{'orb'} = shift;
return $self;
}
We also implement the Add()
method — it retrieves its parameters and returns their sum.
sub Add {
my $self = shift;
my $x = shift;
my $y = shift;
return $x+$y;
}
1;
The server begins with a means to start the Perl interpreter.
# Perl/server/server.pl
eval '(exit $?0)' && eval 'exec perl -w -S $0 ${1+"$@"}'
& eval 'exec perl -w -S $0 $argv:q'
if 0;
We need several libraries — opalORB itself, MathImpl
, and Getopt
for argument processing.
use Env '$OPALORB_ROOT';
use lib "$OPALORB_ROOT";
use strict;
use CORBA;
use CORBA::Exception;
use MathImpl;
use Getopt::Long;
my $status = 0;
As the server needs to parse only the -ior
option, standard Perl option processing can be used as -ior
is followed by only one argument.
try {
# obtain arguments
my $ior_file;
GetOptions('ior=s' => \$ior_file);
Once again, we follow the pattern used for C++ and Java — initialize the ORB, obtain the root POA and activate its manager, create the MathImpl
instance and activate it in the POA, write a reference to it out to a file, and then start processing incoming requests.
# initialize the ORB
my $orb = CORBA::ORB_init(\@ARGV);
# obtain a reference to the root POA
my $poa = $orb->resolve_initial_references('RootPOA');
# activate the POAManager
my $poamanager = $poa->the_POAManager();
$poamanager->activate();
# create the Math servant
my $impl = new MathImpl($orb);
my $id = $poa->activate_object($impl);
my $obj = $poa->id_to_reference($id);
my $servant = MathModule::Math::_narrow($obj);
# write the object reference to a file
my $fh = new FileHandle();
if (open($fh, ">$ior_file")) {
print $fh $orb->object_to_string($servant);
close($fh);
}
# accept requests from clients
$orb->run();
# cleanup
$orb->destroy();
}
catch CORBA::Exception with {
my $ex = shift;
print STDERR "EXCEPTION: $ex\n";
$status++;
};
exit($status);
Client
Again, the client follows the pattern followed by C++ and Java. We first process the command-line arguments, but have to do it manually, instead of via Getopt
as the -add
option takes two, rather than one, argument, which Getopt
is unable to process.
# Perl/client/client.pl
eval '(exit $?0)' && eval 'exec perl -w -S $0 ${1+"$@"}'
& eval 'exec perl -w -S $0 $argv:q'
if 0;
use strict;
use Env '$OPALORB_ROOT';
use lib "$OPALORB_ROOT";
use CORBA;
use CORBA::Exception;
use MathModule::Math;
my $status = 0;
try {
# obtain arguments
my ($i, $ior_file, $x, $y);
$i = 0;
while ($i<$#ARGV) { # not <= as will have args with at least 1 param
if ($ARGV[$i] eq "-ior") {
$ior_file=$ARGV[++$i];
}
if ($ARGV[$i] eq "-add") {
$x=$ARGV[++$i];
$y=$ARGV[++$i];
}
$i++;
}
The remainder of the client is as before — initialize the ORB, de-stringify the object reference, invoke the Add()
method, and then clean up allocated resources before exiting.
# initialize the ORB
my $orb = CORBA::ORB_init(\@ARGV);
# obtain an object reference
my $ior = "file://$ior_file";
my $obj = $orb->string_to_object($ior);
if (CORBA::is_nil($obj)) {
print STDERR "Unable to obtain a reference to the server\n";
exit(1);
}
my $server = MathModule::Math::_narrow($obj);
if (CORBA::is_nil($server)) {
print "Narrow failed.\n";
exit(1);
}
# invoke the method
my $result = $server->Add($x,$y);
print "Sum: $result\n";
# cleanup
$orb->destroy();
}
catch CORBA::Exception with {
my $ex = shift;
print STDERR "EXCEPTION: $ex\n";
$status++;
};
exit($status);
Build and Run
As the client and server do not need any processing, only the IDL project needs to be added into a workspace for convenient execution of the IDL compiler. We create a workspace file as follows:
// Perl/Perl.mwc
workspace {
IDL
}
We use GNU Make for this project as with Java.
%mpc_root%\mwc.pl -type make Perl.mwc
After running make
, we open a command prompt, change to the server
directory, and execute:
perl -I../IDL server.pl -ior my.ior
We open another command prompt, change to the client
directory, and execute:
perl -I../IDL client.pl -ior ../server/my.ior -add 5 6
The sum of 11 is printed, as expected.
IIOP.NET
The fourth product that we will use is IIOP.NET v1.9.0 sp1 [12], written in C#. Unlike the other products, IIOP.NET is not a CORBA ORB implementation, per se, but a means for .NET Remoting to be compatible with IIOP. In .NET Remoting, method invocations, and serialized objects, can be transmitted over a communications channel, as described in [13], [14] and elsewhere. IIOP.NET provides a channel that encodes and decodes IIOP.
For this example, we set the environment variable, IIOP_ROOT
, to the IIOP.NET executables and libraries, and create a directory hierarchy as before. Under a CS
directory, we create the IDL
, server
and client
directories, and copy Math.idl
into the IDL
directory.
IDL
Although not a full CORBA implementation, IIOP.NET does include an IDL compiler which compiles IDL directly into a .NET assembly. We still create a custom MPC type to do this, but the structure is a bit different. Due to the nature of the output, this time source_outputext
must be specified to indicate the output type. Also, we must provide the name of the assembly to generate as the first argument to the IDL compiler, which we do via the commandflags
option.
// CS/IDL/IDL.mpc
project {
Define_Custom(CS_IDL) {
command = $(IIOP_ROOT)/IDLToCLSCompiler
inputext = .idl
source_outputext = .dll
}
CS_IDL_Files {
commandflags += Math
Math.idl
}
}
Server
As before, we develop a MathImpl
class to act as a servant. Unlike the other examples, it does not inherit from a CORBA-related type, but instead MarshalByRefObject
. This class allows a .NET object to communicate across application boundaries via proxy objects. MathImpl
also inherits from the Math
interface which provides an abstract declaration of the Add()
method to override.
// CS/server/server.cs
using System;
using System.Runtime.Remoting.Channels;
using System.Threading;
using Ch.Elca.Iiop;
using omg.org.CORBA;
using System.IO;
public class MathImpl : MarshalByRefObject, MathModule.Math {
public int Add(int x, int y)
{
return x+y;
}
}
As before, the server begins by decoding the -ior
command-line argument, as well as an optional -port
argument so the default port can be overriden.
public class MathServer {
public static void Main(string[] args) {
// obtain arguments
string iorFile = args[Array.IndexOf(args, "-ior") + 1];
int port = 8087;
int portIndex = Array.IndexOf(args, "-port");
if (portIndex>=0)
port = int.Parse(args[portIndex+1]);
We now create an IiopChannel
and register it with the system. An IiopChannel
encapsulates an IiopServerChannel
which waits for incoming request messages and sends replies, and anIiopClientChannel
which sends the requests and receives the replies.
// initialize the channel
IiopChannel chan = new IiopChannel(port);
ChannelServices.RegisterChannel(chan, false);
Next, we instantiate the servant.
// create the Math servant
MathImpl math = new MathImpl();
The class OrbServices
does provide familiar CORBA ORB functionality, such as object_to_string()
and related methods.
// write the object reference to a file
OrbServices orb = OrbServices.GetSingleton();
String ior = orb.object_to_string(math);
StreamWriter sw = new StreamWriter(iorFile);
sw.WriteLine(ior);
sw.Close();
With the server established, we now wait for incoming requests.
// accept requests from clients
Thread.Sleep(Timeout.Infinite);
}
}
We now create an MPC file so the server can be built. It is similar to what we have seen before, although an IIOP.NET library, IIOPChannel.dll
, and the output of the IDL compilation, Math.dll
must be included explicitly via the lit_libs
option.
// CS/server/server.mpc
project {
exename = server
after += IDL
lit_libs += $(IIOP_ROOT)/IIOPChannel.dll
lit_libs += ../IDL/Math.dll
}
Client
As we have done before, the client begins by processing command-line arguments to retrieve the filename containing the object reference, as well as the integers to sum.
// CS/client/client.cs
using System;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting;
using Ch.Elca.Iiop;
using System.IO;
public class MathClient {
public static void Main(string[] args) {
try {
// obtain arguments
string iorFile = args[Array.IndexOf(args, "-ior") + 1];
int addIndex = Array.IndexOf(args, "-add");
int x = int.Parse(args[addIndex + 1]);
int y = int.Parse(args[addIndex + 2]);
The client must also create a communications channel, and register it with the system.
// initialize the channel
IiopClientChannel channel = new IiopClientChannel();
ChannelServices.RegisterChannel(channel, false);
Finally, we retrieve the object reference from the file, and call RemotingServices.Connect()
in order to obtain a proxy to the remote object. This takes the place of string_to_object()
and narrow()
as was done before. We are then able to invoke the Add()
method.
// obtain an object reference
StreamReader stream = new StreamReader(iorFile);
String ior = stream.ReadLine();
MathModule.Math math =
(MathModule.Math)RemotingServices.Connect(typeof(MathModule.Math), ior);
// invoke the method
Console.WriteLine("Sum: " + math.Add(x, y));
} catch (Exception e) {
Console.WriteLine("Exception: " + e);
}
}
}
With the client complete, we create an MPC file so it can be built. It is identical to what is used by the server, as the build process automatically includes the .cs
files that are in the same directory as the MPC file, so MPC is able to create the appropriate project structure.
// CS/client/client.mpc
project {
exename = client
after += IDL
lit_libs += $(IIOP_ROOT)/IIOPChannel.dll
lit_libs += ../IDL/Math.dll
}
Build and Run
We create one more workspace file so the client and server can be built, and the IDL compiler executed. For proper processing, we must specify that the client and server projects are compiled as csharp
.
// CS.mwc
workspace {
specific {
cmdline += -language csharp
client
server
}
IDL
}
From a command prompt, while in the CS directory, executing:
%ace_root%\bin\mwc.pl -type vc8 CS.mwc
will generate the Visual Studio 2005 (vc8) solution file CS.sln
. That file can now be opened in Visual Studio and the projects compiled. Opening a command prompt and executing
server -ior my.ior
from the server debug directory (presuming a debug build was performed), and opening another command prompt and executing
client -ior ..\..\server\Debug\my.ior -add 5 6
from the client directory will display the expected sum of 11.
Test Framework
We have shown above that clients developed in each of the products work with servers developed with the same product — TAO client with a TAO server, etc. For ease in demonstrating that CORBA allows applications written in different languages with different CORBA products to interoperate, we can write a test framework that uses PerlACE, a Perl-based system used in TAO unit testing.
We first create a Perl script, and indicate that we wish to use PerlACE.
// Test/run_test.pl
eval '(exit $?0)' && eval 'exec perl -S $0 ${1+"$@"}'
& eval 'exec perl -S $0 $argv:q'
if 0;
use strict;
use Env '$ACE_ROOT';
use lib "$ACE_ROOT/bin";
use PerlACE::Run_Test;
Next, we create a subroutine that manages server creation. It takes three arguments — a path to the server executable, a command-line to pass to the executable, and a file name for where to store the object reference.
sub start_server {
# obtain the arguments
my $server_path = shift;
my $server_cmdline = shift;
my $ior_filename = shift;
To ensure no stale object reference file, any existing one is deleted.
# remove the file containing the IOR if it currently exists
my $ior = PerlACE::LocalFile($ior_filename);
unlink $ior;
For cleaner test results, we redirect standard output and standard error to the file server_output.data
. If a server fails to start, this file can be examined to determine why.
# start the client with output redirected to a file
my $server_output_file = "server_output.data";
unlink $server_output_file;
open (OLDOUT, ">&STDOUT");
open (STDOUT, ">$server_output_file") or die "can't redirect stdout: $!";
open (OLDERR, ">&STDERR");
open (STDERR, ">&STDOUT") or die "can't redirect stderror: $!";
We now take advantage of PerlACE's ability to start a process. We start the server, passing the supplied command-line. The command-line can be retrieved, if desired, from the process variable by the CommandLine()
method.
my $server_process = new PerlACE::Process($server_path, $server_cmdline);
# print $server_process->CommandLine() . "\n";
$server_process->Spawn();
We now reset standard output and standard error to what they had been originally.
close (STDERR);
close (STDOUT);
open (STDOUT, ">&OLDOUT");
open (STDERR, ">&OLDERR");
By the time a server is ready to process requests (run()
has been called on the ORB reference), the object reference of the Math
object has been written to the file. We use another feature of PerlACE to wait for that file to be created to ensure the server is running before proceeding further.
# wait for the IOR to be written to the file
if (PerlACE::waitforfile_timed($ior,
$PerlACE::wait_interval_for_process_creation) == -1)
{
print STDERR "ERROR: cannot find file <$ior>\n";
$server_process->Kill();
unlink $ior;
exit 1;
}
Finally, we perform clean up, and return the name of the object reference file and a reference to the process back to the caller.
unlink $server_output_file;
return ($ior, $server_process);
}
Now that we are able to run a server, we do something similar to execute a client. Again, we need three parameters — the client executable, the client command line, and the file where the object reference to the Math
object is located.
sub run_test {
# obtain the arguments
my $client_path = shift;
my $client_cmdline = shift;
my $ior_filename = shift;
As the Add()
operation sums two integers, we generate two integers to sum, and display the expected result to the user.
# generate random numbers from 1 to 100 to add
my $x = int(rand(100)) + 1;
my $y = int(rand(100)) + 1;
my $expected_sum = $x + $y;
print " $x+$y => Expected: $expected_sum ";
We now start the client as we did the server. We first redirect standard output and standard error to the file client_output.data
.
# start the client with output redirected to a file
my $client_output_file = "client_output.data";
unlink $client_output_file;
open (OLDOUT, ">&STDOUT");
open (STDOUT, ">$client_output_file") or die "can't redirect stdout: $!";
open (OLDERR, ">&STDERR");
open (STDERR, ">&STDOUT") or die "can't redirect stderror: $!";
Next, we start the client by passing the supplied command-line, the arguments we need to locate the object reference file, and the integers to sum.
my $client = new PerlACE::Process($client_path, $client_cmdline .
" -ior $ior_filename -add $x $y");
$client->Spawn();
We expect the client to display the result of the call to Add()
and terminate quickly, but we will kill the process if the execution is abnormally long, indicating an error has occured.
my $client_return = $client->WaitKill(15);
We return standard output and standard error to their original destinations.
close (STDERR);
close (STDOUT);
open (STDOUT, ">&OLDOUT");
open (STDERR, ">&OLDERR");
The redirected output of the client contains the result of the summation, and possibly other text (such as JacORB logging messages). We search the file for the expected output and retrieve the summation result. If it matches the expected sum, the $match_found
variable is set to true. The result is displayed to the user.
# find the result
open(CLIENT_OUTPUT_FILE, $client_output_file);
my @lines=;
close(CLIENT_OUTPUT_FILE);
my $pattern = 'Sum: (\d+)'; # $1 is "Sum: nnn", $2 is "nnn"
my $match_found = 0;
my $line;
foreach $line (@lines) {
if ($line =~ m/($pattern)/) {
print "Actual: $2";
if ($2 == $expected_sum) {
$match_found = 1;
last;
}
}
}
if ($match_found) {
print " => success\n";
}
else {
print " => failure\n";
}
We now perform clean up and return to the caller.
# clean-up
unlink $client_output_file;
if ($client_return != 0) {
print STDERR "ERROR: Client returned <$client_return>\n";
exit 1 ;
}
}
To aid in running JacORB applications, we now create a function to construct command-line arguments based on the jaco
script that is part of JacORB. We must update the CLASSPATH
to include our example code, identify the location of the JVM, and, to make output cleaner, set the JacORB logging level to 0.
# retrieve info needed to execute JacORB
sub jacorb_args {
# add the locations of the JacORB client and server to the classpath
use Env '$CLASSPATH';
my $classpath = "$CLASSPATH;../Java/Server;../Java/Client";
# retrieve the location of JacORB
use Env '$JACORB_HOME';
my $jacorb_home = "$JACORB_HOME";
# jaco.bat is really "javac $jaco_args", so rebuild the arguments here
my $jaco_args = "-Djava.endorsed.dirs=$jacorb_home/lib "
. "-Djacorb.home=$jacorb_home "
. "-Dorg.omg.CORBA.ORBClass=org.jacorb.orb.ORB "
. "-Dorg.omg.CORBA.ORBSingletonClass=org.jacorb.orb.ORBSingleton "
. "-classpath $classpath ";
# locate Java
use Env '$JAVA_HOME';
my $java_path = "$JAVA_HOME/bin/java";
# suppress JacORB logging
my $jacorb_args = $jaco_args . " -Djacorb.log.default.verbosity=0 ";
return ($java_path, $jacorb_args);
}
Now, in the main body of the test application, we obtain the JacORB arguments, plus locate Perl.
# locate Java and startup arguments
my ($java_path, $jacorb_args);
($java_path, $jacorb_args) = jacorb_args();
# locate Perl
use Env '$PERL_ROOT';
my $perl_path = "$PERL_ROOT/bin/perl";
Next, we make an array of hashes to list the servers that we wish to start.
# servers to execute
my @servers = (
{
product => "TAO",
path => "../CPP/server/server",
cmdline => "-ior cpp.ior",
ior_file => "cpp.ior"
},
{ product => "JacORB",
path => $java_path,
cmdline => "$jacorb_args Server -ior java.ior",
ior_file => "java.ior"
},
{ product => "opalORB",
path => $perl_path,
cmdline =>
"-I../Perl/server -I../Perl/IDL" .
" ../Perl/server/server.pl -ior perl.ior",
ior_file => "perl.ior"
},
{ product => "IIOP.NET",
path => "../CS/server/Debug/server",
cmdline => "-ior cs.ior",
ior_file => "cs.ior"
}
);
We do the same for the clients that we wish to run.
# clients to execute
my @clients = (
{ product => "TAO",
path => "../CPP/client/client",
cmdline => ""
},
{ product => "JacORB",
path => $java_path,
cmdline => "$jacorb_args Client"
},
{ product => "opalORB",
path => $perl_path,
cmdline >
"-I../Perl/client -I../Perl/IDL" .
" ../Perl/client/client.pl"
},
{ product => "IIOP.NET",
path => "../CS/client/Debug/client",
cmdline => ""
}
);
Now, we loop over the server array, starting each server in turn. The process variable and full path to the object reference file are also stored in the hash.
# start servers
my $num_servers = scalar(@servers);
my $i;
for ($i=0; $i<$num_servers; $i++) {
print "Starting " . $servers[$i]{'product'} . " server\n";
my ($ior, $process);
($ior, $process) = start_server($servers[$i]{'path'},
$servers[$i]{'cmdline'}, $servers[$i]{'ior_file'});
$servers[$i]{'ior'} = $ior;
$servers[$i]{'process'} = $process;
}
For each server, we now loop over the client array, running each client against each server.
# run tests
my $num_clients = scalar(@clients);
my $j;
for ($i=0; $i<$num_servers; $i++) {
for ($j=0; $j<$num_clients; $j++) {
print "Running test: " . $servers[$i]{'product'} .
" server, " . $clients[$j]{'product'} . " client\n";
run_test($clients[$j]{'path'}, $clients[$j]{'cmdline'},
$servers[$i]{'ior'});
}
}
With the tests complete, we stop the servers and exit.
# shut down servers
for ($i=0; $i<$num_servers; $i++) {
print "Stopping " . $servers[$i]{'product'} . " server\n";
$servers[$i]{'process'}->Kill();
unlink $servers[$i]{'ior_file'};
}
exit 0;
When run_test.pl
is executed, we see that each client is able to successfully interact with each server.
Conclusion
In this article, we have shown how to implement a simple CORBA-based client-server system using four different CORBA products, written in four different languages. As we have demonstrated, interoperability across a number of popular programming languages has been achieved, with over a dozen additional language mappings in existence. Developers can choose to use languages, operating systems, and hardware platforms appropriate for the tasks at hand. Many open source products exist to support CORBA development, four of which have been shown above, so users need not be "locked in" to a single vendor's implementation. In addition, consulting and support [15] are available from OCI for all of the products presented in the article.
References
[1] Catalog of OMG IDL / Language Mappings Specifications
http://www.omg.org/technology/documents/idl2x_spec_catalog.htm
[2] Common Object Request Broker Architecture (CORBA) Specification, Version 3.1, Part 2: CORBA Interoperability (formal/2008-01-07)
http://www.omg.org/spec/CORBA/3.1/Interoperability/PDF
[3] Common Object Request Broker Architecture (CORBA) Specification, Version 3.1, Part 1: CORBA Interfaces (formal/2008-01-04)
http://www.omg.org/spec/CORBA/3.1/Interfaces/PDF
[4] OCI's distribution of TAO
http://www.theaceorb.com
[5] MPC product page
https://objectcomputing.com/products/mpc
[6] TAO FAQ
http://theaceorb.com/faq/index.html#HowToBuildACEandTAOonWindows
[7] TAO Developer's Guide
http://www.theaceorb.com/product/index.html#doc1.6a
[8] JacORB home page
http://www.jacorb.org
[9] JacORB Programming Guide 2.3
http://www.jacorb.org/releases/2.3.0/ProgrammingGuide.zip
[10] GnuWin32 Packages
http://gnuwin32.sourceforge.net
[11] opalORB project page on SourceForge
http://opalorb.sourceforge.net
[12] IIOP.NET project page on SourceForge
http://iiop-net.sourceforge.net
[13] .NET Remoting with an easy example
http://www.codeproject.com/KB/IP/Net_Remoting.aspx
[14] Mixing ACE/TAO and .NET Clients and Servers
http://www.codeproject.com/KB/IP/iiopnetandtao.aspx
[15] OCI Consulting and Support
https://objectcomputing.com/products