Introduction to CORBA with VisiBroker and C++Builder

By: Michael Sawczyn

Abstract: An introductory paper for those familiar with C++ and C++Builder and wanting to learn about CORBA and how to create, use, and deploy CORBA systems.

Introduction

Whats CORBA?

The CORBA (Common Object Request Broker Architecture) standard was developed by the Object Management Group, a membership organization based in Framingham, Massachusetts. From its beginnings in 1989 with eleven member organizations, the OMG has grown to over 800 member companies. At its most basic, CORBA is a way for objects to interoperate across a network.

The CORBA specification v1.1 was introduced in 1991 to give a standard mechanism for objects to communicate across a network. The specification has evolved since that time, with the introduction of CORBA 2.0 in 1994 and CORBA 3.0 soon to be released in 2000. Its important to note that CORBA is a specification, not a product. Vendors wishing to create products that follow the spec are free to do so, but must ensure that they follow the spec precisely in order to claim to be CORBA compliant. Its because of this adherence to a written standard that various compliant products from different vendors are able to interoperate. The standards called The Common Object Request Broker: Architecture and Specification and is available from the OMG for a nominal cost, or available as a free download from their web site. The OMG can be reached at http://www.omg.org.

One important thing to remember is a CORBA is language independent. As long as a mapping exists from the CORBA interface definition language specification to the language you program in, you will be able to write to CORBA applications in the language, and they will be able to communicate with CORBA applications in any other language.

In essence, CORBA defines a standard way for objects to talk to each other without you having to write the code for that communication to happen.

Ok. So whats the big deal?

Well, lets assume that you were sold on the business case for object orientation. You understand the need for thorough object-oriented analysis and design, youre concerned that implementation takes too long.

If thats your concern, youre right. Software development takes too long and costs too much. Typically, each project starts from scratch, which makes large applications that model the enterprise extremely cumbersome to build and costly to maintain.

But the problems with distributed objects dont end there. Its incredibly difficult to get the hardware  computers and networks  to work together. We cant always count on homogenous systems in our networks since the different platforms have different strengths. Unfortunately, the truth is that different platforms dont work and play well together in a heterogeneous system.

As difficult as it is to get hardware to work together, software can be worse. An enterprise system today is a collection of diverse parts, working together toward a common goal. It costs time and money to integrate the disparate parts into a whole, and unfortunately too much effort t in getting the parts to talk to each other, taking energy away from the problem at hand.

CORBA helps in this interoperability dilemma by providing a specification that third parties can adhere to and, if they follow the rules correctly, assure that applications built using their libraries will interoperate.

While CORBA is an excellent accomplishment, it only connects objects, not applications. Enterprise integration requires a lot more than this, and the OMG provides it with the Object Management Architecture (OMA). Based on CORBA, the OMA specifies a set of standard interfaces and functions for components. Its divided into two major components: lower-level CORBAservices and intermediate-level CORBAfacilities.

CORBAservices provide basic functionality that almost any object might need, such as object lifecycle services, naming and directory services, and other basics. These can be thought of as horizontal  CORBAservices touch all different kinds of CORBA applications. In contrast, CORBAfacilities can be thought of as vertical  specifications central to an industry, such as health care, finance, etc., or central to a type of programming task, such as user interface, information or systems management, or task management.

How does Visibroker fit into this?

Visibroker is a CORBA 2.3 compliant object request broker (or ORB), with a suite of tools thrown in. If youre not sure about what an ORB is, dont worry; well be talking more about that as we go on. Visibroker for C++ includes libraries and headers that allow you to write client and server programs that can communicate with each other across a network, without having to deal with the complicated code underlying that communication. If you have a TCP/IP network, Visibroker (like any other CORBA-compliant ORB product) contains all the functionality needed to get your programs to talk to each other.

A Birds Eye View of CORBA

What exactly is CORBA and what are the various components that make up the CORBA client/server pair? The diagram below may help illustrate how communication happens between a CORBA client and CORBA server.

The ORB (object request broker) and POA (portable object adapter) are, for our purposes, C++ objects that you instantiate in your code. They come from the Visibroker libraries that ship with the Visibroker installation and are essentially black boxes that you can use without totally understanding their underlying complexity. From the server side, you write the code for your worker objects (shown in blue). These objects interface with the POA (via generated skeleton code) and the POA interfaces with the ORB so that the ORB may communicate to the client. As you can see from the diagram, the ORBs purpose in life is to provide a communication mechanism between applications.

The skeleton and stub code in the diagram is automatically generated from your object model by tools supplied by the vendor.

From the clients view of the world, the client has a reference to an object that it got from the ORB. In C++, this reference translates to a wholly separate object type that knows it should communicate with the object being referenced on the server. This is quite convenient from the clients perspective, since the client application has no idea that the object its referencing lives on another computer. In fact, CORBA objects are location transparent. This means that the client and server applications may or may not be same computer, in fact they may be on different computers in the network or may actually live in the same box. For that matter, the client and server applications may share the same application space; there is nothing in the specification that tells you how you must partition the client and server code.

Visibroker has optimizations that kick in when the client and server share the same application space. If the client and server code share the same process space, calls from the client to the server are made as direct function calls. If the client and server are different processes in the same computer, client-to-server calls are treated as remote procedure calls. It is only when the client and server run on different computers into the network that TCP/IP packets are sent from application to application.

