CORBA is dead!

Though it's no longer trendy, CORBA is alive and well in Linux. The rumors of CORBA's demise have been greatly exaggerated. In fact, the Common Object Request Broker Architecture is alive and well, and provides valuable services.

Every five years or so, a technology comes along that sounds like the solution to every software developer’s problem and the answer to every IT manager’s prayer. The Common Object Request Broker Architecture (CORBA) was one of those technologies. Launched in 1991, CORBA was to take the countless object-oriented, distributed, but very proprietary systems that had been developed during the 1980s and unite them under a single standard. Systems that conformed to the CORBA specification could work together transparently: applications could be assembled from whatever software components were available to provide the necessary services, regardless of what language they were written in, what operating system they ran on, or which company sold them. CORBA promised to help developers build custom distributed systems faster and cheaper than ever before. Perhaps predictably, it didn’t quite work out that way.

The first implementations of CORBA were difficult to learn, expensive to build, and so bulky and slow that they taxed even the most powerful desktop computers of the day. Still, CORBA showed potential and became popular enough that in 1996 the Object Management Group (OMG) released version 2 of the spec, making CORBA objects more sophisticated and even more compatible with each other. By that time, however, five years had passed, and another new technology promised to be the solution to all our problems: the Internet.

Suddenly, rather than trying to paste systems together with CORBA glue, companies began to think of ways to link their software services through the World Wide Web. That made sense, too: if you want to distribute a database across an enterprise, why build a custom, three-tier, client/server system in CORBA? Coding up a form in HTML and sending database queries through the web is usually simpler, cheaper, and just as effective.

As a result, many organizations eventually chose web browsers to take on the role that CORBA was supposed to have played. Elsewhere, CORBA’s younger, hipper competitors took over, such as Microsoft’s Distributed Component Object Model (DCOM) and Sun’s Remote Method Invocation (RMI). Today, the latest buzzword in distributed software technologies is the Simple Object Access Protocol (SOAP), and according to the hype, it can provide all the features of CORBA in a cleaner, 100 percent Internet-compatible package. The result: CORBA is dead, dying, or merely forgotten.

A CORBA Renaissance

Or is it? DCOM doesn’t work without Windows, RMI won’t work without Java, SOAP scales poorly, and not every application can be squeezed into the confines of a web browser. Moreover, the computing world has changed a lot since 1991. The powerful computers needed to run CORBA back in those days can now fit in the palm of your hand, so the overhead and bulk of CORBA is no longer a major issue. Meanwhile, the CORBA specification has been cleaned up and clarified, dozens of books have been written about it, and thousands of engineers have been trained to use it. That means CORBA isn’t as esoteric as it once was, and its learning curve isn’t as steep. More importantly, the need to build distributed systems hasn’t gone away.

Despite its stalwart competition and its age, CORBA still provides three valuable services:

* HIGH-LEVEL REMOTE COMMUNICATION. Berkeley-style sockets are a popular way of getting hosts on a network to talk to each other, but for most applications, they can be much too cumbersome. For instance, Berkeley-style sockets force you to worry about little details like proper byte order, and if you want to send a structure (as opposed to a raw stream of bytes), then you have to flatten the structure yourself, split it into packets if necessary, pipe it through the socket, and make sure that the other machine knows how to unpack it properly. Wouldn’t it be nice if you could simply declare the structure, pass it as a function argument as you normally would, and forget about it? This is exactly what CORBA provides, and as a bonus, it offers nifty features such as location transparency and exception handling. (Examples of each will come later.)

* INTERPROCESS COMMUNICATION (IPC). Passing messages around a program is easy. You just call a function and send your messages as parameters. Sending messages between threads is a bit trickier, but not too difficult if you’re careful about synchronization. But what if you need to send messages between two separate programs running in two separate processes? Unfortunately, there’s no easy answer. Sockets can work, but the same issues that make them ill-suited for high-level network programming also make them a poor choice for IPC. Looking elsewhere, we can find a number of alternatives in Unix, such as signals, pipes, and shared memory, but those are just as cumbersome as sockets and even less portable. CORBA, on the other hand, has become so widespread over the years that it works on almost any operating system you can name, and you can pick and choose from a variety of CORBA implementations to find one that meets your particular needs. (For instance, there is VisiBroker for large server clusters, TAO for hard real-time systems, and e*ORB for tiny embedded devices.) While CORBA might not be as fast or as simple as other IPC techniques, once you’ve learned how to use it on the network, using it for IPC is almost trivial, as we’ll see later.

