![]() |
![]() |
![]() |
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.
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#).
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 CORBA long type), and we indicate
that the values are passed into the method via the in syntax. The intention of this
operation is to add x and y and return the result. We name this file
with the same name as the interface, and with an .idl extension, thus Math.idl.
Defining the Math 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 an IDL 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.
After describing the service, we must implement a server that hosts it and provides a means for the outside world to access it.
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.
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.
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".
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.
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.
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.
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 method object_to_string() and written to a file on disk.
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 call run(),
which will not return until the server terminates via a call to shutdown().
As our server has no additional work to perform, we will call run().
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.
With the server complete, we can now build a client of the server.
As before, to start using CORBA functionality, we must call ORB_init().
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 call string_to_object() to convert it into a usable
reference.
Now that a reference to the Math object has been obtained, we can
invoke the Add() method.
As before, to free ORB resources, we must call destroy()
on the ORB reference.
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 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.
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]
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 <iostream> #include <fstream> #include <string> #include <ace/arg_shifter.h> #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.
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 <iostream> #include <fstream> #include <string> #include <sstream> #include <ace/arg_shifter.h> #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
}
}
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.
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.
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.
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;.
}
}
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;.
}
}
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.
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.
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
}
}
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);
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);
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.
The fourth product that we will use is IIOP.NET v1.9.0 sp1 [12], written in C#. Unike 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.
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
}
}
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 an IiopClientChannel
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
}
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
}
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.
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=<CLIENT_OUTPUT_FILE>;
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.
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.
[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
http://www.ociweb.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
http://www.ociweb.com/products
Object Computing, Inc. (OCI) is the leading provider of object-oriented technology training in the Midwest. Thousands of students participate in our training program every year. Targeted toward software engineers and the development community, our extensive program of over 50 hands-on workshops is delivered to corporations and individuals throughout the U.S. and internationally. OCI's Education Services include Private Training, Public Training, and Lab Rentals. Visit www.ociweb.com/training or contact us at training@ociweb.com.
OCI offers downloads and commercial support for a variety of middleware technologies.
Copyright
©2009
Object Computing, Inc. All rights reserved.
OMG, CORBA, IIOP, and all OMG marks and logs are trademarks or registered
trademarks of Object Management Group, Inc. in the United States and/or
other countries.
Java and all
Java-based marks are trademarks or registered trademarks of Sun
Microsystems, Inc. in the United States and/or other countries.
.NET, C#, and .NET-based marks are trademarks or registered trademarks of Microsoft
Corporation in the United States and/or other countries.