CORBA provides mechanisms for getting references to objects that live somewhere on the network without necessarily knowing exactly where they live. In fact, they may not even be available at the time the client asks for them. The mechanism of creating an object that doesnt exist is called object activation. We wont be talking about object activation here, so from here on in assumed that any contract referenced from a client already exists on the server.

Client-side references maintain their life span via reference counting. Unfortunately, its up to you as the client application programmer to ensure that when a reference is no longer used, its reference count drops to zero. Each class thats generated contains a _duplicate and _release function for incrementing and decrementing its objects reference counts. In practice, though, this can be an incredibly daunting task, so the CORBA specification outlines the requirements for smart pointer objects that need to be generated along with the C++ translations of the interfaces. Well be talking more about these later in the section entitled <class_name>_var and <class_name>_ptr types.

At the heart of creating CORBA applications is the CORBA interface definition language. Interface definition language (or IDL) contains a full set of variable types that map to many different languages. As of July 1999, language mappings existed for Ada, C, C++, Cobol, Java, and Smalltalk. Well concern ourselves with the C++ mapping here.

Chapter 3 of the CORBA specification outlines the syntax of IDL. IDL contains syntactical elements that allow for both attributes and operations to be specified. For our purposes, we can think of attributes as a member of variables in a class and operations map directly to C++ functions. CORBA does not offer every C++ variable type, and thats primarily due to it being a universal language. Of special note is that pointers are not available in CORBA IDL since not every language offers direct access to pointers. A lot of thought went into the types of variables that would be available in CORBA IDL with the goal of interoperability in mind.

A program that ships with Visibroker, idl2cpp.exe, is used to come to compile CORBA IDL into C++ code. The code thats generated can be used for both client and server applications.

IDL Syntax

Interface definition language is used to describe the interfaces that client objects calling and servers provide. And interface definition written in IDL completely defines the interface and fully specifies each operations parameters. We dont actually write programs in IDL, but use IDL to define the classes of objects that we will be using in our programs.

IDL is very similar to C++, down to preprocessing features. It was written with C++ in mind, and according to the CORBA specification, the OMG IDL specification is expected to track relevant changes to C++ introduced by the ANSI standardization effort.

Files containing interface specifications written in IDL must have an . idl extension. While the formal grammar of IDL uses Extended Backus-Naur Format (EBNF), well discuss its syntax in English. Those of you wishing to view the EBNF are referred to the CORBA specification, chapter 3.

The naming of interfaces

The name of an interface in IDL is similar to the name of a class in C++. But the name changes when we use the interface at runtime. The formal description of an interface name uses the following structure:

IDL:[Module[/Module]/]Name:Version

Where IDL: is required and module names, if present, are listed separated by a / character. The name of the interface comes next, and is separated from the version of the interface by a :. The first version of an interface is, by default, 1.0, and is the version number that will be created IDL compiler unless otherwise specified.

A lot like C++

An IDL file looks a lot like a C++ header file, with a few syntactical differences. In the section below entitled IDL to C++ mappings, well discuss how IDL keywords map to C++ keywords. But since IDL was designed, for the most part, from C++, its not unusual that well see incredible similarities in their structure.

Lets take a look at a prototypical IDL file.

module BorCon2K {

interface Person {
attribute string firstName;
attribute string lastName;

string FullName(in boolean lastNameFirst);
};

interface Time {
attribute unsigned short hour;
attribute unsigned short minute;
};

interface Date {
attribute unsigned short month;
attribute unsigned short day;

short DayOfConference();
};

enum eRoom {
roomA, roomB, roomC, roomD
};

interface Presentation {
attribute Person speaker;
attribute Time presentationTime;
attribute Date presentationDate;
attribute eRoom presentationRoom;
attribute string topic;

float GetPreviousRating(inout short year);
};

interface Tutorial : Presentation {
attribute boolean onCD;
};

typedef sequence<Presentation> PresentationSeq;
};

Just from looking at the above you can, as a C++ programmer, probably figure out what its trying to do. The module declaration looks suspiciously like a namespace, and indeed thats what it maps to in C++. The interface declarations look like they have member variables and functions, therefore theyre probably classes. Right again  interfaces map directly to classes in C++.

A few differences between IDL and C++ need discussed, though. Some are pretty obvious, some subtle.

Youll notice the attribute keyword used throughout the interface definitions. Thats a required keyword when youre declaring member variables. You also may have noticed that certain C++ types look subtly different, like boolean instead of bool.

Operations (functions) also have modifiers on their parameters. In the example above, you may have noticed in and inout. While we didnt use it, another modifier is out; these argument modifiers clue the ORB on how to package parameters. Parameters that are in wont be modified, while inout or out parameters are expected to be modified by the called function. An inout parameter has a value that the caller has set that the function will use and then modify, while an out parameter has no useful information that the caller has set, but will have useful information in it on return from the function.

One subtle difference is a lack of visibility specifiers. All operations in IDL are presumed to be public, as are all inheritances. Attributes will have public accessor and mutator (i.e., get and set) functions created for them, and it will be up to the implementation to actually create the variables that hold that data, if theyre there at all. BCB programmers can think of attributes as properties: theyre accessed via functions and may or may not actually have 1-for-1 correlations with data members that store the information those functions get and set.