* BRIDGING THE LANGUAGE DIVIDE. No programming language is ideal for every task. You might want to mix the speed of C with the simplicity of Python, or perhaps you want to link a Java front-end to a legacy library written in Ada. Most languages provide a custom way of doing this — Java offers the Java Native Interface (JNI), for example, and Python comes with the Python/C API — but generally, these bindings are limited to one or two languages, and they require that you learn a unique API for every language you want to integrate. CORBA, on the other hand, provides a single API that can link virtually any language to any other. As long as you have CORBA bindings for each language, you can combine as many of them as you like into a single program. (An example of how to link Python to Perl will come later.)

For all of these reasons, CORBA is still a useful tool, even though many alternatives exist. None of them is as mature or as widespread as CORBA, and few provide the same level of freedom from platform, language, and vendor lock-in. Plus, CORBA is an open standard that anyone can implement, and as a result, several free, open source implementations are available for Linux: ORBit and omniORB are two of the most popular.

Let’s take a closer look at these two implementations and see how they can get Linux programs talking to each other, whether they’re on the same host or on opposite sides of the globe.

Cooking with CORBA

Before diving into some actual code, let’s examine a recipe for building CORBA applications. Every CORBA application, big or small, is built by following the same basic steps:

1. CREATE THE INTERFACE DEFINITIONS. The most important step to building any non-trivial system is to decide how the components of the system interact. In CORBA, you do this by describing the interfaces in a language called — predictably enough — the Interface Definition Language, or IDL. You place these definitions (that is, the IDL files) where all components that need them can find them at compile time. IDL supports the usual primitive data types (byte, float, Boolean, and so on), as well as compound types such as structures and lists, and it includes advanced features like exceptions, interface inheritance, and custom type definitions. Table One shows the most common IDL keywords and their meanings. (For some real-world examples of IDL, check the /usr/share/idl directory of your favorite Linux distribution.)

Table One: The most common IDL keywords

keyword meaning
interface Groups a set of functions into a single entity
module Groups a set of interfaces into a single entity
attribute Specifies a data type wrapped by get/set functions
float IEEE 32-bit floating point value
double IEEE 64-bit floating point value
long 32-bit integer (“long long” is 64 bits)
short 16-bit integer
char 8-bit integer
unsigned Marks integer types as unsigned
string Array of chars
boolean True or false
any Generic holder for any data type
typedef Defines custom data types
struct Groups data types into a single entity
array Fixed-length array of a data type
sequence Variable-length array of a data type
in Declares function parameter as input only
out Declares function parameter as output only
inout Declares function parameter as both input and output

2. COMPILE THE IDL FILES. Because IDL is generic, it has to be translated into a form that your code can understand. An IDL compiler reads one or more IDL files and converts them into appropriate mappings for your language.

For instance, a function in IDL that looks like this…

short myFunction(in boolean foo,
inout float bar)

… would be converted into a C++ function that looks like this…

CORBA::Short myFunction(
CORBA::Boolean foo,
CORBA::Float& bar)

… or a Python function that’s as simple as myFunction (self, *args).

Along with these function headers, IDL compilers also generate auxiliary code called stubs and skeletons. Stubs provide a way of sending function calls to the appropriate objects, while skeletons expose the implementations of those objects to the outside world. Together they produce a concrete implementation of the abstract interfaces you defined in IDL. (In scripting languages such as Perl, stubs and skeletons are usually generated on the fly at run-time, in which case you can skip this step entirely.)

3. WRITE THE OBJECT IMPLEMENTATIONS. After creating the interfaces and generating stubs and skeletons, your objects still don’t do anything. You now have to write code that actually performs whatever services your objects — or servants, as they’re called in CORBA lingo — are supposed to provide. Because the IDL compiler and the CORBA libraries do most of the heavy lifting, this step is relatively easy. You just have to write code that performs the necessary calculations, returning values as necessary. Let CORBA handle the job of sending those values to and from other processes and around the network.

4. WRITE THE CLIENT CODE. You now have some servants fully implemented, but they won’t do any work just sitting there. Something has to request their services. In certain situations, you don’t care which clients use your servants; you simply provide the interface definitions (namely, the IDL files) and the location of the servants, and anyone can access them — depending on the CORBA security mechanisms that may be in place, of course. Other times, you’re creating these servants as part of a single integrated system and will write the clients yourself. In either case, the actual implementation of the clients doesn’t matter. You can choose the language, operating system, and CORBA libraries that meet your needs. The only common feature among these diverse clients is that they all rely on a CORBA function called narrowing (either explicitly or implicitly). Narrowing is analogous to casting in C: it converts an object handle to a specific type. We’ll see an example of narrowing a little later.

