Introduction
CORBA (the Common Object Request Broker Architecture) is a standard of the
Object Management Group (OMG), a consortium of organizations responsible for
the creation and maintenance of this cross-platform distributed system specification.
Visibroker, a product of Inprise, the Borland parent company, is an implementation
of the CORBA standard which runs under Windows 9x, Windows NT and Unix. C++
Builder Enterprise Edition offers experts which greatly simplify the creation
of distributed applications that use CORBA, and, since it comes with Visibroker,
Visibroker is the typical Object Request Broker (ORB) used by C++ Builder developers.
CORBA applications consist of clients and servers. Servers implement services
used by clients, and a server may be a client to another server. Servers are
typically multithreaded, and can also have separate instances running on more
than one network node, each of which has its own threads to service requests
from clients.
The Uses Of CORBA and Distributed Programming
CORBA, like any distributed programming technique, is only useful when you
have a problem which would benefit from the simultaneous application of multiple
CPUs, usually over a network. These problems typically have one or more of the
following special requirements:
-
Parallel computations on separate CPUs are needed to attain high performance.
-
Scalability to a large user population or a high-transaction volume requires
the dynamic addition of computational resources.
-
Fault-tolerance in the face of CPU or network failures requires the ability
to migrate capabilities to accessible computing resources.
How CORBA Works
CORBA aids in solving such problems by providing an abstraction of multiple
independent objects residing on one or more network nodes (determining the location
of the objects is performed by the ORB, and the actual location of the object
used is generally not known to the client). These objects appear to communicate
by standard class method invocations, but CORBA actually puts the arguments
to a method call into one or more network packets (called marshaling),
and sends them to the object, which is typically executing on another CPU. The
object, a multithreaded executable, unpacks the request (called unmarshaling),
and creates or reuses an internal thread to perform the request. That thread
may set up results to be returned to the client, or may throw a CORBA exception.
In either case, the results or exception are marshaled and sent to the client,
which then either gets the result or experiences the exception as a conventional
C++ exception of a specific type (standard CORBA or programmer-defined).
Because CORBA objects can exist on separate nodes in the network, and because
those nodes may have different CPU architectures or operating systems, the marshaling
/ unmarshaling process translates values to the appropriate format for the destination.
For instance, some CPUs use integers with the high order bit to the left, others
use integers with high order bits to the right. CORBA handles these issues without
programmer intervention, but does require the programmer to create a platform
/ language independent specification of the object interface, in an Interface
Description Language (IDL) that is fairly close to C++. The IDL description
is used to create header files that establish the interface for C++ client and
server executables.
The Example Application
The example is a very simple subsection of a distributed ray tracer. A ray
tracer is a program which creates an image based on geometric descriptions of
physical objects and their properties using the laws of optics on simulated
light rays cast into the scene. Because ray tracing is so computationally intensive,
and because many ray-tracing calculations can proceed in parallel, it is a natural
for a CORBA implementation. Note however that the subset shown is very small
and does not involve the performance or merging of parallel computations (that
will be left for a future article).
The application does use a client server database, which makes it essentially
a three-tier application (client, server objects, DBMS).
The portion of the application shown is a client and an object which maintains
a database table of "spaces" within which objects to be raytraced
can reside. Currently, this is a very simple two attribute table, with a space
ID and a space name. The server object is called "multiverse", and
supports adding, removing, getting the names of all spaces, getting the name
of a space based on its ID and getting the ID of a space based on its name.
The Project Group and Projects
The Project Group contains two projects - RayTracer (the client) and Multiverse
(the server object).
The Multiverse Object
First to be created is the Multiverse project. To create this project, first
one creates the IDL file which describes the object interface, using File |
New | Multitier | CORBA IDL File. The following is the text added to the IDL
file:
module Multiverse
{
interface SpaceRegistry
{
exception SpaceNotFound{};
unsigned long AddSpace(in
string theSpaceName);
void DeleteSpace(in unsigned
long theSpaceID) raises (SpaceNotFound);
string NameOf(in unsigned
long theSpaceID) raises (SpaceNotFound);
unsigned long IDOf(in
string theSpaceName) raises (SpaceNotFound);
typedef sequence<string>
NameList;
NameList Spaces();
};
};
As you can see, there are some similarities to C++. "module" is essentially
a namespace declaration, "interface" is essentially a declaration
of the object class, "exception" defines an exception which can be
thrown to the client, and the rest involves methods and type definitions available
to users of the object. Note that method definitions identify the exceptions
that they can throw and that method arguments can be in, out or inout, depending
on whether they are from the client, values returned to the client or both.
Note also that methods can be functions (that is, can return a value).
Note that the Visibroker documentation does not call attention to the need
for a module declaration, but it is not a thing you want to add later (for instance
after you have generated the header files from the IDL).
Naming Things In The IDL File
There are probably many approaches to naming objects in a CORBA IDL. In this
case, all interfaces will reside in the Multiverse module, simply because the
multiverse contains the space registry and the spaces which it references. Other
parts of the application may reside in different modules, which will be determined
later.
Creating The Server Project
The IDL file is then saved and is used as the basis for the server. This is
done with File | New | Multitier | CORBA Server. This server is a Windows application
and uses the IDL file created above. The result is a project with a form, and
the project source contains a couple of calls to initialize the ORB and the
Basic Object Adapter (BOA) which handles activation of the server and registering
the objects with the ORB.
At this point, the Multiverse server project contains the IDL, but the object
represented by the IDL has not been made available to the server. First, compile
the IDL, which creates the C++ header files that represent the server interface.
If you are lucky, you will never ever have to look at these things, which rival
ActiveX header files in their nasty appearance and undocumented complexity -
but you must have them available.
The act of compilation adds two units to your project. These are the _c and
_s .cpp files. Once again, there should be no need to look at these in any detail.
Save the project at this point.
Hooking The IDL To The Server
Next, you must set up the implementation of the interface described by the
IDL. There are two ways to do this. One is to have the IDE generate a class
which inherits from the IDL-based classes in the generated header files. But
the most powerful method, and the method which allows for the easiest use of
VCL components in your server, is to use "delegation".
To set up for delegation, with the IDL selected in your project manager, pick
File | New | CORBA Object Implementation. If you have not picked the IDL in
the project manager, the IDE may appear to stop working until you repeatedly
hit the ESC key, in which case you should select the IDL and repeat the action.
A dialog should appear immediately, showing the IDL file path. Pick the interface
name from the combo (in this case there is only one). Check off the "Delegation
(Tie)" and "Data Module" check boxes. You can change the implementation
class name from the default (for instance SpaceRegistryImpl to SpaceRegistryImplementation,
if you happen to like spelling things out (I do)).
The result of this after hitting OK will be a data module with a header and
.cpp file containing empty method bodies to implement the interface. In this
case it looks like
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include <corba.h>
#include "SpaceRegistryServer.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TSpaceRegistryImplementation *SpaceRegistryImplementation;
//---------------------------------------------------------------------------
__fastcall TSpaceRegistryImplementation::TSpaceRegistryImplementation(TComponent*
Owner)
: TDataModule(Owner)
{
}
//---------------------------------------------------------------------------
char* __fastcall TSpaceRegistryImplementation::NameOf(CORBA::ULong theSpaceID)
{
}
CORBA::ULong __fastcall TSpaceRegistryImplementation::AddSpace(const
char* theSpaceName)
{
}
CORBA::ULong __fastcall TSpaceRegistryImplementation::IDOf(const char*
theSpaceName)
{
}
Multiverse::SpaceRegistry::NameList* __fastcall TSpaceRegistryImplementation::Spaces()
{
}
void __fastcall TSpaceRegistryImplementation::DeleteSpace(CORBA::ULong
theSpaceID)
{
}
You can reformat the code as needed and add any additional class methods, properties
or variables you desire. Obviously, you also need to implement the interface.
Implementing The Interface
All CORBA servers are multithreaded Therefore it is important to protect against
simultaneous access to shared memory or resources. The easiest way to do this
is to use a CORBA mutex to create critical sections in your implementation methods.
First, add the mutex to the implementation data module class definition private
section:
VISMutex SpaceRegistryInstance;
Then at the top of each method implementation (this may be overkill, but is
certainly safest) use the mutex:
VISMutex_var Lock(SpaceRegistryInstance);
The mutex will be automatically released when it goes out of scope (on return
from the method or if an exception is thrown).
Another issue is throwing exceptions. This is handled in a fairly simple fashion,
as illustrated by the implementation of the IDOf method:
VISMutex_var Lock(SpaceRegistryInstance);
MultiverseMainForm->LogMemo->Lines->Add("IDOf");
if (Table->Locate("NAME",theSpaceName,TLocateOptions()))
{
return TableID->AsInteger;
}
else
{
throw Multiverse::SpaceRegistry::SpaceNotFound();
};
A final issue is working with databases. Since the object is multithreaded,
you must have a TSession and TDatabase object specifically placed in the implementation
data module. The following .dfm shows the components used (the database is an
Interbase database). The user name and password are hard coded to avoid any
prompting on object activation.
object SpaceRegistryImpl: TSpaceRegistryImpl
OldCreateOrder = False
Left = 3
Top = 343
Height = 289
Width = 371
object Source: TDataSource
DataSet = Table
Left = 96
Top = 176
end
object Table: TQuery
DatabaseName = 'SpaceRegistryImplDatabase'
SessionName = 'Session1_1'
RequestLive = True
SQL.Strings = (
'SELECT * FROM
MULTIVERSE ORDER BY NAME')
ValidateWithMask = True
Left = 96
Top = 104
object TableID: TIntegerField
DisplayWidth = 10
FieldName = 'ID'
Origin = 'MULTIVERSE.ID'
end
object TableNAME: TStringField
DisplayLabel = 'Name'
DisplayWidth = 80
FieldName = 'NAME'
Origin = 'MULTIVERSE.NAME'
Size = 255
end
end
object IDSource: TQuery
DatabaseName = 'SpaceRegistryImplDatabase'
SessionName = 'Session1_1'
RequestLive = True
SQL.Strings = (
'SELECT
TABLENAME,NEXTID FROM IDSOURCE WHERE TABLENAME = "MULTIVE' +
'RSE"')
ValidateWithMask = True
Left = 216
Top = 104
object IDSourceNEXTID: TIntegerField
FieldName = 'NEXTID'
Origin = 'IDSOURCE.NEXTID'
end
object IDSourceTABLENAME: TStringField
FieldName = 'TABLENAME'
Origin = 'IDSOURCE.TABLENAME'
Size = 255
end
end
object Database: TDatabase
AliasName = 'RayTrace'
Connected = True
DatabaseName = 'SpaceRegistryImplDatabase'
LoginPrompt = False
Params.Strings = (
'USER NAME=multiverse'
'PASSWORD=keytospace')
SessionName = 'Session1_1'
Left = 96
Top = 16
end
object Session1: TSession
Active = True
AutoSessionName = True
Left = 208
Top = 16
end
end
After completing the implementation, compile and link the project. You can
(as I have) use the form to report the actions of the server. This is not required,
and you can use logging or other strategies (or no strategy) for keeping track
of requests and responses. My form shows both the database in a grid and each
method logs a message to a TMemo on the form.
The Client
Creating the client requires a new project. File | New | Multitier | CORBA
Client is the way to get started. Make it a Windows app and use the same IDL
as the server.
Next, add a data module to the project. This will be the client's view of the
server object. With the IDL selected in your project manager, pick Edit | Use
CORBA Object. If you have not picked the IDL in the project manager, the IDE
may appear to stop working until you repeatedly hit the ESC key, in which case
you should select the IDL and repeat the action.
Pick the interface you wish to access from its combo in the dialog. Pick the
data module you just created from the Use In Form page combo. Enter an object
name which is the same as the name you can find in the project source for the
server (in the line Multiverse_tie_SpaceRegistry tie_spaceRegistry_SpaceRegistryObject(*spaceRegistry_SpaceRegistryObject,
"SpaceRegistryObject", true); "SpaceRegistryObject" is the object name), and
change the Use In Form property name to the same as the name of the interface
(by default it gets a lower case initial letter, which can be changed).
The result is a set of class variables, methods and properties. The implementation
of the property getter for the SpaceRegistry will bind to the object if it has
not yet been bound and will return the interface class, which can then be used
quite naturally by the client UI, for instance in a statement like
try
{
MultiverseData->SpaceRegistry->AddSpace(SpaceNameEdit->Text.c_str());
}
catch (const CORBA::SystemException &CorbaException)
{
Application->MessageBox((char *) CorbaException._name(),"Add
Space Click",0L);
};
Note the exception catch, and that the string must be passed to the method
as a char *. The cast on the exception makes the message non-const, which is
required by Application->MessageBox.
Once your client implementation is complete, compile and link its project.
Then you are ready to start testing.
Testing Your Client And Server
There are three things you need to set up before starting local testing. First,
the Object Activation Daemon needs to be run on your machine. This is done by
running oad.exe from the command line, Windows Explorer or a Start Menu entry.
Next, register the server with the OAD, with a batch file or command line like
oadutil reg -r IDL:Multiverse/SpaceRegistry:1.0 -o SpaceRegistryObject
-cpp g:\develop\cbuilder4\test\corba\raytracer\multiverse.exe -a SpaceRegistryObject
-p shared
This command registers the server so that the OAD can find it when the object
is requested (the object name is the -o parameter), and identifies how it should
be activated. In this instance, it is activated as a shared server, which means
it will be started only once and will not be terminated by the OAD. If it were
unshared, a separate instance would be started for each request, and then terminated
when the request is completed. Shared servers are best for frequent requests
and for local testing. Unshared is best for infrequent requests, where the overhead
of startup is not as important.
Finally, run the Smart Agent so that the OAD can be found by the client.
Now you are ready to test. If you are running under Windows NT, you can run
both the client and the server from your project group undr debugger control
in the IDE. If you are running Windows 9x, you must pick one or the other to
run from the IDE and run the other from a command line or Windows Explorer.
It is usually best to start by running the client from the IDE. This lets you
make sure that the client side is OK before trying to debug the server. Note
that the default is for the client to wait forever for a response from the server,
so if the server fails or has a problem, or cannot be found (for instance because
the Smart Agent isn't running, you may need to reset the client from the debugger.
At this point, if the server is throwing an exception or otherwise experiencing
a problem, but the client seems OK, you can run the server from the IDE and
the client from the command line or the Explorer. In this case, you can set
breakpoints etc. as needed, and calls to the server will be trapped by them.
Conclusion
Once you've completed a successful local test, you can use remote debugging
or other strategies to perform remote testing across the network.
In any event, remember that a good distributed application will minimize network
traffic and maximize the use of multiple CPUs.
And have fun with CORBA!
|