IDL to C++ mappings

Lets look at the various mappings between IDL and C++.

IDL type

WIN32 C++ definition

Visibroker type

short

short

CORBA::Short

long

long

CORBA::Long

unsigned short

unsigned short

CORBA::UShort

unsigned long

unsigned long

CORBA::ULong

float

float

CORBA::Float

double

double

CORBA::Double

char

char

CORBA::Char

boolean

unsigned char

CORBA::Boolean

octet

unsigned char

CORBA::Octet

long long

VISLongLong

CORBA::LongLong

ulong long

VISULongLong

CORBA::ULongLong

One thing youll immediately note is that in the second column, Visibroker type, each attribute type is prefixed with CORBA::. This is because with Visibroker creates a namespace called CORBA and defines all CORBA functions and typedefs in that namespace. Interestingly enough, there is no integer type. Integers are divided into either shorts or longs. Again, this is due to CORBAs desire to be language independent. IDL also offers two types that are not available in standard C++: long long and unsigned long long. Since they are not present in C++, they are implemented in Visibroker as the classes VISLongLong and VISULongLong.

Its also important to note that the IDL boolean type is defined by CORBA to have only one of two values: 1 or 0. Using other values for a Boolean will result in the undefined behavior. You would think that the Boolean type would map to the C++ bool type directly, but remember that the specification for C++ mapping was written long before the Boolean type was added to the language.

Modules

The CORBA specification states that an IDL module maps to a C++ namespace. By default, though, Visibroker maps the module keyword to a C++ class, which is not in compliance with the standard. This is done because older compilers may not necessarily support the namespace keyword. In Visibroker 4.0 a switch to the C++ code generator idl2cpp (the -namespace switch) can be used to cause a module to generate a namespace.

Modules can be used quite effectively to separate class names from class names that could have been created by other programmers. The designers of IDL realized that the problem C++ programmers were running into with namespace clashes would certainly be a problem in IDL. The module keyword was created to help alleviate that problem.

Interfaces

Interface specifications can contain constants, attributes, operations, and typedefs. An interface maps directly to a C++ class, and contains attributes and operations specific to that class. There are no visibility specifiers in interfaces, instead, all attributes and operations are presumed to be public. Should you need protected or private members of the C++ class, youll define them in the implementation class.

Attributes in interfaces generate accessor and mutator functions that have defined signatures. For example, in attribute defined as

attribute string someString;

would generate two functions:

void _set_someString(const char* _someString);
const char* _get_someString();

In normal practice, you wont use these functions. The generated client-side code will create two alternate functions for use by the client:

void someString(const char* _someString);
const char* someString();

When a client calls these functions, the client-side generated the stub code packages the parameter (if applicable) and the ORB that was instantiated by the client communicates with the ORB on the server. The server-side ORB calls a servant object whose purpose in life is to communicate with the implementation object you wrote in the server application. Its in the servant object where the _get_ and _set_ functions are defined.

For the most part, unless youre writing applications that take advantage of the Dynamic Invocation Interface, you wont need to worry about the _get_ or _set_ functions. The Dynamic Invocation Interface specification is beyond what were going to cover in this document for more information see the CORBA specification.

Attributes and Operations

The IDL keyword attribute is used to preface member variables. Operations are specified much in the same way that they would be specified in C++, but arguments must be specified as in, out, or inout. This argument specification is required to help the communications functions determine how to package the arguments for sending between the client and the server. As you might guess, sending a unit of information with no expectation that it will be modified is vastly different than sending a piece of data that need be changed and that change must be noted by the sender.

Strings

CORBA IDL supports string objects directly. Strings are specified in IDL using the keyword string, and optionally modified by specifying the maximum length for the string using angle brackets, as in

attribute string<36> aString;

Here, the variable aString is restricted to 36 characters. This maps to a character pointer (char*) in C++, and since character pointers dont contain length information, the 36-character restriction is discarded information. Other languages, though, could use the restriction when their code is being generated.

Allocating and freeing string storage space in CORBA clients and servers is a bit problematic. Since a string could be returned from a server to a client, or passed from a client to a server, and thus need to be shared between process spaces, the CORBA libraries provide specific functions to allocate and free string memory. The functions CORBA::string_alloc and CORBA::string_free should be used for string parameters passed between CORBA-compliant programs. CORBA::string_dup is also available as a quick way of allocating and setting CORBA allocated memory by using quoted strings or other char* types, as in

some_function_that_wants_a_string(
CORBA::string_dup(some string));

Constants

CORBA IDL offers the concept of the constant, much in the same way that C++ has constants. In fact, IDL was modeled very closely after C++, so its no surprise that much of its syntax looks quite a bit like C++. Using the const keyword, as in the following, specifies a constant:

 

const long l = 100;

Constants can be specified at any level, either inside or outside an interface (class) definition.

Enumerations and Typedefs

Since IDL was closely modeled after C++, its no surprise that enumerations (enum) and type definitions (typedef) are also available. The syntax for these two are identical to C++.

Structures

An IDL struct maps directly to a C++ struct in much the same way that an interface maps to a class. The difference here is that a struct may not contain operations, which is a departure from C++. A struct in CORBA is identical to a struct in C.

Other types

Other mappings from IDL to C++ are available, including arrays, values, and containers. For our introductory talk, though, youve got more than enough to get started. More information can be found at the OMG web site, http://www.omg.org/.