5. BOOTSTRAP THE SYSTEM. The trickiest part in deploying a CORBA application is deciding how clients should locate servants. The simplest way is through an Interoperable Object Reference (IOR). IORs are also called stringified references because they are simply a long string of hexadecimal digits preceded by the phrase IOR:. Embedded within these digits is all the information a client needs to locate the object, such as its type ID and the name of the host where it resides.

IORs are somewhat like IP addresses and have the same drawbacks: if the object changes its location, the IOR also changes. IORs are also difficult to work with because of their length and because they aren’t human-readable. (Here’s a tip: On Linux systems with the ORBit libraries installed, the command ior-decode-2 extracts the information from an IOR.)

An alternative is CORBA’s name service. Like the telephone system’s white pages, the name service provides a mapping between a cryptic IOR and an easy-to-read name. Clients can refer to servants by name, and if the servants move, they only have to update their IOR in the name service to let all clients know about it. Of course, there is still the problem of telling clients where the name service is, but because a name service is just another CORBA object, this is usually accomplished by putting the IOR of the name service in a globally accessible place, such as a file on a web server.

6. START THE SERVER AND WAIT FOR CLIENTS TO CONNECT. Once everything’s been compiled, you can run the code as you would normally, being sure to start the name service first if necessary.

At this point, we know what CORBA can do and have a general sense of how to use it, but to really understand its power and its benefits, we need to look at some real live code.

Listing One shows the IDL for a hypothetical bank account object. It defines a module called Bank containing an interface called Account, which in turn contains two functions, deposit and withdraw. The semantics of these functions should be obvious, but their syntax in IDL might not be.

Listing One: The IDL of a bank account object

1 module Bank {
2 interface Account {
3 exception Overdrawn {};
4 readonly attribute long long balance;
5 void deposit(in long amount);
6 void withdraw(in long amount) raises (Overdrawn);
7 };
8 };

At first glance, it looks like normal C syntax with a void return value and a long parameter. The unusual in keyword simply means that the parameter is pass-by-value: any changes that the servant makes to this parameter will not be visible to the client. If inout had been specified, then the parameter would be similar to pass-by-reference in C. That is, any changes the servant makes to the parameter would reflect on the client side. (The out keyword is similar, except that the servant cannot see data passed in by the client.)

The withdraw function is slightly different from deposit because it can raise an exception called Overdrawn. Exceptions in IDL map directly to traditional exception handling routines in Python, Java, and other languages, so they aren’t very mysterious. If necessary, you can specify additional state information to be carried around with the exception, but the simple Overdrawn exception leaves its body empty, as shown in line 3.

The only other piece of information in this IDL file is the balance attribute on line 4. In IDL, attributes are pieces of data that the IDL compiler automatically wraps with get and set accessor methods just for convenience. In this case, balance is marked read-only, which tells the compiler to leave out the set method so that only the servant can modify the attribute. (Incidentally, the long long in line 4 is just IDL’s quirky way of extending the 32-bit long data type to 64 bits.)

Now that we have an IDL file to play with, we can compile it and write a servant for it. That means we need to find an IDL compiler and an ORB. (CORBA implementations and their libraries are known as Object Request Brokers, or ORBs.) On Linux, ORBit is the most popular ORB, but omniORB is gaining acceptance for its speed and support for Python. Instead of choosing one over the other, we’ll use both.

A Server in Perl

Let’s start with the servant. Listing Two shows a Perl program that implements the Account object and registers it in the name service. It uses ORBit because it is one of the few ORBs that offers Perl bindings. (ORBit-specific references appear on lines 1 and 30.) It starts by loading and compiling the bank.idl and name-service.idl files automatically — no IDL compile step necessary here — and then it defines a Perl class called MyAccount that does the actual work of the deposit and withdraw functions. For instance, it throws an Overdrawn exception on line 18 if a client tries to withdraw more money than is available.

Listing Two: A bank account object implemented in Perl

