Multi-Language CORBA Development with C++, Java, Perl, and C#

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 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.

  • 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.

    1. 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.

    2. 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.

    3. 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".

    4. 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.

    1. 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.

    2. 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.

    3. 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 method object_to_string() and written to a file on disk.

    4. 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 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().

    5. 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 Client

    With the server complete, we can now build a client of the server.

    1. Initialize the ORB

      As before, to start using CORBA functionality, we must call ORB_init().

    2. 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 call string_to_object() to convert it into a usable reference.

    3. Invoke the Add() operation

      Now that a reference to the Math object has been obtained, we can invoke the Add() method.

    4. Upon termination, free the resources used by the ORB

      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 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