IDL2CPP generated types

The IDL2CPP compiler generates a number of types for you. Lets discuss some of those.

<interface_name> class

The <interface_name> class is generated for a particular IDL interface, intended for use my client applications. This class provides all the stub methods defined for a particular IDL interface. When a client calls an operation on a server-side object through an object reference, the stub methods are actually invoked. These stub methods package the parameters for the operation, send them to the object implementation, and unpackage the results upon return. This entire process is transparent to the client application. Note that you should never modify the contents of a stub class generated by the IDL compiler.

_POA_<class_name> and _sk_<class_name> classes

The _POA_<class_name> class is an abstract base class generated by the IDL compiler that is used as a base class for your implementation class. Object implementations are usually derived from this type of servant class, which provides the necessary methods for receiving and interpreting client operation requests.

The _sk_<class_name> is generated if you use the -boa option to IDL2CPP, as is the default for IDE-based CORBA servers. Both of these classes help satisfy the requirement that all object implementations derive from CORBA::Object.

_tie_<class_name> class

It became clear to the developers of CORBA thats not every programmer would want to restructure their object inheritance hierarchy to derive every class from CORBA::Object. The _tie_<class_name> class is generated by the IDL compiler to help in those cases.

The tie class serves as a wrapper around your implementation class. It duplicates all functions in your implementation class and holds a pointer to an object of your type. A tie object derives from CORBA::Object, satisfying the inheritance requirement, and all client requests are passed to the tie object. Since this object holds a pointer to your implementation object, it calls your object with the parameters passed to it, and forwards your return value back to the client.

<class_name>_var and <class_name>_ptr types

For any given interface type, a_var and a _ptr type will be generated. The _ptr type is a typedef that can be used in place of a <class_name>*, and the_var type is a class that an capsulate a smart pointer for references to <class_name>.

You are strongly encouraged to use the smart pointer types whenever possible. Since the CORBA references use reference counting to determine when they should be deleted, using the smart pointers reduces the headaches youll have to face in trying to keep the reference counts accurate.

Sequences

A sequence is a C++ class thats generated from the following IDL:

typedef sequence<someType> sequenceName;

where someType is an IDL type (either a built-in type or a predefined interface) and sequenceName is the name you've given to this new sequence type. This class is a container class, and can hold either a defined number of objects or an undefined number (i.e., be either bounded or unbounded).

Lets look, for example, at sequence of long integers. The IDL would for that would look like the following:

typedef sequence<long> LongSeq;

Note that youre required to typedef the sequence in order for the IDL2CPP compiler to generate a class name for the sequence.

The C++ class is generated from this code looks like the following:

class LongSeq {

private:

CORBA::Long* _contents;

CORBA::ULong _count;

CORBA::ULong _num_allocated;

CORBA::Long _ref_count;

CORBA::Boolean _release_flag;

public:

static CORBA::Long *allocbuf(CORBA::ULong _nelems);

static void freebuf(CORBA::Long *_data);

LongSeq(CORBA::ULong _max=0);

LongSeq(CORBA::ULong _max,

CORBA::ULong _len,

CORBA::Long *_data,

CORBA::Boolean _release=0);

LongSeq(const LongSeq&);

~LongSeq();

 

LongSeq& operator=(const LongSeq&);

CORBA::ULong maximum() const { return _num_allocated; }

void length(CORBA::ULong _len);

CORBA::ULong length() const { return _count;}

CORBA::Long& operator[](CORBA::ULong _index) ;

const CORBA::Long& operator[](CORBA::ULong _index) const;

 

friend VISostream& operator<<(VISostream&, const LongSeq&);

inline friend VISostream& operator<<(VISostream& _strm,
const LongSeq *_obj) {

if ( _obj == (LongSeq*)NULL )

throw CORBA::BAD_PARAM();

else

_strm << *_obj;

return _strm;

}

 

friend VISistream& operator>>(VISistream&, LongSeq&);

inline friend VISistream& operator>>(VISistream& _strm,
LongSeq_ptr & _obj) {

_obj = new LongSeq;

_strm >> *_obj;

return _strm;

}

 

friend Ostream& operator<<(Ostream&, const LongSeq&);

inline friend Istream& operator>>(Istream& _strm, LongSeq& _obj) {

VISistream _istrm(_strm);

_istrm >> _obj;

return _strm;

}

 

inline friend Istream& operator>>(Istream& _strm,
LongSeq_ptr & _obj) {

VISistream _istrm(_strm);

_istrm >> _obj;

return _strm;

}

 

static LongSeq *_duplicate(LongSeq* _ptr) {

if (_ptr) _ptr->_ref_count++;

return _ptr;

}

 

static void _release(LongSeq *_ptr) {

if (_ptr && ( --_ptr->_ref_count == 0) ) delete _ptr;

}

 

};

One of the nice things about the sequence function is that it overloads the [] operator, giving you random access to the contents of the sequence. Adding an element to sequence is a simple matter of assigning an object to the sequence using these operators, as in

LongSeq_var mySequence = new LongSeq;
mySequence[0] = 70L;
mySequence[1] = 100L;

Its important to note that when creating a sequence, youre required to specify the length of the sequence  it doesnt do it automatically. So in order to finish off the above code, we would need to add the line