1 use CORBA::ORBit idl =>
2 [ 'bank.idl', '/usr/share/idl/name-service.idl' ];
4 package MyAccount; # Implements CORBA object Bank::Account
6 use base POA_Bank::Account; # Derive from Bank::Account
8 sub new { bless {} } # No-op constructor
10 sub deposit {
11 my ($self, $amount) = @_;
12 $self->{current_balance} += $amount;
13 }
15 sub withdraw {
16 my ($self, $amount) = @_;
17 if ($amount > $self->{current_balance}) {
18 throw Bank::Account::Overdrawn
19 }
20 $self->{current_balance} -= $amount;
21 }
23 sub _get_balance {
24 $_[0]->{current_balance}; # Return the current balance
25 }
27 package main; # Implements a server for Bank::Account
29 # Initialize CORBA and the Portable Object Adapter
30 $orb = CORBA::ORB_init(“orbit-local-orb”);
31 $poa = $orb->resolve_initial_references (“RootPOA”);
32 $poa->_get_the_POAManager->activate();
34 # Create a Bank::Account object and register it with CORBA
35 $account = new MyAccount;
36 $obj = $poa->servant_to_reference($account);
38 # Register the Bank::Account object with the name service
39 $name_service = $orb->resolve_initial_references(“NameService”);
40 $name_service->rebind([{ id => "Account" }],
$obj );

42 # Wait for incoming clients to connect
43 $orb->run();

The only clue that gives away this class as a CORBA servant in disguise is its inheritance from POA_Bank::Account on line 6. The POA, which stands for Portable Object Adapter, keeps track of every servant an ORB knows about. Each CORBA servant must inherit from one of these POA base classes, but because IDL compilers create POA classes for you (as Perl has done automatically here), you don’t have worry about them. All you need to do is initialize the POA (lines 30-32), create an instance of your servant (line 35), and tell the POA that it exists (line 36).

Next, the Perl server gets a handle to the name service (line 39) and registers its servant under the name Account (line 40). Once that is done, any client that looks up the ID Account in this name service finds an IOR pointing to our Perl servant. It doesn’t matter where the servant is in relation to the client. The two could be on the same machine or thousands of miles apart. The convenience of the name service makes the locations completely transparent.

Finally, the Perl server calls ORBit’s run() method to sit and wait for incoming client requests. It doesn’t have to do anything else because CORBA handles the grunt work of responding to the requests, forwarding them to the servant, converting parameters from CORBA data types to Perl data types, and so on.

A Client in Python

Servants are useless without clients, so let’s look at a bank account client written in Python. As shown in Listing Three, CORBA weds with Python quite nicely. First, note that we are using omniORB, not ORBit, as indicated by the import statement on line 1. (ORBit has Python bindings, as well, but we want to show that different ORBs can interoperate under the guidance of CORBA.) You may wonder why IDL files are not specified here. The code assumes that the Python stubs and skeletons have already been generated by running the command omniidl -bpython bank.idl. This invokes omniORB’s IDL compiler and tells it to use the Python backend to compile bank.idl.

Listing Three: A bank account client written in Python

1 from omniORB import CORBA
2 import sys, CosNaming, Bank
4 # Initialize CORBA
5 orb = CORBA.ORB_init(sys.argv)
7 # Get a handle to the name service
8 nameService = orb.resolve_initial_references (“NameService”)
9 if nameService is None:
10 print “Error: Could not find naming service”
11 sys.exit()
13 # Locate the Bank::Account object in the name service
14 try:
15 name = [CosNaming.NameComponent("Account", "")]
16 obj = nameService.resolve(name)
17 except CosNaming.NamingContext.NotFound:
18 print “Error: Name not found”
19 sys.exit()
21 # Convert the Bank::Account object to an Account reference
22 account = obj._narrow(Bank.Account)
23 if account is None:
24 print “Object reference is not valid”
25 sys.exit(1)
27 # Use the Bank::Account object
28 try:
29 account.deposit(300)
30 account.withdraw(100)
31 print “Balance is”, account._get_balance()
32 account.withdraw(1000)
33 except Bank.Account.Overdrawn:
34 print “Account was overdrawn!”

The code begins by initializing the ORB on line 5, then getting a handle to the name service in line 8. This name service could be whatever omniORB has been configured to use, but in this case, of course, it must be set to the same IOR that ORBit used for the bank account’s name service. On lines 15 and 16, the code reaches into this name service and pulls out the object reference that matches the name Account. If everything goes correctly, this should be the same object reference that the Perl server registered when starting up.

Finally, the client narrows its generic object reference to a reference of type Bank.Account, the same type as our IDL but in Python syntax. Lines 28 through 34 give a brief demonstration of calling functions in the bank account object.

The output looks like this:

Balance is 400
Account was overdrawn!


Such is the beauty of CORBA. ORBit doesn’t have to know anything about omniORB. Perl doesn’t have to know anything about Python. And a client doesn’t have to know whether its server is local or remote. As long as everyone conforms to the CORBA standard, they can all play nicely together.