mySequence.length(2);

so that when other code queries the sequence forgets length, it will return the appropriate value.

CORBA servers are the workforce applications of the client/server model. Typically, the CORBA server is written as a console mode application, since it does not usually require a pretty user interface. Its the client applications that typically contain a GUI front-end since theyll need to be accessed by regular users. The servers the application that sits behind locked doors in the computer room, silently chugging away and providing the objects to the clients when they request them.

Borland C++Builders wizards allow for quick creation of CORBA server code from IDL. You dont need to use the wizard to create a server, but it sure makes life easier. Lets take a look. The following will work in either version 4 or version 5 of C++Builder Enterprise.

Open up BCB and select File/New and, on the multitier tab, click CORBA Server. The window you see should look something like the following:

After clicking on CORBA server, you should see the following dialog:

Here is where you select whether your application will be a console app or a Windows app. For our purposes in this demonstration lets create a console mode application for the server. Just click OK without adding any IDL files at this time. You should see following code opened in your editor window.

//--------------------------------------------------------------------

 

1 #include <corbapch.h>

2 #pragma hdrstop

 

//--------------------------------------------------------------------

 

3 #include <corba.h>

4 #include <condefs.h>

5 #pragma argsused

6 main(int argc, char* argv[])

7 {

8 try

9 {

10 // Initialize the ORB and BOA

11 CORBA::ORB_var orb = CORBA::ORB_init(argc, argv);

12 CORBA::BOA_var boa = orb->BOA_init(argc, argv);

13 // Wait for incoming requests

14 boa->impl_is_ready();

15 }

16 catch(const CORBA::Exception& e)

17 {

18 Cerr << e << endl;

19 return(1);

20 }

21 return 0;

22 }

//--------------------------------------------------------------------

Lets take a look at what we got.

Line 1 includes an optimized set of precompiled headers for CORBA work. These are included here rather than down in line 3 since they will be used for any type of CORBA application, from clients to servers to extensions.

Now look at line 11. Heres where the fun begins. In line 11 were creating a reference to the ORB, the object that will be used to communicate to our client applications. Well be calling the function

CORBA::ORB_init(int argc, char** argv)

which passes in argc and argv from main, in case we started the server with any ORB-specific parameters. You probably wont need any parameters, but theyre there in case you need them.

The reference to the ORB is stored in an ORB_var, one of the smart pointer generated types we talked about before.

In line 12, we call a function off the ORB to get a reference to a BOA. The BOA (basic object adaptor) is the object that comes from the Visibroker libraries and serves to connect our objects to the ORB. Well be storing the reference to the BOA in the smart pointer variable as well.

We should take a moment to note that the BOA space is no longer used in the CORBA 2.3. The object that connects user objects to the ORB is the POA, or portable object adapter. The BOA is a relic from a previous version of CORBA, but is implemented in with Visibroker 4.0 as a wrapper around the POA. In reality, discussing the POA is more than we want to go into here. The BOA is a much simpler object, and since Visibroker implements it correctly for CORBA 2.3, well continue to use the BOA in our code. To get the IDL compiler to generate code that uses the BOA, we need to pass the -boa to it at IDL compilation time. In the project options dialog, this flag is passed by default. If you change this default value, you will need to manually change the code that the wizard generates for your application to use a POA.

The comment on line 13 tells us exactly whats going to happen next: line of 14 waits for incoming requests. Unfortunately, although our server will work, it doesnt do anything. We havent created an object for our clients to link to. Lets do that now.

From the main menu, choose File/New and click on the multitier tab. This time select CORBA IDL File and click OK. Youll get a new blank file with an .idl extension. Enter the following code:

interface foo {

attribute string s1;

};

Now save the file and youll see, by looking at the project manager window, that its been added to the project. Right-click on it there and choose Compile.

If everything goes right now, youll have two more files added to your project. If you saved the IDL file as file1.idl, the new files will be named file1_c.cpp and file1_s.cpp. Lets talk about these two for a moment.

The _c file contains code that should be compiled into the client application. The _s file contains code for the server. Due to the structure of the inheritance hierarchy, the client code is necessary for the server side application as well. So youll need to compile both of these files into your server application.

The code generated by IDL2CPP should never been modified directly by you. If you make changes to your IDL file, recompile the IDL and new generated files will be created.

Youll also see header files that have been generated for both the client and server side modules. C++Builders wizard will include the headers in the correct modules for you.

Instantiating an object implementation

Now that we have the source code generated for our object, we needed to make the object available to the client by instantiating it before the client asks for a reference to it. The easiest place to do this is in main, right before we wait for incoming requests.

Again, choose File/New from the main menu but this time, on the multitier tab, choose CORBA object implementation. You should see a dialog that looks something like this:

This dialog gives you a choice of IDL files that are in your project. After choosing an IDL file from the first combo box, the interfaces listed in that IDL file will be available for you in the second combo box. Our project only has one IDL file, so choosing that will show us that foo is the only interface available in that file. After selecting the foo interface in the second combo box, we get a dialog that looks something like this:

As you can see, the implementation class name and unit name (i.e., the name of the .cpp file) has been filled in for you. At the bottom of this dialog, in the instantiation section, were given the option to create an object in the main function. By default that is checked, and since we want to instantiate an object in main, well leave it like that. Click OK.

Youll see now look back in another file, fooServer.cpp, has been added to the project. Lets take a look at the contents of that file.

//--------------------------------------------------------------------

 

#include <corbapch.h>

#pragma hdrstop

 

#include <corba.h>

#include "fooServer.h"

//--------------------------------------------------------------------

#pragma package(smart_init)

 

 

fooImpl::fooImpl(const char *object_name):

_sk_foo(object_name)

{

}

 

char* fooImpl::s1()

{

}

 

void fooImpl::s1(const char* _s1)

{

}

Since we only had one attribute in this very simple interface, only a few functions were generated for us. A constructor was generated for the class fooImpl to give us a chance to do any setup work for the object. Youll see that the implementation object were defined, fooImpl, is derived from some code generated by IDL2CPP. This class, _sk_foo, is called a skeleton class. Its derived from the foo class that the client-side code will use to access our object across the network. The parameter to the constructor allows us to name this object so that clients may access it by name. This name isnt a part of the CORBA standard, but a Visibroker extension to the standard. (Extensions are allowed under the standard, as long as the entire specification in the standard is followed.) Well talk more about named objects later when we build our client.

In this example we dont need to do anything so well leave the constructor empty. Lets take a look at the two accessor functions that were created for our attribute.

Writing attribute accessors and mutators

The CORBA specification indicates that accessor (i.e., get functions) and mutators (i.e., set functions) should be named the same as the attribute specified in the IDL. Get functions return the type of the attribute, and set functions return void but take one argument which is of the same type as the attribute. Our attribute, s1, was a string attribute, and as we have discussed previously a string maps to a C++ char*.

The body of the get and set functions are left blank since, obviously, the code generator has no idea what we want to do with these functions. We need to add implementation code, so lets do that now.

Change the two functions to look like the following:

char* fooImpl::s1()

{

static int counter = 0;

char buffer[100];

sprintf(buffer, string %d, counter++));

return CORBA::string_dup(buffer);

}

 

void fooImpl::s1(const char* _s1)

{

cout << Client tried to set s1 to  << _s1 << endl;

CORBA::string_free(_s1);

}

Remember that, in order to use strings, we need to allocate and a free memory using the supplied CORBA string allocation functions. Thats why we didnt simply allocate the memory using new and free the memory with the delete operator.

Looking back into our main module, youll see that the code in the try block has changed as well. It now looks like

// Initialize the ORB and BOA

CORBA::ORB_var orb = CORBA::ORB_init(argc, argv);

CORBA::BOA_var boa = orb->BOA_init(argc, argv);

fooImpl foo_fooObject("fooObject");

boa->obj_is_ready(&foo_fooObject);

// Wait for incoming requests

boa->impl_is_ready();

Our fooImpl objector has been instantiated, and a call to boa->obj_is_ready has been acted prior the code which waits for incoming requests. That function call, off the BOA, register is our object with the BOA. Now a request from the client transmitted to our object through the BOA.

The call to boa->impl_is_ready causes the server to block waiting for calls from clients. If for some reason we decide that the server should terminate, it will be required to call the function boa->shutdown to cause execution to return to the main function and continue after that function call. As you can see from the code, this will cause main to exit.

Our server is ready to go. It only provides one object, and a pretty skimpy object at that. But, what the heck, its a working server. Lets create a client.

Save your server project and lets create a new client project.

CORBA client wizard

Choose File/New and, on the multitier tab, select CORBA client application. You should see a dialog that looks something like this:

At the top, were given the opportunity to choose between a console and a Windows application. Windows applications automatically have VCL enabled, while console applications can optionally enable VCL in order to use such classes as TDatabase and other nonvisual VCL components. Lets create a Windows app, the default.

In the initialization section, we have the opportunity to initialize a BOA. This isnt important unless were going to be creating objects for use by other clients. By default, this checkbox is checked, and it wont cause any harm to leave it checked. For our example we wont be using the BOA, so it doesnt make any difference whether you leave it checked or not.

Were also given the opportunity, in this wizard, to add IDL files so that we can compile them into client-side C++ code. Lets go ahead and do this. Click the Add button and to find the file1.idl file that we created for the server. Once you have done this, click OK to dismiss this dialog.

The code generated in WinMain looks very much like in the other BCB application. The only difference is the instantiation of the ORB object and the instantiation of the BOA.

Adding the stubs

If we were to compile the IDL file now (by right clicking on it in the project manager and choosing compile), youd get both the client- and server-side .cpp files in your project. Since were writing the client here, we dont need the server code, so lets do something about that. Edit the project options by either right clicking on the .exe in the project manager or choosing Project/Options from the main menu. Navigate to the CORBA tab and youll see these defaults:

There are a good in the options available to us in this configuration tab, but the one were most interested in is the bottom-left: IDL compilation. Clear the Add server unit to project checkbox and click OK.

A most snappy user interface

Lets pop over to the main form and add three controls: a listbox to hold values we get from the server, a button to call the get function from our server-side object, and a button to call the set function on the same object. Your form should look like this (or something significantly more aesthetically pleasing  I never said I was an artist):

Now comes the first bit of code that we have to add that doesnt come from a wizard. We do some way of getting a reference server-side object  but how do we do that?