As a natural side-effect of all this integration, CORBA can also help programmers avoid reinventing the wheel. GNOME, for example, wraps commonly used components inside CORBA interfaces so that programs can share them easily. This makes the programs simpler and prevents them from duplicating the work of others.

Listing Four shows an example of how code reuse in GNOME gets a little help from CORBA. The listing is based on some C code (originally written by Miguel de Icaza and modified by Radek Doulik) that tests GNOME’s spell-checker component, gnome-spell. It obtains a reference to a gnome-spell object and uses this reference to check whether certain words are in the dictionary (lines 47 and 50) and to offer suggestions if they aren’t (lines 52 and 53). It outputs:

Is Linux in the dictionary? 1
Is Linix in the dictionary? 0
Suggestions for Linix: Linux Linus Links

Listing Four: A client written in C that calls GNOME’s spell checker component via CORBA

1 #include <gnome.h>
2 #include <liboaf/liboaf.h>
3 #include <bonobo.h>
4 #include “Spell.h”
6 #define SPELL_CLIENT_VERSION “0.1″
7 #define SPELL_API_VERSION “0.2″
9 void initBonobo(int argc, char **argv) {
10 CORBA_ORB orb;
12 gnome_init_with_popt_table(
13 “spell-client”, SPELL_CLIENT_VERSION, argc, argv,
14 oaf_popt_options, 0, NULL);
16 orb = oaf_init(argc, argv);
18 if (!bonobo_init(orb, CORBA_OBJECT_NIL, CORBA_OBJECT_NIL))
19 g_error(“Could not initialize Bonobo”);
21 bonobo_activate();
22 }
24 int main(int argc, char **argv) {
25 BonoboObjectClient *client;
26 GNOME_Spell_Dictionary dict;
27 GNOME_Spell_StringSeq *seq;
28 CORBA_Environment env;
29 int i;
31 initBonobo(argc, argv);
32 CORBA_exception_init(&env);
34 client = bonobo_object_activate(
35 “OAFIID:GNOME_Spell_Dictionary:” SPELL_API_VERSION, 0);
37 if (!client) {
38 g_error(“Could not create instance of spell checker”);
39 return 1;
40 }
42 dict = bonobo_object_corba_objref (BONOBO_OBJECT(client));
44 GNOME_Spell_Dictionary_setLanguage(dict, “en”, &env);
46 printf (“Is %s in the dictionary? %d\n”, “Linux”,
47 GNOME_Spell_Dictionary_checkWord(dict, “Linux”, &env));
49 printf(“Is %s in the dictionary? %d\n”, “Linix”,
50 GNOME_Spell_Dictionary_checkWord(dict, “Linix”, &env));
52 seq = GNOME_Spell_Dictionary_getSuggestions(
53 dict, “Linux”, &env);
55 printf(“Suggestions for Linix:\n”);
56 for (i = 0; i < seq->_length && i < 4; i++)
57 printf(“\t%s\n”, seq->_buffer[i]);
59 CORBA_free(seq);
60 CORBA_exception_free(&env);
61 bonobo_object_unref(BONOBO_OBJECT(client));
63 return 0;
64 }

As the code listing shows, GNOME’s CORBA support is actually an aberration because it forgoes the name service in favor of its own proprietary lookup and activation mechanism called Bonobo. Clients specify a name standardized by Bonobo’s Object Activation Framework (OAF), as in line 35, to retrieve a reference to the object. From there, they use stubs generated by ORBit’s IDL compiler to invoke the servant’s methods. (In this case, ORBit generated C functions such as GNOME_Spell_Dictionary_checkWord() from the Dictionary interface defined in gnome-spell‘s IDL.) Despite this difference, the process of building CORBA applications in GNOME follows the same basic recipe outlined earlier.

The previous example shows that C is certainly not the ideal language for building and using high-level CORBA components. CORBA applications written in C are extremely verbose, and due to the absence of a garbage collector, object references must be tracked very carefully to avoid memory leaks. This may explain why CORBA didn’t quite catch on when it was first introduced. At that time, C++ was the dominant language, and it shared C’s complications with CORBA.

These days, garbage-collected languages such as Python and Java are much more CORBA-friendly, and as a result, CORBA is experiencing a revival. Although it can be extremely complex if you want to use its most advanced features, mixing CORBA with the appropriate languages can greatly ease the pain of building integrated applications and distributed systems.

You can reach Trevor Harmon at trevor@vocaro.com. You can download the code used in this article from http://www.linux-mag.com/downloads/2004-03/corba.

Comments are closed.