The CORBA specification offers two methods of getting the object references: a function from the ORB that takes a string-encoded representation (called a stringification) of the object and turns it into an object reference, and another function off the ORB that takes the name of a well-known service and returns a reference to an object supplying that service. We dont have a stringified object, and our object does not supply a well-known service. What do we do?

Visibroker offers an extension to the CORBA specification that allows us to get an object reference (called binding to an object) by specifying the IDL type of the object and optionally an object name. Remember the constructor parameter to our server-side object?

If we had no idea what the name of the object was, we could attempt to bind to any object of the correct type. We wouldnt know precisely which object we got, but in most cases it may not be important.

Our generated client-side code contains a bind function for each interface in the IDL. The bind function is overloaded a number of times, but for our purposes were interested in the bind function that takes one char* as a parameter. That parameter is the name of the object, and we can pass a null if we dont know (or dont care about) the name of the object were after.

For the curious, lets consider the situation where the server was not created using Visibroker. In that case, we would in the need to have a stringified object. We would have to get the stringification from the server, and a common mechanism for doing that is to create the object, and by using a function off the ORB called object_to_string, create the stringification and write it to persistent storage (say, to a disk file). The client would then access the stringification and convert into an object reference. Messy? You bet. Thats why well use binding by name.

Since our client is fairly simple (we are only going to be talking to one object), lets bind to it at form-creation time. Double-click on the form and add the following code:

void __fastcall TForm1::FormCreate(TObject *Sender)

{

objRef = foo::_bind(NULL);

}

And, of course, well need to add the variable to our form, so put the following in the private section of the TForm1 declaration:

foo_var objRef;

Note that were using a foo_var, an automatically generated smart pointer, to hold the reference to the server-side object. That way we wont have to concern ourselves with ensuring that the object reference count is kept accurate  we can leave it up to the smart pointer to do that for us.

In order to called the Handset functions in our server-side object, we need to add some code to the buttons on our form. Double-click each button and ask some code that looks like the following:

//--------------------------------------------------------------------

void __fastcall TForm1::Button1Click(TObject *Sender)

{

CORBA::String_var temp = objRef->s1();

ListBox1->Items->Add((const char*)temp);

}

//--------------------------------------------------------------------

void __fastcall TForm1::Button2Click(TObject *Sender)

{

AnsiString temp = TDateTime::CurrentDateTime();

objRef->s1(CORBA::string_dup(temp.c_str()));

}

//--------------------------------------------------------------------

In Button1Click, were calling the get function from our object in storing its result in the listbox. By storing the return value in a CORBA::String_var object, we allow that object to manage the strings memory, freeing us from having to remember to call CORBA::string_free when we were done with the memory. In Button2Click, were calling the sack function on the objects s1 attribute and passing a string with the current date and time to the server. Using these server code we wrote previously, that string will be displayed on the console.

Save and compile your client were done. Its not much, granted, but weve now created a working CORBA client and server. By following the conventions here, it would not have mattered in which language either application were written in. The ORB solves the problem of data translation between languages since, regardless of the language either the client or server is written in, the object model is the same IDL.

One of the major achievements of the CORBA specification is the rule set detailing who is responsible for memory allocation and deallocation. The following is excerpted from the CORBA C++ language mapping, section 1.22.1.

Table 1-3 displays the mapping for the basic OMG IDL parameter passing modes and return type according to the type being passed or returned, while Table 1-4 displays the same information for T_var types. T_var Argument and Result Passing is merely for informational purposes; it is expected that operation signatures for both clients and servers will be written in terms of the parameter passing modes shown in Basic Argument and Result Passing, with the exception that the T_out types will be used as the actual parameter types for all out parameters. It is also expected that T_var types will support the necessary conversion operators to allow them to be passed directly. Callers should always pass instances of either T_var types or the base types shown in Basic Argument and Result Passing, and callees should treat their T_out parameters as if they were actually the corresponding underlying types shown in Table 1-3.

In Table 1-3, fixed-length arrays are the only case where the type of an out parameter differs from a return value, which is necessary because C++ does not allow a function to return an array. The mapping returns a pointer to a slice of the array, where a slice is an array with all the dimensions of the original specified except the first one.

A caller is responsible for providing storage for all arguments passed as in arguments.

Basic Argument and Result Passing

Table 1-3

Data Type

In

Inout

Out

Return

short

Short

Short&

Short&

Short

long

Long

Long&

Long&

Long

long long

LongLong

LongLong&

LongLong&

LongLong

unsigned short

UShort

UShort&

UShort&

UShort

unsigned long

ULong

ULong&

ULong&

ULong

unsigned long long

ULongLong

ULongLong&

ULongLong&

ULongLong

float

Float

Float&

Float&

Float

double

Double

Double&

Double&

Double

long double

LongDouble

LongDouble&

LongDouble&

LongDouble

boolean

Boolean

Boolean&

Boolean&

Boolean

char

Char

Char&

Char&

Char

wchar

WChar

WChar&

WChar&

WChar

octet

Octet

Octet&

Octet&

Octet

enum

enum

enum&

enum&

enum

object reference

ptr

objref_ptr

objref_ptr&

objref_ptr&

struct, fixed

const struct&

struct&

struct&

struct

struct, variable

const struct&

struct&

struct*&

struct*

union, fixed

const union&

union&

union&

union

union, variable

const union&

union&

union*&

union*

string

const char*

char*&

char*&

char*

wstring

const WChar*

WChar*&

WChar*&

WChar*

sequence

const sequence&

sequence&

sequence*&

sequence*

array, fixed

const array

array

array

array slice*

array, variable

const array

array

array slice*&

array slice*

any

const any&

any&

any*&

any*

fixed

const fixed&

fixed&

fixed&

fixed

valuetype

valuetype*

valuetype*&

valuetype*&

valuetype*

 

T_var Argument and Result Passing

Data Type

In

Inout

Out

Return

object reference var

const objref_var&

objref_var&

objref_var&

objref_var

struct_var

const struct_var&

struct_var&

struct_var&

struct_var

union_var

const union_var&

union_var&

union_var&

union_var

string_var

const string_var&

string_var&

string_var&

string_var

sequence_var

const sequence_var&

sequence_var&

sequence_var&

sequence_var

array_var

const array_var&

array_var&

array_var&

array_var

any_var

const any_var&

any_var&

any_var&

any_var

valuetype_var

const valuetype_var&

valuetype_var&

valuetype_var&

valuetype_var

Caller Argument Storage Responsibilities and Argument Passing Cases

The following describe the callers responsibility for storage associated with inout and out parameters and for return results.

Type

Inout Param

Out Param

Return Result

short

1

1

1

long

1

1

1

long long

1

1

1

unsigned short

1

1

1

unsigned long

1

1

1

unsigned long long

1

1

1

float

1

1

1

double

1

1

1

long double

1

1

1

boolean

1

1

1

char

1

1

1

wchar

1

1

1

octet

1

1

1

enum

1

1

1

object reference ptr

2

2

2

struct, fixed

1

1

1

struct, variable

1

3

3

union, fixed

1

1

1

union, variable

1

3

3

string

4

3

3

wstring

4

3

3

sequence

5

3

3

array, fixed

1

1

6

array, variable

1

6

6

any

5

3

3

fixed

1

1

1

valuetype

7

7

7

1.      Caller allocates all necessary storage, except that which may be encapsulated and managed within the parameter itself. For inout parameters, the caller provides the initial value, and the callee may change that value. For out parameters, the caller allocates the storage but need not initialize it, and the callee sets the value. Function returns are by value.

2.      Caller allocates storage for the object reference. For inout parameters, the caller provides an initial value; if the callee wants to reassign the inout parameter, it will first call CORBA::release on the original input value. To continue to use an object reference passed in as an inout, the caller must first duplicate the reference. The caller is responsible for the release of all out and return object references. Release of all object references embedded in other structures is performed automatically by the structures themselves.

3.      For out parameters, the caller allocates a pointer and passes it by reference to the callee. The callee sets the pointer to point to a valid instance of the parameters type. For returns, the callee returns a similar pointer. The callee is not allowed to return a null pointer in either case. In both cases, the caller is responsible for releasing the returned storage. To maintain local/remote transparency, the caller must always release the returned storage, regardless of whether the callee is located in the same address space as the caller or is located in a different address space. Following the completion of a request, the caller is not allowed to modify any values in the returned storageto do so, the caller must first copy the returned instance into a new instance, then modify the new instance.

4.      For inout strings, the caller provides storage for both the input string and the char* or wchar* pointing to it. Since the callee may deallocate the input string and reassign the char* or wchar* to point to new storage to hold the output value, the caller should allocate the input string using string_alloc() or wstring_alloc(). The size of the out string is therefore not limited by the size of the in string. The caller is responsible for deleting the storage for the out using string_free() or wstring_free(). The callee is not allowed to return a null pointer for an inout, out, or return value.

5.      For inout sequences and anys, assignment or modification of the sequence or any may cause deallocation of owned storage before any reallocation occurs, depending upon the state of the Boolean release parameter with which the sequence or any was constructed.

6.      For out parameters, the caller allocates a pointer to an array slice, which has all the same dimensions of the original array except the first, and passes the pointer by reference to the callee. The callee sets the pointer to point to a valid instance of the array. For returns, the callee returns a similar pointer. The callee is not allowed to return a null pointer in either case. In both cases, the caller is responsible for releasing the returned storage. To maintain local/remote transparency, the caller must always release the returned storage, regardless of whether the callee is located in the same address space as the caller or is located in a different address space. Following completion of a request, the caller is not allowed to modify any values in the returned storageto do so, the caller must first copy the returned array instance into a new array instance, then modify the new instance.

7.      Caller allocates storage for the valuetype instance. For inout parameters, the caller provides an initial value; if the callee wants to reassign the inout pointer value to point to a different valuetype instance, it will first call _remove_ref on the original input valuetype. To continue to use a valuetype instance passed in as an inout after the invoked operation returns, the caller must first invoke _add_ref on the valuetype instance. The caller is responsible for invoking _remove_ref on all out and return valuetype instances. The reduction of reference counts via _remove_ref for all valuetype instances embedded in other structures is performed automatically by the structures themselves.


About the Author

Michael Sawczyn, Ohio Police & Fire Pension Fund

A speaker at previous Borland Conferences, Michael Sawczyn is the chief technology officer at the Ohio Police & Fire Pension Fund and an adjunct professor of computer science at Franklin University in Columbus, Ohio.

Paper originally presented at the 11th Annual Borland Conference, July 2000.


Server Response from: ETNASC01